create([ 'username' => $username, 'name' => 'Tester', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); $role = AdminRole::query()->create([ 'slug' => 'role_'.$username, 'name' => 'Role '.$username, ]); $role->syncLegacyPermissionSlugs($permissionSlugs); $siteId = AdminUser::defaultAdminSiteId(); $admin->roles()->sync([ (int) $role->id => [ 'site_id' => $siteId, 'granted_at' => now(), ], ]); return $admin->createToken('test', ['*'], now()->addDay())->plainTextToken; } test('admin user permission apis require rbac permission', function (): void { $token = makeAdminWithPermissions('rbac_viewer', ['prd.report.view']); $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/admin-users') ->assertForbidden() ->assertJsonPath('code', ErrorCode::AdminForbidden->value); }); test('admin can list platform users and read effective permissions', function (): void { $token = makeAdminWithPermissions('rbac_manager', ['prd.admin_user.manage']); $target = AdminUser::query()->create([ 'username' => 'target_user', 'name' => 'Target User', 'email' => 'target@example.com', 'password' => Hash::make('secret-strong'), 'status' => 0, ]); $targetRole = AdminRole::query()->create(['slug' => 'target_role', 'name' => 'Target Role']); $drawCodes = AdminPermissionBridge::menuActionCodesForLegacy('prd.draw_result.view'); $drawIds = DB::table('admin_menu_actions') ->whereIn('permission_code', $drawCodes) ->where('status', 1) ->pluck('id') ->all(); foreach ($drawIds as $mid) { DB::table('admin_role_menu_actions')->insert([ 'role_id' => $targetRole->id, 'menu_action_id' => (int) $mid, ]); } $siteId = AdminUser::defaultAdminSiteId(); $target->roles()->sync([ (int) $targetRole->id => [ 'site_id' => $siteId, 'granted_at' => now(), ], ]); $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/admin-user-permission-catalog') ->assertOk() ->assertJsonPath('code', ErrorCode::Success->value) ->assertJsonFragment(['slug' => 'prd.admin_user.manage']); $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/admin-users?keyword=target') ->assertOk() ->assertJsonPath('code', ErrorCode::Success->value) ->assertJsonPath('data.items.0.username', 'target_user') ->assertJsonPath('data.items.0.roles.0', 'target_role'); $list = $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/admin-users?keyword=target') ->assertOk() ->json('data.items.0.effective_permissions'); expect($list)->toContain('prd.draw_result.view'); }); test('admin can sync user roles for default site', function (): void { $token = makeAdminWithPermissions('rbac_role_editor', ['prd.admin_user.manage', 'prd.admin_role.manage']); $r1 = AdminRole::query()->create(['slug' => 'role_sync_a', 'name' => 'Role A']); $r2 = AdminRole::query()->create(['slug' => 'role_sync_b', 'name' => 'Role B']); $target = AdminUser::query()->create([ 'username' => 'role_target', 'name' => 'Role Target', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/admin-users/'.$target->id.'/roles', [ 'admin_site_id' => AdminUser::defaultAdminSiteId(), 'role_slugs' => ['role_sync_b', 'role_sync_a'], ]) ->assertOk() ->assertJsonPath('code', ErrorCode::Success->value); $slugs = $target->fresh()->adminRoleSlugs(); sort($slugs); expect($slugs)->toBe(['role_sync_a', 'role_sync_b']); }); test('platform admin users list excludes agent accounts', function (): void { $token = makeAdminWithPermissions('platform_list_manager', ['prd.admin_user.manage']); $platformUser = AdminUser::query()->create([ 'username' => 'platform_only_user', 'name' => 'Platform Only', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); $siteId = AdminUser::defaultAdminSiteId(); $root = AgentNode::query()->where('admin_site_id', $siteId)->where('depth', 0)->firstOrFail(); $super = AdminUser::query()->create([ 'username' => 'agent_list_super', 'name' => 'Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $branch = app(AgentNodeService::class)->createChild($super, [ 'parent_id' => (int) $root->id, 'code' => 'list-branch', 'name' => 'List Branch', ]); app(AgentAdminUserService::class)->createUnderAgent($branch, [ 'username' => 'agent_hidden_user', 'nickname' => 'Agent Hidden', 'password' => 'secret-strong-2', 'role_ids' => [], ]); $items = $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/admin-users') ->assertOk() ->json('data.items'); expect(collect($items)->pluck('username')->all()) ->toContain('platform_only_user') ->not->toContain('agent_hidden_user'); }); test('platform account apis reject agent accounts and agent roles', function (): void { $token = makeAdminWithPermissions('platform_scope_manager', ['prd.admin_user.manage', 'prd.admin_role.manage']); $siteId = AdminUser::defaultAdminSiteId(); $root = AgentNode::query()->where('admin_site_id', $siteId)->where('depth', 0)->firstOrFail(); $super = AdminUser::query()->create([ 'username' => 'platform_guard_super', 'name' => 'Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $branch = app(AgentNodeService::class)->createChild($super, [ 'parent_id' => (int) $root->id, 'code' => 'platform-guard-branch', 'name' => 'Platform Guard Branch', ]); $agentRole = AdminRole::query()->create([ 'slug' => 'guard_agent_role', 'name' => 'Guard Agent Role', 'scope_type' => AdminRole::SCOPE_AGENT, 'owner_agent_id' => $branch->id, ]); $agentRole->syncLegacyPermissionSlugs(['prd.agent.role.view']); $agentUser = app(AgentAdminUserService::class)->createUnderAgent($branch, [ 'username' => 'guard_agent_user', 'nickname' => 'Guard Agent User', 'password' => 'secret-strong-3', 'role_ids' => [(int) $agentRole->id], ]); $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/admin-users/'.$agentUser->id.'/roles', [ 'role_slugs' => ['guard_agent_role'], ]) ->assertStatus(422); $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/admin-roles/'.$agentRole->id.'/permissions', [ 'permission_slugs' => ['prd.admin_role.manage'], ]) ->assertStatus(422); }); test('permission catalog groups permissions by admin navigation order', function (): void { $token = makeAdminWithPermissions('nav_group_catalog', ['prd.admin_user.manage', 'prd.admin_role.manage']); $groups = $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/admin-user-permission-catalog') ->assertOk() ->json('data.permission_menu_groups'); expect(array_column($groups, 'key'))->toBe([ 'dashboard', 'agents', 'draws', 'tickets', 'players', 'settlement', 'wallet', 'reconcile', 'reports', 'rules_plays', 'rules_odds', 'jackpot', 'risk_cap', 'currencies', 'admin_users', 'admin_roles', 'audit', 'settings', 'risk', ]); expect($groups[1]['key'])->toBe('agents'); expect($groups[2]['key'])->toBe('draws'); $groupsByKey = collect($groups)->keyBy('key'); expect($groupsByKey['admin_users']['label'])->toBe('管理列表'); expect(array_column($groupsByKey['admin_users']['permissions'], 'slug'))->toBe(['prd.admin_user.manage']); expect($groupsByKey['admin_roles']['label'])->toBe('角色管理'); expect(array_column($groupsByKey['admin_roles']['permissions'], 'slug'))->toBe(['prd.admin_role.manage']); expect(array_column($groupsByKey['tickets']['permissions'], 'slug'))->toBe([ 'prd.tickets.view', ]); expect(array_column($groupsByKey['reports']['permissions'], 'slug'))->toContain( 'prd.report.view', ); expect(array_column($groupsByKey['jackpot']['permissions'], 'slug'))->toContain( 'prd.jackpot.manage', 'prd.jackpot.view', ); expect(array_column($groupsByKey['reconcile']['permissions'], 'slug'))->toBe([ 'prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', ]); }); test('admin can adjust platform agent role permissions from the catalog', function (): void { $token = makeAdminWithPermissions('role_permission_repairer', ['prd.admin_user.manage', 'prd.admin_role.manage']); $catalog = $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/admin-user-permission-catalog') ->assertOk() ->json('data'); $catalogSlugs = collect($catalog['permission_menu_groups']) ->flatMap(static fn (array $group): array => array_column($group['permissions'], 'slug')) ->unique() ->values() ->all(); expect($catalogSlugs) ->toContain('prd.admin_user.manage') ->toContain('prd.admin_role.manage') ->toContain('prd.report.view') ->toContain('prd.wallet_reconcile.manage'); $agentRole = collect($this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/admin-roles') ->assertOk() ->json('data.items')) ->firstWhere('slug', 'agent'); expect($agentRole)->not->toBeNull(); $repairResponse = $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/admin-roles/'.$agentRole['id'].'/permissions', [ 'permission_slugs' => ['prd.report.view', 'prd.wallet_reconcile.manage'], ]) ->assertOk() ->assertJsonPath('data.slug', 'agent'); expect($repairResponse->json('data.permission_slugs')) ->toContain('prd.report.view', 'prd.wallet_reconcile.manage'); $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/admin-roles/'.$agentRole['id'].'/permissions', [ 'permission_slugs' => ['prd.admin_role.manage'], ]) ->assertOk() ->assertJsonPath('data.permission_slugs', ['prd.admin_role.manage']); $persistedRole = collect($this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/admin-roles') ->assertOk() ->json('data.items')) ->firstWhere('slug', 'agent'); expect($persistedRole['permission_slugs'])->toBe(['prd.admin_role.manage']); }); test('admin can create update and delete users with crud rules', function (): void { $token = makeAdminWithPermissions('crud_actor', ['prd.admin_user.manage']); $crudRole = AdminRole::query()->create(['slug' => 'crud_new_user_role', 'name' => 'Crud Role']); $this->withHeader('Authorization', 'Bearer '.$token) ->postJson('/api/v1/admin/admin-users', [ 'username' => 'NewUser_XX', 'nickname' => '新用户', 'email' => 'newuser@example.com', 'password' => 'secret-long', 'status' => 0, 'admin_site_id' => AdminUser::defaultAdminSiteId(), 'role_slugs' => ['crud_new_user_role'], ]) ->assertOk() ->assertJsonPath('code', ErrorCode::Success->value) ->assertJsonPath('data.username', 'newuser_xx') ->assertJsonPath('data.roles.0', 'crud_new_user_role'); $created = AdminUser::query()->where('username', 'newuser_xx')->firstOrFail(); expect($created->adminRoleSlugs())->toContain('crud_new_user_role'); $this->withHeader('Authorization', 'Bearer '.$token) ->postJson('/api/v1/admin/admin-users', [ 'username' => 'newuser_xx', 'nickname' => 'dup', 'email' => null, 'password' => 'secret-long', 'admin_site_id' => AdminUser::defaultAdminSiteId(), 'role_slugs' => [$crudRole->slug], ]) ->assertStatus(422) ->assertJsonPath('code', ErrorCode::ValidationFailed->value); $target = AdminUser::query()->where('username', 'newuser_xx')->firstOrFail(); $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/admin-users/'.$target->id, [ 'nickname' => '已改名', 'email' => null, 'password' => 'new-secret-9', ]) ->assertOk() ->assertJsonPath('data.nickname', '已改名'); expect(Hash::check('new-secret-9', $target->fresh()->password))->toBeTrue(); $victim = AdminUser::query()->create([ 'username' => 'to_delete', 'name' => 'Delete Me', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); $this->withHeader('Authorization', 'Bearer '.$token) ->deleteJson('/api/v1/admin/admin-users/'.$victim->id) ->assertOk() ->assertJsonPath('data.deleted', true); expect(AdminUser::query()->whereKey($victim->id)->exists())->toBeFalse(); }); test('admin user create requires at least one role slug', function (): void { $token = makeAdminWithPermissions('create_need_roles', ['prd.admin_user.manage']); AdminRole::query()->create(['slug' => 'role_for_create_gate', 'name' => 'Gate Role']); $this->withHeader('Authorization', 'Bearer '.$token) ->postJson('/api/v1/admin/admin-users', [ 'username' => 'no_roles_user', 'nickname' => 'NR', 'email' => null, 'password' => 'secret-long', 'admin_site_id' => AdminUser::defaultAdminSiteId(), 'role_slugs' => [], ]) ->assertStatus(422) ->assertJsonPath('code', ErrorCode::ValidationFailed->value); }); test('admin cannot delete self', function (): void { $token = makeAdminWithPermissions('self_guard', ['prd.admin_user.manage']); $me = AdminUser::query()->where('username', 'self_guard')->firstOrFail(); $this->withHeader('Authorization', 'Bearer '.$token) ->withHeader('X-Locale', 'zh') ->deleteJson('/api/v1/admin/admin-users/'.$me->id) ->assertStatus(422) ->assertJsonPath('code', ErrorCode::ValidationFailed->value) ->assertJsonPath('msg', '不能删除当前登录账号。'); }); test('admin cannot delete the last super admin', function (): void { $token = makeAdminWithPermissions('super_deleter', ['prd.admin_user.manage']); $s1 = AdminUser::query()->create([ 'username' => 'super_one', 'name' => 'S1', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($s1); $this->withHeader('Authorization', 'Bearer '.$token) ->withHeader('X-Locale', 'zh') ->deleteJson('/api/v1/admin/admin-users/'.$s1->id) ->assertStatus(422) ->assertJsonPath('msg', '不能删除最后一个超级管理员。'); $s2 = AdminUser::query()->create([ 'username' => 'super_two', 'name' => 'S2', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($s2); $this->withHeader('Authorization', 'Bearer '.$token) ->withHeader('X-Locale', 'zh') ->deleteJson('/api/v1/admin/admin-users/'.$s1->id) ->assertOk(); $this->withHeader('Authorization', 'Bearer '.$token) ->withHeader('X-Locale', 'zh') ->deleteJson('/api/v1/admin/admin-users/'.$s2->id) ->assertStatus(422) ->assertJsonPath('msg', '不能删除最后一个超级管理员。'); });