From 5c28488bc596590ecb2c53a379f1f0075698cf20 Mon Sep 17 00:00:00 2001 From: Nick Zeng Date: Mon, 9 Mar 2026 14:15:44 +0800 Subject: [PATCH] update test --- .../Middleware/PermissionMiddlewareTest.php | 263 +++++++++++++ .../Unit/Model/RoutePermissionModelTest.php | 294 +++++++++++++++ .../Unit/Service/ScopeBitmapServiceTest.php | 350 ++++++++++++++++++ .../Unit/Service/ScopeTableManagerTest.php | 192 ++++++++++ 4 files changed, 1099 insertions(+) create mode 100644 backend/test/Cases/Unit/Middleware/PermissionMiddlewareTest.php create mode 100644 backend/test/Cases/Unit/Model/RoutePermissionModelTest.php create mode 100644 backend/test/Cases/Unit/Service/ScopeBitmapServiceTest.php create mode 100644 backend/test/Cases/Unit/Service/ScopeTableManagerTest.php diff --git a/backend/test/Cases/Unit/Middleware/PermissionMiddlewareTest.php b/backend/test/Cases/Unit/Middleware/PermissionMiddlewareTest.php new file mode 100644 index 0000000..8552310 --- /dev/null +++ b/backend/test/Cases/Unit/Middleware/PermissionMiddlewareTest.php @@ -0,0 +1,263 @@ +guard('jwt')->login($user); + } + + protected function authHeaders(User $user): array + { + return ['Authorization' => 'Bearer ' . $this->getAuthToken($user)]; + } + + protected function createTestUser(string $role_name, array $overrides = []): User + { + $role = Role::query()->where('name', $role_name)->firstOrFail(); + $suffix = bin2hex(random_bytes(4)); + + return User::query()->create(array_merge([ + 'username' => 'perm_test_' . $suffix, + 'email' => 'perm_test_' . $suffix . '@example.com', + 'password' => 'Pass_' . $suffix, + 'status' => 1, + 'role_id' => $role->id, + ], $overrides)); + } + + // ========== Step 1: 路由访问检查 ========== + + public function test_administrator_bypasses_route_check(): void + { + $user = $this->createTestUser('administrator'); + + $response = $this->get('/api/v1/users', [], $this->authHeaders($user)); + + $response->assertStatus(200); + $response->assertJsonPath('code', 0); + } + + public function test_non_admin_without_route_group_returns_403(): void + { + $user = $this->createTestUser('accessor'); + + // accessor 没有路由组授权,应返回 403 + $response = $this->get('/api/v1/users', [], $this->authHeaders($user)); + + $response->assertStatus(403); + } + + public function test_route_group_authorization_allows_access(): void + { + $user = $this->createTestUser('developer'); + + $route = Route::query() + ->where('method', 'GET') + ->where('path', '/api/v1/users') + ->first(); + + if (!$route) { + $this->markTestSkipped('routes 表中无 GET /api/v1/users 路由'); + } + + // 创建路由组并授权 + $group = RouteGroup::query()->create([ + 'name' => 'test_user_mgmt_' . uniqid(), + 'label' => '用户管理测试', + ]); + + $old_group_id = $route->group_id; + $route->group_id = $group->id; + $route->save(); + + Db::table('role_route_groups')->insert([ + 'role_id' => $user->role_id, + 'group_id' => $group->id, + ]); + + $response = $this->get('/api/v1/users', [], $this->authHeaders($user)); + $response->assertStatus(200); + + // 清理 + Db::table('role_route_groups') + ->where('role_id', $user->role_id) + ->where('group_id', $group->id) + ->delete(); + $route->group_id = $old_group_id; + $route->save(); + $group->delete(); + } + + public function test_override_allows_access_bypassing_group(): void + { + $user = $this->createTestUser('accessor'); + + $route = Route::query() + ->where('method', 'GET') + ->where('path', '/api/v1/users') + ->first(); + + if (!$route) { + $this->markTestSkipped('routes 表中无 GET /api/v1/users 路由'); + } + + // 设置 override 为允许(无需路由组) + RoleRouteOverride::query()->create([ + 'role_id' => $user->role_id, + 'route_id' => $route->id, + 'allowed' => true, + ]); + + $response = $this->get('/api/v1/users', [], $this->authHeaders($user)); + $response->assertStatus(200); + + // 清理 + RoleRouteOverride::query() + ->where('role_id', $user->role_id) + ->where('route_id', $route->id) + ->delete(); + } + + public function test_override_deny_overrides_group_auth(): void + { + $user = $this->createTestUser('developer'); + + $route = Route::query() + ->where('method', 'GET') + ->where('path', '/api/v1/users') + ->first(); + + if (!$route) { + $this->markTestSkipped('routes 表中无 GET /api/v1/users 路由'); + } + + // 先授权路由组 + $group = RouteGroup::query()->create([ + 'name' => 'test_override_deny_' . uniqid(), + 'label' => '覆盖拒绝测试', + ]); + + $old_group_id = $route->group_id; + $route->group_id = $group->id; + $route->save(); + + Db::table('role_route_groups')->insert([ + 'role_id' => $user->role_id, + 'group_id' => $group->id, + ]); + + // 设置 override 为拒绝(优先级高于 group) + RoleRouteOverride::query()->create([ + 'role_id' => $user->role_id, + 'route_id' => $route->id, + 'allowed' => false, + ]); + + $response = $this->get('/api/v1/users', [], $this->authHeaders($user)); + $response->assertStatus(403); + + // 清理 + RoleRouteOverride::query() + ->where('role_id', $user->role_id) + ->where('route_id', $route->id) + ->delete(); + Db::table('role_route_groups') + ->where('role_id', $user->role_id) + ->where('group_id', $group->id) + ->delete(); + $route->group_id = $old_group_id; + $route->save(); + $group->delete(); + } + + // ========== Step 2: 数据范围检查 ========== + + public function test_user_without_role_returns_403(): void + { + $suffix = bin2hex(random_bytes(4)); + $user = User::query()->create([ + 'username' => 'norole_' . $suffix, + 'email' => 'norole_' . $suffix . '@example.com', + 'password' => 'Pass_' . $suffix, + 'status' => 1, + 'role_id' => null, + ]); + + $response = $this->get('/api/v1/users', [], $this->authHeaders($user)); + $response->assertStatus(403); + } + + public function test_accessor_with_store_scope_and_override(): void + { + $user = $this->createTestUser('accessor'); + + $route = Route::query() + ->where('method', 'GET') + ->where('path', '/api/v1/users') + ->first(); + + if (!$route) { + $this->markTestSkipped('routes 表中无 GET /api/v1/users 路由'); + } + + // 授权路由访问 + RoleRouteOverride::query()->create([ + 'role_id' => $user->role_id, + 'route_id' => $route->id, + 'allowed' => true, + ]); + + // 添加 store scope + $store = Store::query()->first(); + if ($store) { + UserDataScope::query()->create([ + 'user_id' => $user->id, + 'scope_type' => 'store', + 'scope_id' => $store->id, + ]); + } + + $response = $this->get('/api/v1/users', [], $this->authHeaders($user)); + $response->assertStatus(200); + + // 清理 + RoleRouteOverride::query() + ->where('role_id', $user->role_id) + ->where('route_id', $route->id) + ->delete(); + UserDataScope::query()->where('user_id', $user->id)->delete(); + } + + public function test_administrator_scope_type_is_all(): void + { + $user = $this->createTestUser('administrator'); + + // administrator 应有 scope_type='all',可以正常访问 + $response = $this->get('/api/v1/users', [], $this->authHeaders($user)); + $response->assertStatus(200); + $response->assertJsonPath('code', 0); + } +} diff --git a/backend/test/Cases/Unit/Model/RoutePermissionModelTest.php b/backend/test/Cases/Unit/Model/RoutePermissionModelTest.php new file mode 100644 index 0000000..5d85e33 --- /dev/null +++ b/backend/test/Cases/Unit/Model/RoutePermissionModelTest.php @@ -0,0 +1,294 @@ + 0) { + $callback(); + return; + } + + $exception = null; + \Swoole\Coroutine\run(static function () use ($callback, &$exception): void { + try { + $callback(); + } catch (\Throwable $e) { + $exception = $e; + } + }); + if ($exception) { + throw $exception; + } + } + + // --- RouteGroup 模型测试 --- + + public function test_route_group_fillable(): void + { + $group = new RouteGroup(); + $this->assertEqualsCanonicalizing( + ['name', 'label', 'description', 'sort_order'], + $group->getFillable() + ); + } + + public function test_route_group_table_name(): void + { + $group = new RouteGroup(); + $this->assertSame('route_groups', $group->getTable()); + } + + public function test_route_group_has_no_timestamps(): void + { + $group = new RouteGroup(); + $this->assertFalse($group->timestamps); + } + + // --- Route 模型测试 --- + + public function test_route_fillable(): void + { + $route = new Route(); + $this->assertEqualsCanonicalizing( + ['group_id', 'method', 'path', 'name', 'label'], + $route->getFillable() + ); + } + + public function test_route_table_name(): void + { + $route = new Route(); + $this->assertSame('routes', $route->getTable()); + } + + public function test_route_has_no_timestamps(): void + { + $route = new Route(); + $this->assertFalse($route->timestamps); + } + + // --- RoleRouteOverride 模型测试 --- + + public function test_role_route_override_fillable(): void + { + $override = new RoleRouteOverride(); + $this->assertEqualsCanonicalizing( + ['role_id', 'route_id', 'allowed'], + $override->getFillable() + ); + } + + public function test_role_route_override_table_name(): void + { + $override = new RoleRouteOverride(); + $this->assertSame('role_route_overrides', $override->getTable()); + } + + public function test_role_route_override_casts_allowed_to_boolean(): void + { + $override = new RoleRouteOverride(); + $casts = $override->getCasts(); + $this->assertSame('boolean', $casts['allowed']); + } + + // --- 关联测试(需要数据库) --- + + public function test_route_group_has_many_routes(): void + { + $this->runInCoroutine(function (): void { + Db::beginTransaction(); + try { + $group = RouteGroup::query()->create([ + 'name' => 'test_group_' . uniqid(), + 'label' => '测试分组', + ]); + + Route::query()->create([ + 'group_id' => $group->id, + 'method' => 'GET', + 'path' => '/api/v1/test-' . uniqid(), + ]); + Route::query()->create([ + 'group_id' => $group->id, + 'method' => 'POST', + 'path' => '/api/v1/test-' . uniqid(), + ]); + + $routes = $group->routes; + $this->assertCount(2, $routes); + } finally { + Db::rollBack(); + } + }); + } + + public function test_route_belongs_to_group(): void + { + $this->runInCoroutine(function (): void { + Db::beginTransaction(); + try { + $group = RouteGroup::query()->create([ + 'name' => 'test_group_' . uniqid(), + 'label' => '测试分组', + ]); + + $route = Route::query()->create([ + 'group_id' => $group->id, + 'method' => 'GET', + 'path' => '/api/v1/test-' . uniqid(), + ]); + + $this->assertNotNull($route->group); + $this->assertSame($group->id, $route->group->id); + } finally { + Db::rollBack(); + } + }); + } + + public function test_role_has_many_route_groups(): void + { + $this->runInCoroutine(function (): void { + Db::beginTransaction(); + try { + $role = Role::query()->where('name', 'developer')->first(); + + $group1 = RouteGroup::query()->create([ + 'name' => 'test_group_a_' . uniqid(), + 'label' => '测试分组 A', + ]); + $group2 = RouteGroup::query()->create([ + 'name' => 'test_group_b_' . uniqid(), + 'label' => '测试分组 B', + ]); + + $role->routeGroups()->attach([$group1->id, $group2->id]); + + // 重新加载关联 + $role->load('routeGroups'); + $this->assertCount(2, $role->routeGroups); + + $group_ids = $role->routeGroups->pluck('id')->toArray(); + $this->assertContains($group1->id, $group_ids); + $this->assertContains($group2->id, $group_ids); + } finally { + Db::rollBack(); + } + }); + } + + public function test_role_route_override_query(): void + { + $this->runInCoroutine(function (): void { + Db::beginTransaction(); + try { + $role = Role::query()->where('name', 'accessor')->first(); + + $route = Route::query()->create([ + 'method' => 'DELETE', + 'path' => '/api/v1/test-override-' . uniqid(), + ]); + + RoleRouteOverride::query()->create([ + 'role_id' => $role->id, + 'route_id' => $route->id, + 'allowed' => false, + ]); + + // 查询角色的覆盖规则 + $overrides = $role->routeOverrides; + $this->assertGreaterThanOrEqual(1, $overrides->count()); + + $override = $overrides->where('route_id', $route->id)->first(); + $this->assertNotNull($override); + $this->assertFalse($override->allowed); + + // 查询路由的覆盖规则 + $route_overrides = $route->overrides; + $this->assertCount(1, $route_overrides); + $this->assertSame($role->id, $route_overrides->first()->role_id); + } finally { + Db::rollBack(); + } + }); + } + + public function test_route_method_path_unique_constraint(): void + { + $this->runInCoroutine(function (): void { + Db::beginTransaction(); + try { + $unique_path = '/api/v1/unique-test-' . uniqid(); + + Route::query()->create([ + 'method' => 'GET', + 'path' => $unique_path, + ]); + + $this->expectException(\Throwable::class); + Route::query()->create([ + 'method' => 'GET', + 'path' => $unique_path, + ]); + } finally { + Db::rollBack(); + } + }); + } + + public function test_route_group_roles_inverse_relation(): void + { + $this->runInCoroutine(function (): void { + Db::beginTransaction(); + try { + $role = Role::query()->where('name', 'developer')->first(); + $group = RouteGroup::query()->create([ + 'name' => 'test_inverse_' . uniqid(), + 'label' => '反向关联测试', + ]); + + $role->routeGroups()->attach($group->id); + + // 从 RouteGroup 查询关联的 Role + $group->load('roles'); + $this->assertCount(1, $group->roles); + $this->assertSame($role->id, $group->roles->first()->id); + } finally { + Db::rollBack(); + } + }); + } + + public function test_routes_synced_from_command(): void + { + $this->runInCoroutine(function (): void { + // route:sync 已执行,验证 routes 表有数据 + $count = Route::query()->count(); + $this->assertGreaterThan(0, $count); + + // 验证已知路由存在 + $route = Route::query() + ->where('method', 'GET') + ->where('path', '/api/v1/users') + ->first(); + $this->assertNotNull($route); + $this->assertNotNull($route->name); + }); + } +} diff --git a/backend/test/Cases/Unit/Service/ScopeBitmapServiceTest.php b/backend/test/Cases/Unit/Service/ScopeBitmapServiceTest.php new file mode 100644 index 0000000..16c46f0 --- /dev/null +++ b/backend/test/Cases/Unit/Service/ScopeBitmapServiceTest.php @@ -0,0 +1,350 @@ +service = new ScopeBitmapService(); + } + + protected function runInCoroutine(callable $callback): void + { + if (\Swoole\Coroutine::getCid() > 0) { + $callback(); + return; + } + + $exception = null; + \Swoole\Coroutine\run(static function () use ($callback, &$exception): void { + try { + $callback(); + } catch (\Throwable $e) { + $exception = $e; + } + }); + if ($exception) { + throw $exception; + } + } + + // --- encode/decode 双向转换 --- + + public function test_encode_empty_array_returns_empty_string(): void + { + $this->assertSame('', $this->service->encode([])); + } + + public function test_encode_single_store_id(): void + { + $bitmap = $this->service->encode([1]); + $this->assertSame([1], $this->service->decode($bitmap)); + } + + public function test_encode_multiple_store_ids(): void + { + $ids = [1, 3, 5, 8, 10]; + $bitmap = $this->service->encode($ids); + $decoded = $this->service->decode($bitmap); + $this->assertSame($ids, $decoded); + } + + public function test_encode_decode_large_ids(): void + { + $ids = [1, 100, 500, 1000]; + $bitmap = $this->service->encode($ids); + $decoded = $this->service->decode($bitmap); + $this->assertSame($ids, $decoded); + } + + public function test_decode_empty_string_returns_empty_array(): void + { + $this->assertSame([], $this->service->decode('')); + } + + public function test_encode_preserves_order(): void + { + // 编码时乱序,解码后应升序 + $bitmap = $this->service->encode([5, 1, 3]); + $this->assertSame([1, 3, 5], $this->service->decode($bitmap)); + } + + // --- has 检查 --- + + public function test_has_returns_true_for_existing_id(): void + { + $bitmap = $this->service->encode([1, 5, 10]); + $this->assertTrue($this->service->has($bitmap, 1)); + $this->assertTrue($this->service->has($bitmap, 5)); + $this->assertTrue($this->service->has($bitmap, 10)); + } + + public function test_has_returns_false_for_missing_id(): void + { + $bitmap = $this->service->encode([1, 5, 10]); + $this->assertFalse($this->service->has($bitmap, 2)); + $this->assertFalse($this->service->has($bitmap, 6)); + $this->assertFalse($this->service->has($bitmap, 11)); + } + + public function test_has_returns_false_for_id_beyond_bitmap_length(): void + { + $bitmap = $this->service->encode([1]); + $this->assertFalse($this->service->has($bitmap, 100)); + } + + public function test_has_returns_false_for_empty_bitmap(): void + { + $this->assertFalse($this->service->has('', 1)); + } + + // --- merge 合并 --- + + public function test_merge_two_bitmaps(): void + { + $bitmap1 = $this->service->encode([1, 3]); + $bitmap2 = $this->service->encode([2, 4]); + $merged = $this->service->merge($bitmap1, $bitmap2); + + $this->assertSame([1, 2, 3, 4], $this->service->decode($merged)); + } + + public function test_merge_overlapping_bitmaps(): void + { + $bitmap1 = $this->service->encode([1, 3, 5]); + $bitmap2 = $this->service->encode([3, 5, 7]); + $merged = $this->service->merge($bitmap1, $bitmap2); + + $this->assertSame([1, 3, 5, 7], $this->service->decode($merged)); + } + + public function test_merge_different_length_bitmaps(): void + { + $bitmap1 = $this->service->encode([1]); + $bitmap2 = $this->service->encode([100]); + $merged = $this->service->merge($bitmap1, $bitmap2); + + $this->assertTrue($this->service->has($merged, 1)); + $this->assertTrue($this->service->has($merged, 100)); + } + + public function test_merge_with_empty_bitmaps(): void + { + $bitmap = $this->service->encode([1, 3]); + $merged = $this->service->merge($bitmap, ''); + $this->assertSame([1, 3], $this->service->decode($merged)); + } + + public function test_merge_all_empty_returns_empty(): void + { + $this->assertSame('', $this->service->merge('', '')); + } + + // --- buildForUser(需要数据库) --- + + public function test_build_for_user_with_store_scope(): void + { + $this->runInCoroutine(function (): void { + Db::beginTransaction(); + try { + $user = User::query()->first(); + + // 查已有 store + $stores = Store::query()->limit(2)->pluck('id')->toArray(); + if (count($stores) < 2) { + $this->markTestSkipped('需要至少 2 个 store 记录'); + } + + // 插入 store 类型的 scope + foreach ($stores as $store_id) { + UserDataScope::query()->create([ + 'user_id' => $user->id, + 'scope_type' => 'store', + 'scope_id' => $store_id, + ]); + } + + $bitmap = $this->service->buildForUser($user->id); + $this->assertNotEmpty($bitmap); + + foreach ($stores as $store_id) { + $this->assertTrue($this->service->has($bitmap, $store_id)); + } + } finally { + Db::rollBack(); + } + }); + } + + public function test_build_for_user_with_company_scope(): void + { + $this->runInCoroutine(function (): void { + Db::beginTransaction(); + try { + $user = User::query()->first(); + + // 查找有 store 的 company + $store = Store::query()->first(); + if (!$store) { + $this->markTestSkipped('需要至少 1 个 store 记录'); + } + + UserDataScope::query()->create([ + 'user_id' => $user->id, + 'scope_type' => 'company', + 'scope_id' => $store->company_id, + ]); + + $bitmap = $this->service->buildForUser($user->id); + $this->assertNotEmpty($bitmap); + + // 该 company 下的 store 应在 bitmap 中 + $company_stores = Store::query() + ->where('company_id', $store->company_id) + ->pluck('id') + ->toArray(); + + foreach ($company_stores as $sid) { + $this->assertTrue($this->service->has($bitmap, $sid)); + } + } finally { + Db::rollBack(); + } + }); + } + + public function test_build_for_user_with_no_scopes_returns_empty(): void + { + $this->runInCoroutine(function (): void { + Db::beginTransaction(); + try { + $user = User::query()->first(); + // 不插入 scope + $bitmap = $this->service->buildForUser($user->id); + $this->assertSame('', $bitmap); + } finally { + Db::rollBack(); + } + }); + } + + public function test_build_for_user_with_mixed_scopes(): void + { + $this->runInCoroutine(function (): void { + Db::beginTransaction(); + try { + $user = User::query()->first(); + $store = Store::query()->first(); + if (!$store) { + $this->markTestSkipped('需要至少 1 个 store 记录'); + } + + // 同时添加 company 和 store 类型 + UserDataScope::query()->create([ + 'user_id' => $user->id, + 'scope_type' => 'company', + 'scope_id' => $store->company_id, + ]); + + // 找一个不属于该 company 的 store + $other_store = Store::query() + ->where('company_id', '!=', $store->company_id) + ->first(); + if ($other_store) { + UserDataScope::query()->create([ + 'user_id' => $user->id, + 'scope_type' => 'store', + 'scope_id' => $other_store->id, + ]); + } + + $bitmap = $this->service->buildForUser($user->id); + $this->assertNotEmpty($bitmap); + // company 下的 store 在 bitmap 中 + $this->assertTrue($this->service->has($bitmap, $store->id)); + // 额外的 store 也在 + if ($other_store) { + $this->assertTrue($this->service->has($bitmap, $other_store->id)); + } + } finally { + Db::rollBack(); + } + }); + } + + // --- buildForDeveloper --- + + public function test_build_for_developer_returns_platform_stores(): void + { + $this->runInCoroutine(function (): void { + Db::beginTransaction(); + try { + $user = User::query()->first(); + + // 查找有 store 的 platform + $store = Store::query()->first(); + if (!$store) { + $this->markTestSkipped('需要至少 1 个 store 记录'); + } + + // 将 platform 绑定到用户 + Platform::query() + ->where('id', $store->platform_id) + ->update(['developer_id' => $user->id]); + + $bitmap = $this->service->buildForDeveloper($user->id); + $this->assertNotEmpty($bitmap); + + $platform_stores = Store::query() + ->where('platform_id', $store->platform_id) + ->pluck('id') + ->toArray(); + + foreach ($platform_stores as $sid) { + $this->assertTrue($this->service->has($bitmap, $sid)); + } + } finally { + Db::rollBack(); + } + }); + } + + public function test_build_for_developer_no_platforms_returns_empty(): void + { + $this->runInCoroutine(function (): void { + Db::beginTransaction(); + try { + // 创建一个不维护任何平台的用户 + $user = User::query()->create([ + 'username' => 'dev_no_platform_' . uniqid(), + 'email' => 'devnp_' . uniqid() . '@test.com', + 'password' => 'password', + 'status' => 1, + ]); + + $bitmap = $this->service->buildForDeveloper($user->id); + $this->assertSame('', $bitmap); + } finally { + Db::rollBack(); + } + }); + } +} diff --git a/backend/test/Cases/Unit/Service/ScopeTableManagerTest.php b/backend/test/Cases/Unit/Service/ScopeTableManagerTest.php new file mode 100644 index 0000000..36d5cff --- /dev/null +++ b/backend/test/Cases/Unit/Service/ScopeTableManagerTest.php @@ -0,0 +1,192 @@ +bitmapService = new ScopeBitmapService(); + $this->manager = new ScopeTableManager($this->bitmapService); + } + + protected function runInCoroutine(callable $callback): void + { + if (\Swoole\Coroutine::getCid() > 0) { + $callback(); + return; + } + + $exception = null; + \Swoole\Coroutine\run(static function () use ($callback, &$exception): void { + try { + $callback(); + } catch (\Throwable $e) { + $exception = $e; + } + }); + if ($exception) { + throw $exception; + } + } + + public function test_table_is_created(): void + { + $table = $this->manager->getTable(); + $this->assertInstanceOf(\Swoole\Table::class, $table); + } + + public function test_get_user_scope_returns_null_for_nonexistent_user(): void + { + $this->runInCoroutine(function (): void { + $result = $this->manager->getUserScope(999999); + $this->assertNull($result); + }); + } + + public function test_rebuild_user_scope_for_administrator(): void + { + $this->runInCoroutine(function (): void { + Db::beginTransaction(); + try { + $admin_role = Role::query()->where('name', 'administrator')->first(); + $user = User::query()->where('role_id', $admin_role->id)->first(); + + if (!$user) { + $this->markTestSkipped('需要 administrator 用户'); + } + + $result = $this->manager->rebuildUserScope($user->id); + $this->assertNotNull($result); + $this->assertSame('administrator', $result['role']); + $this->assertSame('', $result['scope']); + $this->assertGreaterThan(0, $result['version']); + } finally { + Db::rollBack(); + } + }); + } + + public function test_get_user_scope_lazy_loads_on_miss(): void + { + $this->runInCoroutine(function (): void { + Db::beginTransaction(); + try { + $admin_role = Role::query()->where('name', 'administrator')->first(); + $user = User::query()->where('role_id', $admin_role->id)->first(); + + if (!$user) { + $this->markTestSkipped('需要 administrator 用户'); + } + + // 第一次调用应触发懒加载 + $result = $this->manager->getUserScope($user->id); + $this->assertNotNull($result); + $this->assertSame('administrator', $result['role']); + + // 第二次调用应命中 Table + $result2 = $this->manager->getUserScope($user->id); + $this->assertNotNull($result2); + $this->assertSame($result['version'], $result2['version']); + } finally { + Db::rollBack(); + } + }); + } + + public function test_invalidate_user_removes_from_table(): void + { + $this->runInCoroutine(function (): void { + Db::beginTransaction(); + try { + $admin_role = Role::query()->where('name', 'administrator')->first(); + $user = User::query()->where('role_id', $admin_role->id)->first(); + + if (!$user) { + $this->markTestSkipped('需要 administrator 用户'); + } + + // 先加载到 Table + $this->manager->rebuildUserScope($user->id); + + // 验证存在 + $table = $this->manager->getTable(); + $this->assertNotFalse($table->get((string) $user->id)); + + // 清除 + $this->manager->invalidateUser($user->id); + + // 验证已删除 + $this->assertFalse($table->get((string) $user->id)); + } finally { + Db::rollBack(); + } + }); + } + + public function test_rebuild_writes_correct_data_to_table(): void + { + $this->runInCoroutine(function (): void { + Db::beginTransaction(); + try { + $admin_role = Role::query()->where('name', 'administrator')->first(); + $user = User::query()->where('role_id', $admin_role->id)->first(); + + if (!$user) { + $this->markTestSkipped('需要 administrator 用户'); + } + + $this->manager->rebuildUserScope($user->id); + + $table = $this->manager->getTable(); + $row = $table->get((string) $user->id); + + $this->assertIsArray($row); + $this->assertArrayHasKey('role', $row); + $this->assertArrayHasKey('scope', $row); + $this->assertArrayHasKey('version', $row); + } finally { + Db::rollBack(); + } + }); + } + + public function test_rebuild_returns_null_for_user_without_role(): void + { + $this->runInCoroutine(function (): void { + Db::beginTransaction(); + try { + // 创建无角色用户 + $user = User::query()->create([ + 'username' => 'no_role_user_' . uniqid(), + 'email' => 'norole_' . uniqid() . '@test.com', + 'password' => 'password', + 'status' => 1, + 'role_id' => null, + ]); + + $result = $this->manager->rebuildUserScope($user->id); + $this->assertNull($result); + } finally { + Db::rollBack(); + } + }); + } +}