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 users and sync direct 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'); $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/admin-users/'.$target->id.'/permissions', [ 'permission_slugs' => ['prd.report.view'], ]) ->assertOk() ->assertJsonPath('code', ErrorCode::Success->value) ->assertJsonFragment(['prd.report.view']); expect($target->fresh()->directLegacyPermissionSlugs())->toContain('prd.report.view'); $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'); expect($list)->toContain('prd.report.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', [ '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('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', 'draws', 'tickets', 'players', 'rules_plays', 'rules_odds', 'jackpot', 'risk_cap', 'wallet', 'settlement', 'reconcile', 'reports', 'currencies', 'integration', 'admin_users', 'admin_roles', 'risk', 'audit', 'settings', ]); expect($groups[1]['key'])->toBe('draws'); expect($groups[14]['label'])->toBe('管理列表'); expect(array_column($groups[14]['permissions'], 'slug'))->toBe(['prd.admin_user.manage']); expect($groups[15]['label'])->toBe('角色管理'); expect(array_column($groups[15]['permissions'], 'slug'))->toBe(['prd.admin_role.manage']); $groupsByKey = collect($groups)->keyBy('key'); 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 repair role permissions from the full catalog after role creation', 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'); $role = $this->withHeader('Authorization', 'Bearer '.$token) ->postJson('/api/v1/admin/admin-roles', [ 'slug' => 'repairable_role', 'name' => 'Repairable Role', 'permission_slugs' => [], ]) ->assertOk() ->assertJsonPath('data.permission_slugs', []) ->json('data'); $repairResponse = $this->withHeader('Authorization', 'Bearer '.$token) ->putJson('/api/v1/admin/admin-roles/'.$role['id'].'/permissions', [ 'permission_slugs' => ['prd.report.view', 'prd.wallet_reconcile.manage'], ]) ->assertOk() ->assertJsonPath('data.slug', 'repairable_role'); 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/'.$role['id'].'/permissions', [ 'permission_slugs' => ['prd.admin_role.manage'], ]) ->assertOk() ->assertJsonPath('data.permission_slugs', ['prd.admin_role.manage']); $persistedPermissions = $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/admin-roles') ->assertOk() ->json('data.items'); $persistedRole = collect($persistedPermissions)->firstWhere('slug', 'repairable_role'); 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, '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', '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', '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) ->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) ->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) ->deleteJson('/api/v1/admin/admin-users/'.$s1->id) ->assertOk(); $this->withHeader('Authorization', 'Bearer '.$token) ->deleteJson('/api/v1/admin/admin-users/'.$s2->id) ->assertStatus(422) ->assertJsonPath('msg', '不能删除最后一个超级管理员'); });