diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml new file mode 100644 index 0000000..b040533 --- /dev/null +++ b/.github/workflows/CI.yaml @@ -0,0 +1,45 @@ +name: CI + +on: [push, pull_request] + +jobs: + fmt: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Aptos CLI + run: | + set -euo pipefail + curl -fsSL "https://aptos.dev/scripts/install_cli.py" | python3 + echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Install Aptos Move fmt + run: | + aptos update movefmt + echo "$HOME/.local/bin" >> $GITHUB_PATH + echo "FORMATTER_EXE=$HOME/.local/bin/movefmt" >> $GITHUB_ENV + + - name: Format Check + run: | + aptos move fmt + git diff --exit-code + + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Aptos CLI + run: | + set -euo pipefail + curl -fsSL "https://aptos.dev/scripts/install_cli.py" | python3 + echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Run Aptos Move Tests + run: aptos move test --dev + + - name: Run Aptos Move Linter + run: aptos move lint --dev \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5441311 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.aptos/ +build/ +.coverage_map* \ No newline at end of file diff --git a/Move.toml b/Move.toml new file mode 100644 index 0000000..82e8e24 --- /dev/null +++ b/Move.toml @@ -0,0 +1,17 @@ +[package] +name = "aptos-movekit" +version = "1.0.0" +authors = [] + +[addresses] +movekit = "_" + +[dev-addresses] +movekit = "0xCAFE" + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-framework.git" +rev = "mainnet" +subdir = "aptos-framework" + +[dev-dependencies] diff --git a/sources/access_control/ARCHITECTURE.md b/sources/access_control/ARCHITECTURE.md new file mode 100644 index 0000000..5edc365 --- /dev/null +++ b/sources/access_control/ARCHITECTURE.md @@ -0,0 +1,194 @@ +# Access Control Architecture + +## Overview + +The Aptos MoveKit Access Control system provides secure, type-safe role-based access control (RBAC) for smart contracts. It consists of two core modules that work together to manage admin privileges and role assignments. + +## Core Components + +### Modules + +1. **`access_control_admin_registry`** - Manages admin transfers using a secure two-step process +2. **`access_control_core`** - Coordinates role management and delegates admin operations + +### Data Structures + +```mermaid +classDiagram + class AdminRegistry { + +address current_admin + +Option pending_admin + } + + class RoleRegistry { + +Table roles + } + + class Admin { + <> + } + + AdminRegistry -- RoleRegistry : synchronized + RoleRegistry -- Admin : manages +``` + +## System Architecture + +```mermaid +graph TB + subgraph "Access Control Core" + Core[access_control_core] + Registry[access_control_admin_registry] + + Core --> Registry + end + + subgraph "Storage" + AdminReg[(AdminRegistry)] + RoleReg[(RoleRegistry)] + end + + subgraph "External" + Client[Client Contracts] + Admin[Admin Users] + end + + Core --> RoleReg + Registry --> AdminReg + Client --> Core + Admin --> Core + + Core -.->|Events| EventSystem[Event System] +``` + +## Admin Transfer Flow + +The system uses a secure two-step transfer process to prevent accidental admin loss: + +```mermaid +sequenceDiagram + participant CurrentAdmin + participant AdminRegistry + participant RoleRegistry + participant NewAdmin + + CurrentAdmin->>AdminRegistry: transfer_admin(new_admin) + AdminRegistry->>AdminRegistry: Set pending_admin + AdminRegistry-->>CurrentAdmin: AdminTransferProposed event + + Note over AdminRegistry: Pending state + + NewAdmin->>AdminRegistry: accept_pending_admin() + AdminRegistry->>AdminRegistry: Update current_admin + AdminRegistry->>RoleRegistry: Synchronize Admin role + RoleRegistry->>RoleRegistry: Grant Admin to new_admin + RoleRegistry->>RoleRegistry: Revoke Admin from old_admin + AdminRegistry-->>NewAdmin: AdminTransferCompleted event + RoleRegistry-->>NewAdmin: AdminRoleTransferred event +``` + +## Role Management Flow + +```mermaid +sequenceDiagram + participant Admin + participant Core + participant Registry as AdminRegistry + participant RoleRegistry + participant Target + + Admin->>Core: grant_role(target) + Core->>Core: assert_not_admin_role() + Core->>Registry: require_admin(admin) + Core->>RoleRegistry: Check !has_role(target) + Core->>RoleRegistry: grant_role_internal(target) + Core-->>Admin: RoleGranted event + + Note over Core: Similar flow for revoke_role +``` + +## Security Model + +### Protection Mechanisms + +1. **Admin Role Protection** - Admin role cannot be manually granted/revoked +2. **Two-Step Transfer** - Prevents accidental admin loss +3. **Authorization Checks** - All operations validate admin permissions +4. **Type Safety** - Phantom types ensure compile-time role verification +5. **State Validation** - Prevents duplicate assignments and missing roles + +### Access Control Matrix + +```mermaid +graph LR + subgraph "Permissions" + Admin[Admin Role] + CustomRole[Custom Roles] + end + + subgraph "Operations" + Transfer[Admin Transfer] + Grant[Grant Roles] + Revoke[Revoke Roles] + Query[Query Functions] + end + + Admin --> Transfer + Admin --> Grant + Admin --> Revoke + Admin --> Query + CustomRole --> Query +``` + +## Event System + +All operations emit events for audit trails: + +- `AdminTransferProposed` - Admin transfer initiated +- `AdminTransferCompleted` - Admin transfer completed +- `AdminTransferCanceled` - Admin transfer canceled +- `AdminRoleTransferred` - Admin role synchronized in RoleRegistry +- `RoleGranted` - Custom role granted +- `RoleRevoked` - Custom role revoked + +## Usage Patterns + +### Role Definition +```move +struct Treasurer has copy, drop {} +struct Manager has copy, drop {} +``` + +### Permission Checks +```move +public entry fun sensitive_operation(account: &signer) { + access_control_core::require_role(account); + // operation logic +} +``` + +### Admin Operations +```move +// Grant role (admin only) +access_control_core::grant_role(admin, target_address); + +// Transfer admin (two-step) +access_control_core::transfer_admin(admin, new_admin_address); +access_control_core::accept_pending_admin(new_admin); +``` + +## Design Principles + +1. **Separation of Concerns** - Admin management separate from role management +2. **Fail-Safe Defaults** - Graceful handling of uninitialized states +3. **Atomic Operations** - State changes are consistent across registries +4. **Storage Efficiency** - Cleanup of empty role maps +5. **Auditability** - Comprehensive event emission + +## Integration Points + +External contracts integrate through: +- `require_role()` for permission checks +- `has_role()` for conditional logic +- View functions for role queries +- Event listening for audit systems \ No newline at end of file diff --git a/sources/access_control/README.md b/sources/access_control/README.md new file mode 100644 index 0000000..494e74b --- /dev/null +++ b/sources/access_control/README.md @@ -0,0 +1,78 @@ +# Access Control System + +Role-based access control (RBAC) for Aptos Move contracts using phantom types. + +## Architecture + +Two modules: +- `access_control_admin_registry` - Manages admin transfers with two-step verification +- `access_control_core` - Handles role assignments and authorization + +## Usage + +### Define Roles +```move +struct Treasurer has copy, drop {} +struct Manager has copy, drop {} +``` + +### Protect Functions +```move +public entry fun withdraw(account: &signer, amount: u64) { + access_control_core::require_role(account); + // protected logic +} +``` + +### Manage Roles (Admin Only) +```move +// Grant role +access_control_core::grant_role(admin, user_address); + +// Revoke role +access_control_core::revoke_role(admin, user_address); + +// Check role +let has_role = access_control_core::has_role(user_address); +``` + +### Transfer Admin +```move +// Step 1: Current admin proposes transfer +access_control_core::transfer_admin(admin, new_admin_address); + +// Step 2: New admin accepts +access_control_core::accept_pending_admin(new_admin); +``` + +## Key Functions + +| Function | Description | +|----------|-------------| +| `require_role(account)` | Assert account has role T | +| `has_role(address)` | Check if address has role T | +| `grant_role(admin, target)` | Grant role T to target | +| `revoke_role(admin, target)` | Revoke role T from target | +| `get_roles(address)` | Get all roles for address | +| `transfer_admin(admin, new_admin)` | Propose admin transfer | +| `accept_pending_admin(new_admin)` | Accept admin transfer | + +## Events + +- `RoleGranted` - Role granted +- `RoleRevoked` - Role revoked +- `AdminTransferProposed` - Admin transfer initiated +- `AdminTransferCompleted` - Admin transfer completed + +## Error Codes + +- `E_NOT_ADMIN` (0) - Caller not admin +- `E_ALREADY_HAS_ROLE` (1) - Role already assigned +- `E_NO_SUCH_ROLE` (2) - Role not found +- `E_ADMIN_ROLE_PROTECTED` (4) - Admin role cannot be manually managed + +## Security Notes + +- Admin role is protected - only transferable via two-step process +- Built-in Admin role type cannot be granted/revoked manually +- All operations emit events for audit trails \ No newline at end of file diff --git a/sources/access_control/admin_registry.move b/sources/access_control/admin_registry.move new file mode 100644 index 0000000..1a4f1e0 --- /dev/null +++ b/sources/access_control/admin_registry.move @@ -0,0 +1,156 @@ +module movekit::access_control_admin_registry { + use std::signer; + use std::event; + use std::option::{Self, Option}; + + struct AdminRegistry has key { + current_admin: address, + pending_admin: Option
+ } + + // -- Constants -- // + const E_NOT_INITIALIZED: u64 = 0; + const E_NOT_ADMIN: u64 = 1; + const E_SELF_TRANSFER_NOT_ALLOWED: u64 = 2; + const E_NO_PENDING_ADMIN: u64 = 3; + const E_NOT_PENDING_ADMIN: u64 = 4; + const E_ALREADY_INITIALIZED: u64 = 5; + + // -- Events -- // + #[event] + struct AdminTransferProposed has copy, drop, store { + current_admin: address, + pending_admin: address + } + + #[event] + struct AdminTransferCompleted has copy, drop, store { + old_admin: address, + new_admin: address + } + + #[event] + struct AdminTransferCanceled has copy, drop, store { + admin: address, + canceled_pending: address + } + + // -- Package Functions -- // + + package fun require_admin(admin: &signer) acquires AdminRegistry { + assert!(exists(@movekit), E_NOT_INITIALIZED); + let registry = &AdminRegistry[@movekit]; + assert!(registry.current_admin == signer::address_of(admin), E_NOT_ADMIN); + } + + /// Current admin proposes new admin + package fun transfer_admin(admin: &signer, new_admin: address) acquires AdminRegistry { + require_admin(admin); + let admin_addr = signer::address_of(admin); + assert!(new_admin != admin_addr, E_SELF_TRANSFER_NOT_ALLOWED); + + // Set pending admin in registry + let registry = &mut AdminRegistry[@movekit]; + registry.pending_admin = option::some(new_admin); + + event::emit( + AdminTransferProposed { current_admin: admin_addr, pending_admin: new_admin } + ); + } + + /// New admin accepts the transfer + package fun accept_pending_admin(new_admin: &signer) acquires AdminRegistry { + let new_admin_addr = signer::address_of(new_admin); + assert!(exists(@movekit), E_NOT_INITIALIZED); + let registry = &mut AdminRegistry[@movekit]; + + // Check that there's a pending admin transfer + assert!(option::is_some(®istry.pending_admin), E_NO_PENDING_ADMIN); + + // Verify that the caller is the intended new admin + let pending_admin_addr = *option::borrow(®istry.pending_admin); + assert!(pending_admin_addr == new_admin_addr, E_NOT_PENDING_ADMIN); + + // Update registry + let old_admin = registry.current_admin; + registry.current_admin = new_admin_addr; + registry.pending_admin = option::none(); + + // Emit completion event + event::emit( + AdminTransferCompleted { old_admin: old_admin, new_admin: new_admin_addr } + ); + } + + /// Cancel pending admin transfer + package fun cancel_admin_transfer(admin: &signer) acquires AdminRegistry { + require_admin(admin); + let registry = &mut AdminRegistry[@movekit]; + assert!(option::is_some(®istry.pending_admin), E_NO_PENDING_ADMIN); + + let canceled_pending = *option::borrow(®istry.pending_admin); + registry.pending_admin = option::none(); + + event::emit( + AdminTransferCanceled { + admin: signer::address_of(admin), + canceled_pending: canceled_pending + } + ); + } + + /// Allow friend modules to initialize admin registry (idempotent) + package fun init_admin_registry(admin: &signer) { + if (!exists(@movekit)) { + let admin_addr = signer::address_of(admin); + move_to( + admin, + AdminRegistry { current_admin: admin_addr, pending_admin: option::none() } + ); + } + } + + #[view] + /// Get pending admin address + package fun get_pending_admin(): address acquires AdminRegistry { + assert!(exists(@movekit), E_NOT_INITIALIZED); + let registry = &AdminRegistry[@movekit]; + assert!(option::is_some(®istry.pending_admin), E_NO_PENDING_ADMIN); + *option::borrow(®istry.pending_admin) + } + + #[view] + /// Check if there's a pending admin transfer + package fun has_pending_admin(): bool acquires AdminRegistry { + if (!exists(@movekit)) return false; + let registry = &AdminRegistry[@movekit]; + option::is_some(®istry.pending_admin) + } + + #[view] + package fun get_current_admin(): address acquires AdminRegistry { + assert!(exists(@movekit), E_NOT_INITIALIZED); + (&AdminRegistry[@movekit]).current_admin + } + + #[view] + package fun is_current_admin(addr: address): bool acquires AdminRegistry { + get_current_admin() == addr + } + + // -- Private Functions -- // + + fun init_module(admin: &signer) { + let admin_addr = signer::address_of(admin); + // admin signer represents @movekit during deployment + move_to( + admin, + AdminRegistry { current_admin: admin_addr, pending_admin: option::none() } + ); + } + + #[test_only] + package fun init_for_testing(admin: &signer) { + init_module(admin); + } +} diff --git a/sources/access_control/core.move b/sources/access_control/core.move new file mode 100644 index 0000000..394a967 --- /dev/null +++ b/sources/access_control/core.move @@ -0,0 +1,358 @@ +module movekit::access_control_core { + + // -- Dependencies -- // + + use std::signer; + use std::event; + use std::type_info::{Self, TypeInfo}; + use aptos_std::table::{Self, Table}; + use aptos_std::ordered_map::{Self, OrderedMap}; + use std::vector; + use movekit::access_control_admin_registry; + + // -- Core Types -- // + + /// Global role registry mapping addresses to role types + struct RoleRegistry has key { + /// Maps addresses to their assigned roles + roles: Table> + } + + /// Built-in Admin role type (managed via transfer only) + struct Admin has copy, drop {} + + // -- Error Codes -- // + + /// Unauthorized access attempt - caller lacks required permissions + const E_NOT_ADMIN: u64 = 0; + /// Role already assigned to target address + const E_ALREADY_HAS_ROLE: u64 = 1; + /// Attempted to revoke non-existent role + const E_NO_SUCH_ROLE: u64 = 2; + /// Attempted operation on uninitialized system + const E_NOT_INITIALIZED: u64 = 3; + /// Admin role cannot be manually managed - use admin transfer instead + const E_ADMIN_ROLE_PROTECTED: u64 = 4; + /// State corruption detected between admin registry and role registry + const E_STATE_CORRUPTION: u64 = 5; + + // -- Events -- // + + #[event] + /// Emitted when a role is successfully granted to an address + struct RoleGranted has copy, drop, store { + /// Address of admin who granted the role + admin: address, + /// Address that received the role + target: address, + /// Type information of the granted role + role: TypeInfo + } + + #[event] + /// Emitted when a role is successfully revoked from an address + struct RoleRevoked has copy, drop, store { + /// Address of admin who revoked the role + admin: address, + /// Address that lost the role + target: address + } + + #[event] + /// Emitted when admin role is transferred to a new admin + struct AdminRoleTransferred has copy, drop, store { + /// Previous admin who lost Admin role + old_admin: address, + /// New admin who gained Admin role + new_admin: address + } + + // -- Package Functions -- // + + /// Propose admin transfer - delegates to admin registry + package fun transfer_admin(admin: &signer, new_admin: address) { + access_control_admin_registry::transfer_admin(admin, new_admin) + } + + /// Accept pending admin transfer and synchronize role assignments + package fun accept_pending_admin(new_admin: &signer) acquires RoleRegistry { + let new_admin_addr = signer::address_of(new_admin); + + // Capture current state before any modifications + let current_admin_addr = access_control_admin_registry::get_current_admin(); + + // Validate pending transfer exists and matches caller + assert!( + access_control_admin_registry::has_pending_admin(), + E_NOT_INITIALIZED + ); + assert!( + access_control_admin_registry::get_pending_admin() == new_admin_addr, + E_NOT_ADMIN + ); + + // Execute admin transfer atomically + access_control_admin_registry::accept_pending_admin(new_admin); + + // Synchronize Admin role assignments to maintain consistency + synchronize_admin_role(current_admin_addr, new_admin_addr); + + // Emit synchronization event for audit trail + event::emit( + AdminRoleTransferred { + old_admin: current_admin_addr, + new_admin: new_admin_addr + } + ); + } + + /// Cancel pending admin transfer - delegates to admin registry + package fun cancel_admin_transfer(admin: &signer) { + access_control_admin_registry::cancel_admin_transfer(admin) + } + + // -- Role Management Functions -- // + + /// Grant role to target address (Admin role; admin-only) + public fun grant_role(admin: &signer, target: address) acquires RoleRegistry { + // Security: Prevent manual Admin role manipulation + assert_not_admin_role(); + + // Authorize admin access + require_admin(admin); + + // Validate role assignment + assert!(!has_role(target), E_ALREADY_HAS_ROLE); + + // Execute role grant + grant_role_internal(target); + + // Emit audit event + event::emit( + RoleGranted { + admin: signer::address_of(admin), + target: target, + role: type_info::type_of() + } + ); + } + + /// Revoke role from target address (Admin role; admin-only) + public fun revoke_role(admin: &signer, target: address) acquires RoleRegistry { + // Security: Prevent manual Admin role manipulation + assert_not_admin_role(); + + // Authorize admin access + require_admin(admin); + + // Validate role exists + assert!(has_role(target), E_NO_SUCH_ROLE); + + // Execute role revocation + revoke_role_internal(target); + + // Emit audit event + event::emit( + RoleRevoked { admin: signer::address_of(admin), target: target } + ); + } + + // -- Public functions -- // + + /// Assert caller has required role or abort with clear error + /// Useful for other modules requiring specific role authorization + public fun require_role(account: &signer) acquires RoleRegistry { + assert!( + has_role(signer::address_of(account)), + E_NO_SUCH_ROLE + ); + } + + // -- View Functions -- // + + #[view] + /// Check if address has a specific role + /// Returns false if the registry or user entry does not exist + public fun has_role(addr: address): bool acquires RoleRegistry { + // Handle uninitialized system gracefully + if (!exists(@movekit)) return false; + + let registry = &RoleRegistry[@movekit]; + + // Handle non-existent user gracefully + if (!registry.roles.contains(addr)) return false; + + let user_roles = registry.roles.borrow(addr); + let target_type = type_info::type_of(); + + user_roles.contains(&target_type) + } + + #[view] + /// Get current admin address from admin registry + public fun get_current_admin(): address { + access_control_admin_registry::get_current_admin() + } + + #[view] + /// Check if given address is the current admin + public fun is_current_admin(addr: address): bool { + access_control_admin_registry::is_current_admin(addr) + } + + #[view] + /// Get all roles assigned to an address in sorted order + /// Returns empty vector for uninitialized system or non-existent users + public fun get_roles(addr: address): vector acquires RoleRegistry { + // Handle uninitialized system gracefully + if (!exists(@movekit)) return vector::empty(); + + let registry = &RoleRegistry[@movekit]; + + // Handle non-existent user gracefully + if (!registry.roles.contains(addr)) return vector::empty(); + + let user_roles = registry.roles.borrow(addr); + + // Extract keys from OrderedMap (automatically sorted) + ordered_map::keys(user_roles) + } + + #[view] + /// Count total roles assigned to an address + public fun get_role_count(addr: address): u64 acquires RoleRegistry { + // Handle uninitialized system gracefully + if (!exists(@movekit)) return 0; + + let registry = &RoleRegistry[@movekit]; + + // Handle non-existent user gracefully + if (!registry.roles.contains(addr)) return 0; + + let user_roles = registry.roles.borrow(addr); + + ordered_map::length(user_roles) + } + + #[view] + /// Get pending admin address from admin registry + public fun get_pending_admin(): address { + access_control_admin_registry::get_pending_admin() + } + + #[view] + /// Check if admin has pending transfer + public fun has_pending_admin(): bool { + access_control_admin_registry::has_pending_admin() + } + + // -- Internal Implementation -- // + + /// Synchronize Admin role during admin transfer + /// Ensures exactly one Admin role exists and belongs to current admin + fun synchronize_admin_role(old_admin: address, new_admin: address) acquires RoleRegistry { + // Validate registry is initialized + assert!(exists(@movekit), E_NOT_INITIALIZED); + + // Grant Admin role to new admin (safe - handles duplicates) + grant_role_internal(new_admin); + + // Revoke Admin role from old admin (safe - handles non-existence) + revoke_role_internal(old_admin); + + // Verify state consistency after synchronization + assert!(has_role(new_admin), E_STATE_CORRUPTION); + assert!(!has_role(old_admin), E_STATE_CORRUPTION); + } + + /// Internal role granting with duplicate protection + fun grant_role_internal(target: address) acquires RoleRegistry { + let registry = &mut RoleRegistry[@movekit]; + let role_type = type_info::type_of(); + + // Initialize user's role map if needed + if (!registry.roles.contains(target)) { + registry.roles.add(target, ordered_map::new()); + }; + + let user_roles = registry.roles.borrow_mut(target); + + // Only add role if not already present (idempotent operation) + if (!user_roles.contains(&role_type)) { + user_roles.add(role_type, true); + } + } + + /// Internal role revocation with non-existence protection + fun revoke_role_internal(target: address) acquires RoleRegistry { + let registry = &mut RoleRegistry[@movekit]; + let role_type = type_info::type_of(); + + // Handle non-existent user gracefully + if (!registry.roles.contains(target)) return; + + let user_roles = registry.roles.borrow_mut(target); + + // Only remove if role exists (idempotent operation) + if (user_roles.contains(&role_type)) { + user_roles.remove(&role_type); + + // Clean up empty role maps to save storage + if (ordered_map::is_empty(user_roles)) { + let empty_map = registry.roles.remove(target); + ordered_map::destroy_empty(empty_map); + } + } + } + + /// Require admin authorization with clear error messaging + fun require_admin(admin: &signer) { + access_control_admin_registry::require_admin(admin); + } + + /// Security check: prevent manual Admin role manipulation + fun assert_not_admin_role() { + assert!( + type_info::type_of() != type_info::type_of(), + E_ADMIN_ROLE_PROTECTED + ); + } + + /// System initialization - creates role registry and grants initial Admin role + fun init_module(admin: &signer) acquires RoleRegistry { + let admin_addr = signer::address_of(admin); + + // Initialize admin registry first (idempotent operation) + access_control_admin_registry::init_admin_registry(admin); + + // Create role registry if not already exists + if (!exists(@movekit)) { + move_to( + admin, + RoleRegistry { + roles: table::new>() + } + ); + }; + + // Grant initial Admin role to deployer + grant_role_internal(admin_addr); + + // Emit initial role grant event + event::emit( + RoleGranted { + admin: admin_addr, + target: admin_addr, + role: type_info::type_of() + } + ); + } + + // -- Testing Support -- // + + #[test_only] + /// Initialize system for testing purposes + package fun init_for_testing(admin: &signer) acquires RoleRegistry { + init_module(admin); + } +} diff --git a/tests/access_control/admin_registry_test.move b/tests/access_control/admin_registry_test.move new file mode 100644 index 0000000..3297aa6 --- /dev/null +++ b/tests/access_control/admin_registry_test.move @@ -0,0 +1,801 @@ +#[test_only] +module movekit::access_control_admin_registry_tests { + use std::signer; + use movekit::access_control_admin_registry; + + // Test constants matching the module + const E_NOT_INITIALIZED: u64 = 0; + const E_NOT_ADMIN: u64 = 1; + const E_SELF_TRANSFER_NOT_ALLOWED: u64 = 2; + const E_NO_PENDING_ADMIN: u64 = 3; + const E_NOT_PENDING_ADMIN: u64 = 4; + const E_ALREADY_INITIALIZED: u64 = 5; + + // =========================================== + // INITIALIZATION TESTS + // =========================================== + + #[test(deployer = @movekit)] + fun test_init_module_success(deployer: &signer) { + access_control_admin_registry::init_for_testing(deployer); + + let deployer_addr = signer::address_of(deployer); + + // Should set deployer as current admin + assert!(access_control_admin_registry::get_current_admin() == deployer_addr); + assert!(access_control_admin_registry::is_current_admin(deployer_addr)); + + // No pending admin initially + assert!(!access_control_admin_registry::has_pending_admin()); + } + + #[test(deployer = @movekit)] + #[expected_failure] + fun test_double_initialization_fails(deployer: &signer) { + access_control_admin_registry::init_for_testing(deployer); + // Second initialization should fail + access_control_admin_registry::init_for_testing(deployer); + } + + #[test] + #[ + expected_failure( + abort_code = E_NOT_INITIALIZED, + location = movekit::access_control_admin_registry + ) + ] + fun test_get_current_admin_not_initialized() { + // Should fail when not initialized + access_control_admin_registry::get_current_admin(); + } + + #[test] + #[ + expected_failure( + abort_code = E_NOT_INITIALIZED, + location = movekit::access_control_admin_registry + ) + ] + fun test_is_current_admin_not_initialized() { + // Should fail when not initialized (now that is_current_admin calls get_current_admin) + access_control_admin_registry::is_current_admin(@0x123); + } + + // =========================================== + // ADMIN CHECK TESTS + // =========================================== + + #[test(deployer = @movekit)] + fun test_require_admin_success(deployer: &signer) { + access_control_admin_registry::init_for_testing(deployer); + + // Should not abort for actual admin + access_control_admin_registry::require_admin(deployer); + } + + #[test(deployer = @movekit)] + #[ + expected_failure( + abort_code = E_NOT_INITIALIZED, + location = movekit::access_control_admin_registry + ) + ] + fun test_require_admin_not_initialized(deployer: &signer) { + access_control_admin_registry::require_admin(deployer); + } + + #[test(deployer = @movekit, non_admin = @0x123)] + #[ + expected_failure( + abort_code = E_NOT_ADMIN, location = movekit::access_control_admin_registry + ) + ] + fun test_require_admin_fails_for_non_admin( + deployer: &signer, non_admin: &signer + ) { + access_control_admin_registry::init_for_testing(deployer); + + // Should fail for non-admin + access_control_admin_registry::require_admin(non_admin); + } + + #[test(deployer = @movekit)] + fun test_is_current_admin_various_addresses(deployer: &signer) { + access_control_admin_registry::init_for_testing(deployer); + + let deployer_addr = signer::address_of(deployer); + + assert!(access_control_admin_registry::is_current_admin(deployer_addr)); + assert!(!access_control_admin_registry::is_current_admin(@0x123)); + assert!(!access_control_admin_registry::is_current_admin(@0x456)); + } + + // =========================================== + // TWO-STEP TRANSFER TESTS + // =========================================== + + #[test(admin = @movekit, new_admin = @0x123)] + fun test_transfer_admin_complete_flow( + admin: &signer, new_admin: &signer + ) { + access_control_admin_registry::init_for_testing(admin); + + let admin_addr = signer::address_of(admin); + let new_admin_addr = signer::address_of(new_admin); + + // Step 1: Propose transfer + access_control_admin_registry::transfer_admin(admin, new_admin_addr); + + // Check pending state + assert!(access_control_admin_registry::has_pending_admin()); + assert!(access_control_admin_registry::get_pending_admin() == new_admin_addr); + + // Admin should still be current + assert!(access_control_admin_registry::get_current_admin() == admin_addr); + assert!(access_control_admin_registry::is_current_admin(admin_addr)); + assert!(!access_control_admin_registry::is_current_admin(new_admin_addr)); + + // Step 2: Accept transfer + access_control_admin_registry::accept_pending_admin(new_admin); + + // Check transfer completed + assert!(access_control_admin_registry::get_current_admin() == new_admin_addr); + assert!(access_control_admin_registry::is_current_admin(new_admin_addr)); + assert!(!access_control_admin_registry::is_current_admin(admin_addr)); + + // Check pending admin cleaned up + assert!(!access_control_admin_registry::has_pending_admin()); + } + + #[test(admin = @movekit, new_admin = @0x123)] + fun test_transfer_admin_overwrite_proposal( + admin: &signer, new_admin: &signer + ) { + access_control_admin_registry::init_for_testing(admin); + + let new_admin_addr = signer::address_of(new_admin); + let another_addr = @0x456; + + // First proposal + access_control_admin_registry::transfer_admin(admin, new_admin_addr); + assert!(access_control_admin_registry::get_pending_admin() == new_admin_addr); + + // Second proposal overwrites first + access_control_admin_registry::transfer_admin(admin, another_addr); + assert!(access_control_admin_registry::get_pending_admin() == another_addr); + + // Old proposal is no longer valid + assert!(access_control_admin_registry::get_pending_admin() != new_admin_addr); + } + + #[test(admin = @movekit, new_admin = @0x123)] + fun test_cancel_admin_transfer(admin: &signer, new_admin: &signer) { + access_control_admin_registry::init_for_testing(admin); + + let admin_addr = signer::address_of(admin); + let new_admin_addr = signer::address_of(new_admin); + + // Propose transfer + access_control_admin_registry::transfer_admin(admin, new_admin_addr); + assert!(access_control_admin_registry::has_pending_admin()); + + // Cancel transfer + access_control_admin_registry::cancel_admin_transfer(admin); + + // Check admin unchanged + assert!(access_control_admin_registry::get_current_admin() == admin_addr); + assert!(access_control_admin_registry::is_current_admin(admin_addr)); + + // Check pending admin cleaned up + assert!(!access_control_admin_registry::has_pending_admin()); + } + + // =========================================== + // ERROR CONDITION TESTS + // =========================================== + + #[test(admin = @movekit, non_admin = @0x123)] + #[ + expected_failure( + abort_code = E_NOT_ADMIN, location = movekit::access_control_admin_registry + ) + ] + fun test_transfer_admin_not_admin( + admin: &signer, non_admin: &signer + ) { + access_control_admin_registry::init_for_testing(admin); + access_control_admin_registry::transfer_admin(non_admin, @0x456); + } + + #[test(admin = @movekit)] + #[ + expected_failure( + abort_code = E_SELF_TRANSFER_NOT_ALLOWED, + location = movekit::access_control_admin_registry + ) + ] + fun test_transfer_admin_to_self(admin: &signer) { + access_control_admin_registry::init_for_testing(admin); + let admin_addr = signer::address_of(admin); + + // Should fail - cannot transfer to self + access_control_admin_registry::transfer_admin(admin, admin_addr); + } + + #[test(admin = @movekit, new_admin = @0x123, wrong_admin = @0x456)] + #[ + expected_failure( + abort_code = E_NOT_PENDING_ADMIN, + location = movekit::access_control_admin_registry + ) + ] + fun test_accept_pending_admin_wrong_address( + admin: &signer, new_admin: &signer, wrong_admin: &signer + ) { + access_control_admin_registry::init_for_testing(admin); + let new_admin_addr = signer::address_of(new_admin); + + // Propose transfer to new_admin + access_control_admin_registry::transfer_admin(admin, new_admin_addr); + + // Should fail - wrong_admin tries to accept + access_control_admin_registry::accept_pending_admin(wrong_admin); + } + + #[test(new_admin = @0x123)] + #[ + expected_failure( + abort_code = E_NOT_INITIALIZED, + location = movekit::access_control_admin_registry + ) + ] + fun test_accept_pending_admin_not_initialized(new_admin: &signer) { + // Should fail - admin registry not initialized + access_control_admin_registry::accept_pending_admin(new_admin); + } + + #[test(admin = @movekit, new_admin = @0x123)] + #[ + expected_failure( + abort_code = E_NO_PENDING_ADMIN, + location = movekit::access_control_admin_registry + ) + ] + fun test_accept_pending_admin_no_pending( + admin: &signer, new_admin: &signer + ) { + access_control_admin_registry::init_for_testing(admin); + + // Should fail - no pending admin transfer + access_control_admin_registry::accept_pending_admin(new_admin); + } + + #[test(admin = @movekit, non_admin = @0x123)] + #[ + expected_failure( + abort_code = E_NOT_ADMIN, location = movekit::access_control_admin_registry + ) + ] + fun test_cancel_admin_transfer_not_admin( + admin: &signer, non_admin: &signer + ) { + access_control_admin_registry::init_for_testing(admin); + // Should fail - not admin + access_control_admin_registry::cancel_admin_transfer(non_admin); + } + + #[test(admin = @movekit)] + #[ + expected_failure( + abort_code = E_NO_PENDING_ADMIN, + location = movekit::access_control_admin_registry + ) + ] + fun test_cancel_admin_transfer_no_pending(admin: &signer) { + access_control_admin_registry::init_for_testing(admin); + + // Should fail - no pending transfer to cancel + access_control_admin_registry::cancel_admin_transfer(admin); + } + + #[test(admin = @movekit)] + #[ + expected_failure( + abort_code = E_NO_PENDING_ADMIN, + location = movekit::access_control_admin_registry + ) + ] + fun test_get_pending_admin_no_pending(admin: &signer) { + access_control_admin_registry::init_for_testing(admin); + // Should fail - no pending admin + access_control_admin_registry::get_pending_admin(); + } + + // =========================================== + // CORNER CASE TESTS + // =========================================== + + #[test(admin = @movekit, new_admin = @0x123)] + #[ + expected_failure( + abort_code = E_NO_PENDING_ADMIN, + location = movekit::access_control_admin_registry + ) + ] + fun test_double_accept_pending_admin_fails( + admin: &signer, new_admin: &signer + ) { + access_control_admin_registry::init_for_testing(admin); + let new_admin_addr = signer::address_of(new_admin); + + // Normal transfer + access_control_admin_registry::transfer_admin(admin, new_admin_addr); + + // First accept succeeds + access_control_admin_registry::accept_pending_admin(new_admin); + + // Second accept must fail + access_control_admin_registry::accept_pending_admin(new_admin); + } + + #[test(admin = @movekit, new_admin = @0x123)] + #[ + expected_failure( + abort_code = E_NOT_PENDING_ADMIN, + location = movekit::access_control_admin_registry + ) + ] + fun test_accept_after_overwrite_fails( + admin: &signer, new_admin: &signer + ) { + access_control_admin_registry::init_for_testing(admin); + let new_admin_addr = signer::address_of(new_admin); + + // Original proposal + access_control_admin_registry::transfer_admin(admin, new_admin_addr); + + // Admin overwrites with different address + access_control_admin_registry::transfer_admin(admin, @0x456); + + // Original new_admin can no longer accept + access_control_admin_registry::accept_pending_admin(new_admin); + } + + #[test(admin = @movekit, new_admin1 = @0x123, new_admin2 = @0x456)] + fun test_transfer_chain( + admin: &signer, new_admin1: &signer, new_admin2: &signer + ) { + access_control_admin_registry::init_for_testing(admin); + + let admin_addr = signer::address_of(admin); + let new_admin1_addr = signer::address_of(new_admin1); + let new_admin2_addr = signer::address_of(new_admin2); + + // Transfer 1: admin -> new_admin1 + access_control_admin_registry::transfer_admin(admin, new_admin1_addr); + access_control_admin_registry::accept_pending_admin(new_admin1); + + assert!(access_control_admin_registry::get_current_admin() == new_admin1_addr); + assert!(!access_control_admin_registry::is_current_admin(admin_addr)); + + // Transfer 2: new_admin1 -> new_admin2 + access_control_admin_registry::transfer_admin(new_admin1, new_admin2_addr); + access_control_admin_registry::accept_pending_admin(new_admin2); + + assert!(access_control_admin_registry::get_current_admin() == new_admin2_addr); + assert!(!access_control_admin_registry::is_current_admin(new_admin1_addr)); + assert!(!access_control_admin_registry::is_current_admin(admin_addr)); + } + + // =========================================== + // SPECIAL ADDRESS TESTS + // =========================================== + + #[test(admin = @movekit)] + fun test_transfer_to_zero_address(admin: &signer) { + access_control_admin_registry::init_for_testing(admin); + + // Should allow transfer to zero address (for renouncing) + access_control_admin_registry::transfer_admin(admin, @0x0); + + assert!(access_control_admin_registry::has_pending_admin()); + assert!(access_control_admin_registry::get_pending_admin() == @0x0); + } + + #[test(admin = @movekit)] + #[ + expected_failure( + abort_code = E_SELF_TRANSFER_NOT_ALLOWED, + location = movekit::access_control_admin_registry + ) + ] + fun test_self_transfer_module_address(admin: &signer) { + access_control_admin_registry::init_for_testing(admin); + + // Should fail - transferring to @movekit when admin is @movekit is self-transfer + access_control_admin_registry::transfer_admin(admin, @movekit); + } + + // =========================================== + // VIEW FUNCTION TESTS + // =========================================== + + #[test(admin = @movekit)] + fun test_has_pending_admin_various_states(admin: &signer) { + access_control_admin_registry::init_for_testing(admin); + + // Initially no pending admin + assert!(!access_control_admin_registry::has_pending_admin()); + + // After proposing transfer + access_control_admin_registry::transfer_admin(admin, @0x123); + assert!(access_control_admin_registry::has_pending_admin()); + + // After canceling + access_control_admin_registry::cancel_admin_transfer(admin); + assert!(!access_control_admin_registry::has_pending_admin()); + } + + #[test] + fun test_has_pending_admin_nonexistent_address() { + // Should return false for uninitialized registry + assert!(!access_control_admin_registry::has_pending_admin()); + } + + // =========================================== + // SECURITY TESTS + // =========================================== + + #[test(admin = @movekit, attacker = @0x999)] + #[ + expected_failure( + abort_code = E_NOT_ADMIN, location = movekit::access_control_admin_registry + ) + ] + fun test_privilege_escalation_prevention_transfer( + admin: &signer, attacker: &signer + ) { + access_control_admin_registry::init_for_testing(admin); + // Attacker tries to transfer admin to themselves without being admin + access_control_admin_registry::transfer_admin( + attacker, signer::address_of(attacker) + ); + } + + #[test(admin = @movekit, attacker = @0x999)] + #[ + expected_failure( + abort_code = E_NOT_ADMIN, location = movekit::access_control_admin_registry + ) + ] + fun test_privilege_escalation_prevention_cancel( + admin: &signer, attacker: &signer + ) { + access_control_admin_registry::init_for_testing(admin); + // Attacker tries to cancel admin transfer without being admin + access_control_admin_registry::cancel_admin_transfer(attacker); + } + + #[test(admin = @movekit, attacker = @0x999)] + #[ + expected_failure( + abort_code = E_NOT_PENDING_ADMIN, + location = movekit::access_control_admin_registry + ) + ] + fun test_unauthorized_accept_prevention( + admin: &signer, attacker: &signer + ) { + access_control_admin_registry::init_for_testing(admin); + + // Admin proposes transfer to someone else + access_control_admin_registry::transfer_admin(admin, @0x123); + + // Attacker tries to accept transfer meant for someone else + access_control_admin_registry::accept_pending_admin(attacker); + } + + // =========================================== + // EDGE CASE TESTS + // =========================================== + + #[test(admin = @movekit)] + fun test_multiple_proposal_overwrites(admin: &signer) { + access_control_admin_registry::init_for_testing(admin); + + // Multiple proposals should overwrite each other + access_control_admin_registry::transfer_admin(admin, @0x111); + assert!(access_control_admin_registry::get_pending_admin() == @0x111); + + access_control_admin_registry::transfer_admin(admin, @0x222); + assert!(access_control_admin_registry::get_pending_admin() == @0x222); + + access_control_admin_registry::transfer_admin(admin, @0x333); + assert!(access_control_admin_registry::get_pending_admin() == @0x333); + + // Only the last proposal is valid + assert!(access_control_admin_registry::get_pending_admin() != @0x111); + assert!(access_control_admin_registry::get_pending_admin() != @0x222); + } + + #[test(admin = @movekit, new_admin = @0x123)] + fun test_pending_admin_cleanup_after_accept( + admin: &signer, new_admin: &signer + ) { + access_control_admin_registry::init_for_testing(admin); + + let new_admin_addr = signer::address_of(new_admin); + + // Propose and accept transfer + access_control_admin_registry::transfer_admin(admin, new_admin_addr); + access_control_admin_registry::accept_pending_admin(new_admin); + + // No longer has pending admin after transfer + assert!(!access_control_admin_registry::has_pending_admin()); + assert!(!access_control_admin_registry::has_pending_admin()); + } + + #[test(admin = @movekit, new_admin = @0x123)] + fun test_pending_admin_cleanup_after_cancel( + admin: &signer, new_admin: &signer + ) { + access_control_admin_registry::init_for_testing(admin); + + let new_admin_addr = signer::address_of(new_admin); + + // Propose and cancel transfer + access_control_admin_registry::transfer_admin(admin, new_admin_addr); + access_control_admin_registry::cancel_admin_transfer(admin); + + // No longer has pending admin after cancellation + assert!(!access_control_admin_registry::has_pending_admin()); + assert!(!access_control_admin_registry::has_pending_admin()); + } + + // =========================================== + // STORAGE LEAK CHECKS + // =========================================== + + #[test(admin = @movekit, new_admin = @0x123)] + fun test_no_pending_admin_leaks_after_accept( + admin: &signer, new_admin: &signer + ) { + access_control_admin_registry::init_for_testing(admin); + + let new_admin_addr = signer::address_of(new_admin); + + // Setup transfer + access_control_admin_registry::transfer_admin(admin, new_admin_addr); + + // Verify pending admin exists + assert!(access_control_admin_registry::has_pending_admin()); + + // Accept transfer + access_control_admin_registry::accept_pending_admin(new_admin); + + // Verify NO pending admin state exists + assert!(!access_control_admin_registry::has_pending_admin()); + assert!(!access_control_admin_registry::has_pending_admin()); + } + + #[test(admin = @movekit)] + fun test_no_pending_admin_leaks_after_cancel(admin: &signer) { + access_control_admin_registry::init_for_testing(admin); + + // Setup transfer + access_control_admin_registry::transfer_admin(admin, @0x123); + + // Verify pending admin exists + assert!(access_control_admin_registry::has_pending_admin()); + + // Cancel transfer + access_control_admin_registry::cancel_admin_transfer(admin); + + // Verify NO pending admin state exists + assert!(!access_control_admin_registry::has_pending_admin()); + } + + #[test(admin = @movekit)] + fun test_no_pending_admin_leaks_after_overwrite(admin: &signer) { + access_control_admin_registry::init_for_testing(admin); + + // Multiple overwrites + access_control_admin_registry::transfer_admin(admin, @0x111); + access_control_admin_registry::transfer_admin(admin, @0x222); + access_control_admin_registry::transfer_admin(admin, @0x333); + + // Should only have ONE pending admin (the latest) + assert!(access_control_admin_registry::has_pending_admin()); + assert!(access_control_admin_registry::get_pending_admin() == @0x333); + + // Cancel and verify clean slate + access_control_admin_registry::cancel_admin_transfer(admin); + assert!(!access_control_admin_registry::has_pending_admin()); + } + + // =========================================== + // CONCURRENT/RAPID SUCCESSION TESTS + // =========================================== + + #[test(admin = @movekit)] + fun test_rapid_proposal_overwrites(admin: &signer) { + access_control_admin_registry::init_for_testing(admin); + + // Rapid succession proposals (testing internal state consistency) + access_control_admin_registry::transfer_admin(admin, @0x111); + assert!(access_control_admin_registry::get_pending_admin() == @0x111); + + access_control_admin_registry::transfer_admin(admin, @0x222); + assert!(access_control_admin_registry::get_pending_admin() == @0x222); + + access_control_admin_registry::transfer_admin(admin, @0x333); + assert!(access_control_admin_registry::get_pending_admin() == @0x333); + + // Verify only the last one is valid + assert!(access_control_admin_registry::get_pending_admin() != @0x111); + assert!(access_control_admin_registry::get_pending_admin() != @0x222); + } + + #[test(admin = @movekit, new_admin = @0x123)] + fun test_propose_cancel_propose_sequence( + admin: &signer, new_admin: &signer + ) { + access_control_admin_registry::init_for_testing(admin); + + let new_admin_addr = signer::address_of(new_admin); + + // Rapid sequence: propose -> cancel -> propose -> accept + access_control_admin_registry::transfer_admin(admin, new_admin_addr); + access_control_admin_registry::cancel_admin_transfer(admin); + access_control_admin_registry::transfer_admin(admin, new_admin_addr); + access_control_admin_registry::accept_pending_admin(new_admin); + + // Verify final state is correct + assert!(access_control_admin_registry::get_current_admin() == new_admin_addr); + assert!(!access_control_admin_registry::has_pending_admin()); + } + + #[test(admin = @movekit, new_admin = @0x123)] + fun test_interleaved_operations(admin: &signer, new_admin: &signer) { + access_control_admin_registry::init_for_testing(admin); + + let new_admin_addr = signer::address_of(new_admin); + + // Complex sequence testing state machine robustness + access_control_admin_registry::transfer_admin(admin, @0x999); // Propose to someone else + access_control_admin_registry::transfer_admin(admin, new_admin_addr); // Overwrite + + // Verify state is correct before accept + assert!(access_control_admin_registry::get_pending_admin() == new_admin_addr); + assert!( + access_control_admin_registry::get_current_admin() + == signer::address_of(admin) + ); + + // Accept should work + access_control_admin_registry::accept_pending_admin(new_admin); + + // Verify final state + assert!(access_control_admin_registry::get_current_admin() == new_admin_addr); + } + + // =========================================== + // EDGE CASE OPERATIONAL TESTS + // =========================================== + + #[test(admin = @movekit)] + fun test_view_functions_consistency_under_state_changes( + admin: &signer + ) { + access_control_admin_registry::init_for_testing(admin); + + let admin_addr = signer::address_of(admin); + + // Initial state verification + assert!(access_control_admin_registry::is_current_admin(admin_addr)); + assert!(access_control_admin_registry::get_current_admin() == admin_addr); + assert!(!access_control_admin_registry::has_pending_admin()); + + // After proposal + access_control_admin_registry::transfer_admin(admin, @0x123); + assert!(access_control_admin_registry::is_current_admin(admin_addr)); // Still current + assert!(access_control_admin_registry::get_current_admin() == admin_addr); // Still current + assert!(access_control_admin_registry::has_pending_admin()); // Now has pending + + // After cancel + access_control_admin_registry::cancel_admin_transfer(admin); + assert!(access_control_admin_registry::is_current_admin(admin_addr)); // Still current + assert!(access_control_admin_registry::get_current_admin() == admin_addr); // Still current + assert!(!access_control_admin_registry::has_pending_admin()); // No longer pending + } + + #[test(admin = @movekit)] + fun test_resource_existence_consistency(admin: &signer) { + access_control_admin_registry::init_for_testing(admin); + + // AdminRegistry should always exist after initialization + assert!( + access_control_admin_registry::is_current_admin(signer::address_of(admin)), + 0 + ); + + // These operations shouldn't affect AdminRegistry existence + access_control_admin_registry::transfer_admin(admin, @0x123); + assert!( + access_control_admin_registry::is_current_admin(signer::address_of(admin)), + 1 + ); + + access_control_admin_registry::cancel_admin_transfer(admin); + assert!( + access_control_admin_registry::is_current_admin(signer::address_of(admin)), + 2 + ); + + // Pending admin state should be created and destroyed properly + assert!(!access_control_admin_registry::has_pending_admin()); + + access_control_admin_registry::transfer_admin(admin, @0x123); + assert!(access_control_admin_registry::has_pending_admin()); + + access_control_admin_registry::cancel_admin_transfer(admin); + assert!(!access_control_admin_registry::has_pending_admin()); + } + + // =========================================== + // SYSTEM INVARIANT TESTS + // =========================================== + + #[test(admin = @movekit, new_admin = @0x123)] + fun test_admin_uniqueness_invariant( + admin: &signer, new_admin: &signer + ) { + access_control_admin_registry::init_for_testing(admin); + + let admin_addr = signer::address_of(admin); + let new_admin_addr = signer::address_of(new_admin); + + // Throughout the entire transfer process, exactly one address should be admin + + // Initial: admin is admin, new_admin is not + assert!(access_control_admin_registry::is_current_admin(admin_addr)); + assert!(!access_control_admin_registry::is_current_admin(new_admin_addr)); + + // During proposal: admin still admin, new_admin still not + access_control_admin_registry::transfer_admin(admin, new_admin_addr); + assert!(access_control_admin_registry::is_current_admin(admin_addr)); + assert!(!access_control_admin_registry::is_current_admin(new_admin_addr)); + + // After transfer: new_admin is admin, admin is not + access_control_admin_registry::accept_pending_admin(new_admin); + assert!(!access_control_admin_registry::is_current_admin(admin_addr)); + assert!(access_control_admin_registry::is_current_admin(new_admin_addr)); + + // Verify no other addresses are admin + assert!(!access_control_admin_registry::is_current_admin(@0x999)); + assert!(!access_control_admin_registry::is_current_admin(@movekit)); + } + + #[test(admin = @movekit)] + fun test_state_machine_invariants(admin: &signer) { + access_control_admin_registry::init_for_testing(admin); + + let admin_addr = signer::address_of(admin); + + // Invariant: AdminRegistry always exists after initialization + assert!(access_control_admin_registry::get_current_admin() == admin_addr); + + // Invariant: Pending admin state exists iff there's an active proposal + assert!(!access_control_admin_registry::has_pending_admin()); + + access_control_admin_registry::transfer_admin(admin, @0x123); + assert!(access_control_admin_registry::has_pending_admin()); + + access_control_admin_registry::cancel_admin_transfer(admin); + assert!(!access_control_admin_registry::has_pending_admin()); + + // Invariant: get_current_admin() always returns a valid address + let current = access_control_admin_registry::get_current_admin(); + assert!(current == admin_addr); // Should be a concrete address, not null + } +} diff --git a/tests/access_control/core_test.move b/tests/access_control/core_test.move new file mode 100644 index 0000000..424918d --- /dev/null +++ b/tests/access_control/core_test.move @@ -0,0 +1,825 @@ +#[test_only] +module movekit::access_control_core_tests { + use std::signer; + use movekit::access_control_core::{Self, Admin}; + + // Test role types + struct Treasurer has copy, drop {} + + struct Manager has copy, drop {} + + struct Operator has copy, drop {} + + // Core module error codes + const E_NOT_ADMIN: u64 = 0; + const E_ALREADY_HAS_ROLE: u64 = 1; + const E_NO_SUCH_ROLE: u64 = 2; + const E_NOT_INITIALIZED: u64 = 3; + const E_ADMIN_ROLE_PROTECTED: u64 = 4; + + // Admin registry error codes (for delegated functions) + const E_ADMIN_NOT_INITIALIZED: u64 = 0; + const E_ADMIN_NOT_ADMIN: u64 = 1; + const E_ADMIN_SELF_TRANSFER_NOT_ALLOWED: u64 = 2; + const E_ADMIN_NO_PENDING_ADMIN: u64 = 3; + const E_ADMIN_NOT_PENDING_ADMIN: u64 = 4; + + // =========================================== + // INITIALIZATION & ADMIN REGISTRY TESTS + // =========================================== + + #[test(deployer = @movekit)] + fun test_init_module_creates_registries(deployer: &signer) { + access_control_core::init_for_testing(deployer); + + let deployer_addr = signer::address_of(deployer); + + // Check admin registry was created (delegated functions work) + assert!(access_control_core::get_current_admin() == deployer_addr); + assert!(access_control_core::is_current_admin(deployer_addr)); + assert!(access_control_core::has_role(deployer_addr)); + assert!(access_control_core::get_role_count(deployer_addr) == 1); + } + + #[test(deployer = @movekit)] + fun test_init_module_idempotent_on_double_call(deployer: &signer) { + access_control_core::init_for_testing(deployer); + // Second initialization should be idempotent (no failure) + access_control_core::init_for_testing(deployer); + + // Should still work correctly + let deployer_addr = signer::address_of(deployer); + assert!(access_control_core::get_current_admin() == deployer_addr, 0); + assert!(access_control_core::has_role(deployer_addr), 1); + } + + #[test] + #[ + expected_failure( + abort_code = E_ADMIN_NOT_INITIALIZED, + location = movekit::access_control_admin_registry + ) + ] + fun test_get_current_admin_fails_when_not_initialized() { + // Should fail - no admin registry exists (error comes from admin registry) + access_control_core::get_current_admin(); + } + + #[test] + #[ + expected_failure( + abort_code = E_ADMIN_NOT_INITIALIZED, + location = movekit::access_control_admin_registry + ) + ] + fun test_is_current_admin_fails_when_not_initialized() { + // Should fail when not initialized (since core delegates to admin registry) + access_control_core::is_current_admin(@0x123); + } + + #[test(deployer = @movekit)] + fun test_admin_registry_functions(deployer: &signer) { + access_control_core::init_for_testing(deployer); + + let deployer_addr = signer::address_of(deployer); + let other_addr = @0x123; + + // Test delegated admin functions + assert!(access_control_core::get_current_admin() == deployer_addr, 0); + assert!(access_control_core::is_current_admin(deployer_addr), 1); + assert!(!access_control_core::is_current_admin(other_addr), 2); + } + + // =========================================== + // TWO-STEP ADMIN TRANSFER TESTS + // =========================================== + + #[test(admin = @movekit, new_admin = @0x123)] + fun test_transfer_admin_complete_flow( + admin: &signer, new_admin: &signer + ) { + access_control_core::init_for_testing(admin); + + let admin_addr = signer::address_of(admin); + let new_admin_addr = signer::address_of(new_admin); + + // Step 1: Propose transfer (delegated to admin registry) + access_control_core::transfer_admin(admin, new_admin_addr); + + // Check pending state (delegated functions) + assert!(access_control_core::has_pending_admin(), 0); + assert!(access_control_core::get_pending_admin() == new_admin_addr, 1); + + // Admin should still be current + assert!(access_control_core::get_current_admin() == admin_addr, 2); + assert!(access_control_core::is_current_admin(admin_addr), 3); + assert!(!access_control_core::is_current_admin(new_admin_addr), 4); + + // Step 2: Accept transfer (core coordinates admin registry + role management) + access_control_core::accept_pending_admin(new_admin); + + // Check transfer completed + assert!(access_control_core::get_current_admin() == new_admin_addr, 5); + assert!(access_control_core::is_current_admin(new_admin_addr), 6); + assert!(!access_control_core::is_current_admin(admin_addr), 7); + + // Check roles transferred in role registry + assert!(access_control_core::has_role(new_admin_addr), 8); + assert!(!access_control_core::has_role(admin_addr), 9); + + // Check pending admin cleaned up (delegated to admin registry) + assert!(!access_control_core::has_pending_admin(), 10); + } + + #[test(admin = @movekit, new_admin = @0x123)] + fun test_transfer_admin_cancel_flow( + admin: &signer, new_admin: &signer + ) { + access_control_core::init_for_testing(admin); + + let admin_addr = signer::address_of(admin); + let new_admin_addr = signer::address_of(new_admin); + + // Propose transfer + access_control_core::transfer_admin(admin, new_admin_addr); + assert!(access_control_core::has_pending_admin(), 0); + + // Cancel transfer (delegated to admin registry) + access_control_core::cancel_admin_transfer(admin); + + // Check admin unchanged + assert!(access_control_core::get_current_admin() == admin_addr, 1); + assert!(access_control_core::is_current_admin(admin_addr), 2); + assert!(access_control_core::has_role(admin_addr), 3); + + // Check pending admin cleaned up + assert!(!access_control_core::has_pending_admin(), 4); + } + + #[test(admin = @movekit, new_admin = @0x123)] + fun test_transfer_admin_multiple_proposals( + admin: &signer, new_admin: &signer + ) { + access_control_core::init_for_testing(admin); + + let new_admin_addr = signer::address_of(new_admin); + let another_addr = @0x456; + + // First proposal + access_control_core::transfer_admin(admin, new_admin_addr); + assert!(access_control_core::get_pending_admin() == new_admin_addr, 0); + + // Second proposal overwrites first + access_control_core::transfer_admin(admin, another_addr); + assert!(access_control_core::get_pending_admin() == another_addr, 1); + } + + #[test(non_admin = @0x123)] + #[ + expected_failure( + abort_code = E_ADMIN_NOT_INITIALIZED, + location = movekit::access_control_admin_registry + ) + ] + fun test_transfer_admin_not_admin(non_admin: &signer) { + // Should fail - not admin (error from admin registry) + access_control_core::transfer_admin(non_admin, @0x456); + } + + #[test(admin = @movekit)] + #[ + expected_failure( + abort_code = E_ADMIN_SELF_TRANSFER_NOT_ALLOWED, + location = movekit::access_control_admin_registry + ) + ] + fun test_transfer_admin_to_self(admin: &signer) { + access_control_core::init_for_testing(admin); + let admin_addr = signer::address_of(admin); + + // Should fail - cannot transfer to self (error from admin registry) + access_control_core::transfer_admin(admin, admin_addr); + } + + #[test(admin = @movekit, new_admin = @0x123, wrong_admin = @0x456)] + #[expected_failure(abort_code = E_NOT_ADMIN, location = movekit::access_control_core)] + fun test_accept_pending_admin_wrong_address( + admin: &signer, new_admin: &signer, wrong_admin: &signer + ) { + access_control_core::init_for_testing(admin); + let new_admin_addr = signer::address_of(new_admin); + + // Propose transfer to new_admin + access_control_core::transfer_admin(admin, new_admin_addr); + + // Should fail - wrong_admin tries to accept (core validates this) + access_control_core::accept_pending_admin(wrong_admin); + } + + #[test(new_admin = @0x123)] + #[ + expected_failure( + abort_code = E_ADMIN_NOT_INITIALIZED, + location = movekit::access_control_admin_registry + ) + ] + fun test_accept_pending_admin_no_pending(new_admin: &signer) { + // Should fail - admin registry not initialized (error from admin registry) + access_control_core::accept_pending_admin(new_admin); + } + + #[test(admin = @movekit, new_admin = @0x123)] + #[ + expected_failure( + abort_code = E_NOT_INITIALIZED, location = movekit::access_control_core + ) + ] + fun test_accept_pending_admin_no_pending_initialized( + admin: &signer, new_admin: &signer + ) { + access_control_core::init_for_testing(admin); + + // Should fail - no pending admin transfer (core validates this) + access_control_core::accept_pending_admin(new_admin); + } + + #[test(non_admin = @0x123)] + #[ + expected_failure( + abort_code = E_ADMIN_NOT_INITIALIZED, + location = movekit::access_control_admin_registry + ) + ] + fun test_cancel_admin_transfer_not_admin(non_admin: &signer) { + // Should fail - not admin (error from admin registry) + access_control_core::cancel_admin_transfer(non_admin); + } + + #[test(admin = @movekit)] + #[ + expected_failure( + abort_code = E_ADMIN_NO_PENDING_ADMIN, + location = movekit::access_control_admin_registry + ) + ] + fun test_cancel_admin_transfer_no_pending(admin: &signer) { + access_control_core::init_for_testing(admin); + + // Should fail - no pending transfer to cancel (error from admin registry) + access_control_core::cancel_admin_transfer(admin); + } + + #[test] + #[ + expected_failure( + abort_code = E_ADMIN_NOT_INITIALIZED, + location = movekit::access_control_admin_registry + ) + ] + fun test_get_pending_admin_no_pending() { + // Should fail - no pending admin (error from admin registry) + access_control_core::get_pending_admin(); + } + + // =========================================== + // SECURITY ATTACK PREVENTION TESTS + // =========================================== + + #[test(attacker = @0x999)] + #[ + expected_failure( + abort_code = E_ADMIN_ROLE_PROTECTED, location = movekit::access_control_core + ) + ] + fun test_privilege_escalation_prevention_grant(attacker: &signer) { + // Attacker tries to grant themselves admin role - blocked by role protection + access_control_core::grant_role(attacker, signer::address_of(attacker)); + } + + #[test(attacker = @0x999)] + #[ + expected_failure( + abort_code = E_ADMIN_NOT_INITIALIZED, + location = movekit::access_control_admin_registry + ) + ] + fun test_privilege_escalation_prevention_transfer(attacker: &signer) { + // Attacker tries to transfer admin to themselves (error from admin registry) + access_control_core::transfer_admin(attacker, signer::address_of(attacker)); + } + + #[test(admin = @movekit, attacker = @0x999)] + #[ + expected_failure( + abort_code = E_ADMIN_NOT_ADMIN, + location = movekit::access_control_admin_registry + ) + ] + fun test_unauthorized_role_grant_prevention( + admin: &signer, attacker: &signer + ) { + access_control_core::init_for_testing(admin); + + // Attacker tries to grant roles to others + access_control_core::grant_role(attacker, @0x123); + } + + #[test(admin = @movekit, attacker = @0x999)] + #[ + expected_failure( + abort_code = E_ADMIN_NOT_ADMIN, + location = movekit::access_control_admin_registry + ) + ] + fun test_unauthorized_role_revoke_prevention( + admin: &signer, attacker: &signer + ) { + access_control_core::init_for_testing(admin); + + // Grant role to someone + access_control_core::grant_role(admin, @0x123); + + // Attacker tries to revoke roles from others + access_control_core::revoke_role(attacker, @0x123); + } + + #[test(admin = @movekit, attacker = @0x999)] + #[ + expected_failure( + abort_code = E_ADMIN_NOT_ADMIN, + location = movekit::access_control_admin_registry + ) + ] + fun test_unauthorized_admin_cancel_prevention( + admin: &signer, attacker: &signer + ) { + access_control_core::init_for_testing(admin); + + // Admin proposes transfer + access_control_core::transfer_admin(admin, @0x123); + + // Attacker tries to cancel admin transfer (error from admin registry) + access_control_core::cancel_admin_transfer(attacker); + } + + // =========================================== + // STATE CONSISTENCY TESTS + // =========================================== + + #[test(admin = @movekit)] + fun test_role_registry_admin_sync(admin: &signer) { + access_control_core::init_for_testing(admin); + + let admin_addr = signer::address_of(admin); + + // Verify AdminRegistry and RoleRegistry are in sync + assert!(access_control_core::get_current_admin() == admin_addr, 0); + assert!(access_control_core::has_role(admin_addr), 1); + assert!(access_control_core::is_current_admin(admin_addr), 2); + + // Test consistency after granting other roles + access_control_core::grant_role(admin, admin_addr); + + // Admin should still be admin + assert!(access_control_core::get_current_admin() == admin_addr, 3); + assert!(access_control_core::has_role(admin_addr), 4); + assert!(access_control_core::has_role(admin_addr), 5); + assert!(access_control_core::get_role_count(admin_addr) == 2, 6); + } + + #[test(admin = @movekit, new_admin = @0x123)] + fun test_admin_transfer_maintains_role_consistency( + admin: &signer, new_admin: &signer + ) { + access_control_core::init_for_testing(admin); + + let admin_addr = signer::address_of(admin); + let new_admin_addr = signer::address_of(new_admin); + + // Grant admin some additional roles + access_control_core::grant_role(admin, admin_addr); + access_control_core::grant_role(admin, admin_addr); + + // Verify initial state + assert!(access_control_core::get_role_count(admin_addr) == 3, 0); // Admin + Treasurer + Manager + assert!(access_control_core::get_role_count(new_admin_addr) == 0, 1); + + // Transfer admin role (core coordinates between admin registry and role registry) + access_control_core::transfer_admin(admin, new_admin_addr); + access_control_core::accept_pending_admin(new_admin); + + // Verify role consistency after transfer + assert!(access_control_core::get_role_count(admin_addr) == 2, 2); // Treasurer + Manager (lost Admin) + assert!(access_control_core::get_role_count(new_admin_addr) == 1, 3); // Admin only + + assert!(!access_control_core::has_role(admin_addr), 4); + assert!(access_control_core::has_role(new_admin_addr), 5); + assert!(access_control_core::has_role(admin_addr), 6); // Should keep other roles + assert!(access_control_core::has_role(admin_addr), 7); + } + + #[test(admin = @movekit, new_admin = @0x123)] + fun test_admin_role_consistency_during_transfer( + admin: &signer, new_admin: &signer + ) { + // Test that admin role count remains exactly 1 throughout transfer + access_control_core::init_for_testing(admin); + + let admin_addr = signer::address_of(admin); + let new_admin_addr = signer::address_of(new_admin); + + // Initially: 1 admin role (in old admin) + assert!(access_control_core::has_role(admin_addr), 0); + assert!(!access_control_core::has_role(new_admin_addr), 1); + + // Propose transfer - still 1 admin role + access_control_core::transfer_admin(admin, new_admin_addr); + assert!(access_control_core::has_role(admin_addr), 2); + assert!(!access_control_core::has_role(new_admin_addr), 3); + + // Accept transfer - still 1 admin role (but transferred) + access_control_core::accept_pending_admin(new_admin); + assert!(!access_control_core::has_role(admin_addr), 4); + assert!(access_control_core::has_role(new_admin_addr), 5); + + // AdminRegistry and RoleRegistry should be in sync + assert!(access_control_core::get_current_admin() == new_admin_addr, 6); + assert!(access_control_core::is_current_admin(new_admin_addr), 7); + } + + // =========================================== + // ROLE MANAGEMENT TESTS + // =========================================== + + #[test(admin = @movekit)] + fun test_role_operations_on_nonexistent_users(admin: &signer) { + access_control_core::init_for_testing(admin); + + let nonexistent = @0xDEADBEEF; + + // Should be able to grant role to new address + access_control_core::grant_role(admin, nonexistent); + assert!(access_control_core::has_role(nonexistent), 0); + assert!(access_control_core::get_role_count(nonexistent) == 1, 1); + + // Should be able to grant multiple roles + access_control_core::grant_role(admin, nonexistent); + assert!(access_control_core::has_role(nonexistent), 2); + assert!(access_control_core::get_role_count(nonexistent) == 2, 3); + } + + #[test(admin = @movekit, user = @0x123)] + fun test_grant_role_success(admin: &signer, user: &signer) { + access_control_core::init_for_testing(admin); + + let user_addr = signer::address_of(user); + + // Test granting a role + assert!(!access_control_core::has_role(user_addr), 0); + assert!(access_control_core::get_role_count(user_addr) == 0, 1); + + access_control_core::grant_role(admin, user_addr); + + assert!(access_control_core::has_role(user_addr), 2); + assert!(access_control_core::get_role_count(user_addr) == 1, 3); + } + + #[test(admin = @movekit, user = @0x123)] + #[ + expected_failure( + abort_code = E_ALREADY_HAS_ROLE, location = movekit::access_control_core + ) + ] + fun test_grant_role_already_has_role(admin: &signer, user: &signer) { + access_control_core::init_for_testing(admin); + let user_addr = signer::address_of(user); + access_control_core::grant_role(admin, user_addr); + + // This should fail - user already has Treasurer role + access_control_core::grant_role(admin, user_addr); + } + + #[test(non_admin = @0x123)] + #[ + expected_failure( + abort_code = E_ADMIN_NOT_INITIALIZED, + location = movekit::access_control_admin_registry + ) + ] + fun test_grant_role_not_admin(non_admin: &signer) { + // Test that non-admin cannot grant roles + access_control_core::grant_role(non_admin, @0x456); + } + + #[test(admin = @movekit, user = @0x123)] + fun test_revoke_role_success(admin: &signer, user: &signer) { + access_control_core::init_for_testing(admin); + let user_addr = signer::address_of(user); + access_control_core::grant_role(admin, user_addr); + + // Test revoking a role + assert!(access_control_core::has_role(user_addr), 0); + assert!(access_control_core::get_role_count(user_addr) == 1, 1); + + access_control_core::revoke_role(admin, user_addr); + + assert!(!access_control_core::has_role(user_addr), 2); + assert!(access_control_core::get_role_count(user_addr) == 0, 3); + } + + #[test(admin = @movekit, user = @0x123)] + #[expected_failure( + abort_code = E_NO_SUCH_ROLE, location = movekit::access_control_core + )] + fun test_revoke_role_no_such_role(admin: &signer, user: &signer) { + access_control_core::init_for_testing(admin); + let user_addr = signer::address_of(user); + + // This should fail - user doesn't have Treasurer role + access_control_core::revoke_role(admin, user_addr); + } + + #[test(non_admin = @0x123)] + #[ + expected_failure( + abort_code = E_ADMIN_NOT_INITIALIZED, + location = movekit::access_control_admin_registry + ) + ] + fun test_revoke_role_not_admin(non_admin: &signer) { + // Test that non-admin cannot revoke roles + access_control_core::revoke_role(non_admin, @0x456); + } + + #[test(admin = @movekit, user1 = @0x123, user2 = @0x456)] + fun test_multiple_roles_different_users( + admin: &signer, user1: &signer, user2: &signer + ) { + access_control_core::init_for_testing(admin); + + let user1_addr = signer::address_of(user1); + let user2_addr = signer::address_of(user2); + + // Grant different roles to different users + access_control_core::grant_role(admin, user1_addr); + access_control_core::grant_role(admin, user2_addr); + + // Verify roles + assert!(access_control_core::has_role(user1_addr), 0); + assert!(!access_control_core::has_role(user1_addr), 1); + assert!(access_control_core::get_role_count(user1_addr) == 1, 2); + + assert!(access_control_core::has_role(user2_addr), 3); + assert!(!access_control_core::has_role(user2_addr), 4); + assert!(access_control_core::get_role_count(user2_addr) == 1, 5); + } + + #[test(admin = @movekit, user = @0x123)] + fun test_multiple_roles_same_user(admin: &signer, user: &signer) { + access_control_core::init_for_testing(admin); + + let user_addr = signer::address_of(user); + + // Grant multiple roles to same user + access_control_core::grant_role(admin, user_addr); + access_control_core::grant_role(admin, user_addr); + access_control_core::grant_role(admin, user_addr); + + // Verify all roles + assert!(access_control_core::has_role(user_addr), 0); + assert!(access_control_core::has_role(user_addr), 1); + assert!(access_control_core::has_role(user_addr), 2); + assert!(access_control_core::get_role_count(user_addr) == 3, 3); + + // Revoke one role, others should remain + access_control_core::revoke_role(admin, user_addr); + + assert!(access_control_core::has_role(user_addr), 4); + assert!(!access_control_core::has_role(user_addr), 5); + assert!(access_control_core::has_role(user_addr), 6); + assert!(access_control_core::get_role_count(user_addr) == 2, 7); + } + + #[test(admin = @movekit, user = @0x123)] + fun test_get_roles_function(admin: &signer, user: &signer) { + access_control_core::init_for_testing(admin); + + let user_addr = signer::address_of(user); + + // Initially no roles + let roles = access_control_core::get_roles(user_addr); + assert!(roles.length() == 0, 0); + + // Grant some roles + access_control_core::grant_role(admin, user_addr); + access_control_core::grant_role(admin, user_addr); + + // Check roles vector + let roles = access_control_core::get_roles(user_addr); + assert!(roles.length() == 2, 1); + + // Revoke one role + access_control_core::revoke_role(admin, user_addr); + + let roles = access_control_core::get_roles(user_addr); + assert!(roles.length() == 1, 2); + } + + #[test(user = @0x123)] + fun test_has_role_no_role(user: &signer) { + let user_addr = signer::address_of(user); + assert!(!access_control_core::has_role(user_addr), 0); + assert!(!access_control_core::has_role(user_addr), 1); + assert!(access_control_core::get_role_count(user_addr) == 0, 2); + } + + #[test] + fun test_get_roles_no_registry() { + let roles = access_control_core::get_roles(@0x123); + assert!(roles.length() == 0, 0); + assert!(access_control_core::get_role_count(@0x123) == 0, 1); + } + + // =========================================== + // ADMIN TRANSFER CHAIN TESTS + // =========================================== + + #[test(admin = @movekit, admin2 = @0x123, admin3 = @0x456)] + fun test_admin_transfer_chain( + admin: &signer, admin2: &signer, admin3: &signer + ) { + access_control_core::init_for_testing(admin); + + let admin_addr = signer::address_of(admin); + let admin2_addr = signer::address_of(admin2); + let admin3_addr = signer::address_of(admin3); + + // Transfer 1: admin -> admin2 + access_control_core::transfer_admin(admin, admin2_addr); + access_control_core::accept_pending_admin(admin2); + + assert!(access_control_core::get_current_admin() == admin2_addr, 0); + assert!(access_control_core::has_role(admin2_addr), 1); + assert!(!access_control_core::has_role(admin_addr), 2); + + // Transfer 2: admin2 -> admin3 + access_control_core::transfer_admin(admin2, admin3_addr); + access_control_core::accept_pending_admin(admin3); + + assert!(access_control_core::get_current_admin() == admin3_addr, 3); + assert!(access_control_core::has_role(admin3_addr), 4); + assert!(!access_control_core::has_role(admin2_addr), 5); + } + + #[test(admin = @movekit, new_admin = @0x123)] + fun test_new_admin_can_manage_roles( + admin: &signer, new_admin: &signer + ) { + access_control_core::init_for_testing(admin); + + let admin_addr = signer::address_of(admin); + let new_admin_addr = signer::address_of(new_admin); + + // Transfer admin + access_control_core::transfer_admin(admin, new_admin_addr); + access_control_core::accept_pending_admin(new_admin); + + // New admin should be able to grant roles + access_control_core::grant_role(new_admin, admin_addr); + assert!(access_control_core::has_role(admin_addr), 0); + + // And revoke roles + access_control_core::revoke_role(new_admin, admin_addr); + assert!(!access_control_core::has_role(admin_addr), 1); + } + + // =========================================== + // UTILITY FUNCTION TESTS + // =========================================== + + #[test(admin = @movekit, user = @0x123)] + fun test_require_role_success(admin: &signer, user: &signer) { + access_control_core::init_for_testing(admin); + let user_addr = signer::address_of(user); + access_control_core::grant_role(admin, user_addr); + + // Should not abort + access_control_core::require_role(user); + } + + #[test(user = @0x123)] + #[expected_failure( + abort_code = E_NO_SUCH_ROLE, location = movekit::access_control_core + )] + fun test_require_role_fails_without_role(user: &signer) { + // Should abort - user doesn't have role + access_control_core::require_role(user); + } + + #[test(admin = @movekit, user = @0x123)] + fun test_require_role_admin(admin: &signer, user: &signer) { + access_control_core::init_for_testing(admin); + + // Admin should have Admin role + access_control_core::require_role(admin); + + // User should not have Admin role + let user_addr = signer::address_of(user); + assert!(!access_control_core::has_role(user_addr), 0); + } + + // =========================================== + // EDGE CASES AND ERROR HANDLING + // =========================================== + + #[test] + fun test_has_role_with_no_registry() { + // Test has_role when RoleRegistry doesn't exist + assert!(!access_control_core::has_role(@0x123), 0); + assert!(!access_control_core::has_role(@0x456), 1); + } + + #[test(admin = @movekit)] + fun test_grant_revoke_same_role_multiple_times(admin: &signer) { + access_control_core::init_for_testing(admin); + let target = @0x123; + + // Grant, revoke, grant again + access_control_core::grant_role(admin, target); + assert!(access_control_core::has_role(target), 0); + + access_control_core::revoke_role(admin, target); + assert!(!access_control_core::has_role(target), 1); + + access_control_core::grant_role(admin, target); + assert!(access_control_core::has_role(target), 2); + } + + #[test(admin = @movekit)] + fun test_large_number_of_roles(admin: &signer) { + access_control_core::init_for_testing(admin); + let user = @0x123; + + // Grant multiple different role types to same user + access_control_core::grant_role(admin, user); + access_control_core::grant_role(admin, user); + access_control_core::grant_role(admin, user); + // Note: Admin role is reserved for actual admins + + // Verify all roles exist + assert!(access_control_core::has_role(user), 0); + assert!(access_control_core::has_role(user), 1); + assert!(access_control_core::has_role(user), 2); + assert!(access_control_core::get_role_count(user) == 3, 3); + } + + #[test(admin = @movekit, new_admin = @0x123)] + #[ + expected_failure( + abort_code = E_NOT_INITIALIZED, location = movekit::access_control_core + ) + ] + fun test_double_accept_pending_admin_fails( + admin: &signer, new_admin: &signer + ) { + access_control_core::init_for_testing(admin); + let new_admin_addr = signer::address_of(new_admin); + access_control_core::transfer_admin(admin, new_admin_addr); + + // First accept succeeds + access_control_core::accept_pending_admin(new_admin); + // Second accept must fail (core validates pending transfer exists) + access_control_core::accept_pending_admin(new_admin); + } + + #[test(admin = @movekit)] + #[ + expected_failure( + abort_code = E_ADMIN_ROLE_PROTECTED, location = movekit::access_control_core + ) + ] + fun test_admin_cannot_grant_admin_twice(admin: &signer) { + access_control_core::init_for_testing(admin); + let admin_addr = signer::address_of(admin); + + // Admin role is protected - cannot be granted manually + access_control_core::grant_role(admin, admin_addr); + } + + #[test(current_admin = @movekit, future_admin = @0x123)] + #[ + expected_failure( + abort_code = E_ADMIN_ROLE_PROTECTED, location = movekit::access_control_core + ) + ] + fun test_admin_role_coordinated_during_transfer( + current_admin: &signer, future_admin: &signer + ) { + access_control_core::init_for_testing(current_admin); + + let fut_addr = signer::address_of(future_admin); + + // Cannot pre-grant Admin role - it's protected + access_control_core::grant_role(current_admin, fut_addr); + } +}