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_api_' . $suffix, 'email' => 'perm_api_' . $suffix . '@example.com', 'password' => 'Pass_' . $suffix, 'status' => 1, 'role_id' => $role->id, ], $overrides)); } protected function getAdmin(): User { $admin_role = Role::query()->where('name', 'administrator')->firstOrFail(); $user = User::query()->where('status', 1)->where('role_id', $admin_role->id)->first(); if (!$user) { $user = $this->createTestUser('administrator'); } return $user; } // ========== 路由组 CRUD ========== public function test_admin_can_list_route_groups(): void { $admin = $this->getAdmin(); $response = $this->get('/api/v1/route-groups', [], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); } public function test_admin_can_create_route_group(): void { $admin = $this->getAdmin(); $name = 'test_group_' . bin2hex(random_bytes(4)); $response = $this->post('/api/v1/route-groups', [ 'name' => $name, 'label' => '测试路由组', 'description' => '测试用', 'sort_order' => 10, ], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); $response->assertJsonPath('data.name', $name); $response->assertJsonPath('data.sort_order', 10); // 清理 $body = json_decode($response->getContent(), true); RouteGroup::query()->find($body['data']['id'])?->delete(); } public function test_create_route_group_duplicate_name_returns_400(): void { $admin = $this->getAdmin(); $name = 'dup_group_' . bin2hex(random_bytes(4)); $group = RouteGroup::query()->create(['name' => $name, 'label' => 'dup']); try { $response = $this->post('/api/v1/route-groups', [ 'name' => $name, 'label' => '重复', ], $this->authHeaders($admin)); $response->assertStatus(400); } finally { $group->delete(); } } public function test_admin_can_update_route_group(): void { $admin = $this->getAdmin(); $group = RouteGroup::query()->create([ 'name' => 'upd_group_' . bin2hex(random_bytes(4)), 'label' => '原始', ]); try { $response = $this->put('/api/v1/route-groups/' . $group->id, [ 'label' => '已更新', 'sort_order' => 5, ], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); $response->assertJsonPath('data.label', '已更新'); $response->assertJsonPath('data.sort_order', 5); } finally { $group->delete(); } } public function test_admin_can_delete_route_group(): void { $admin = $this->getAdmin(); $group = RouteGroup::query()->create([ 'name' => 'del_group_' . bin2hex(random_bytes(4)), 'label' => '待删除', ]); $response = $this->delete('/api/v1/route-groups/' . $group->id, [], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); $this->assertNull(RouteGroup::query()->find($group->id)); } public function test_delete_nonexistent_group_returns_404(): void { $admin = $this->getAdmin(); $response = $this->delete('/api/v1/route-groups/999999', [], $this->authHeaders($admin)); $response->assertStatus(404); } // ========== 路由列表与分组分配 ========== public function test_admin_can_list_routes(): void { $admin = $this->getAdmin(); $response = $this->get('/api/v1/routes', [], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); } public function test_admin_can_filter_ungrouped_routes(): void { $admin = $this->getAdmin(); $response = $this->get('/api/v1/routes', ['group_id' => 'ungrouped'], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); // 验证返回的路由都没有 group_id $body = json_decode($response->getContent(), true); foreach ($body['data'] as $route) { $this->assertNull($route['group_id']); } } public function test_admin_can_assign_route_to_group(): void { $admin = $this->getAdmin(); $route = Route::query()->first(); if (!$route) { $this->markTestSkipped('routes 表中无数据'); } $group = RouteGroup::query()->create([ 'name' => 'assign_grp_' . bin2hex(random_bytes(4)), 'label' => '分配测试', ]); $old_group_id = $route->group_id; try { $response = $this->put('/api/v1/routes/' . $route->id . '/group', [ 'group_id' => $group->id, ], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); $response->assertJsonPath('data.group_id', $group->id); } finally { $route->group_id = $old_group_id; $route->save(); $group->delete(); } } public function test_admin_can_remove_route_from_group(): void { $admin = $this->getAdmin(); $route = Route::query()->whereNotNull('group_id')->first(); if (!$route) { $this->markTestSkipped('routes 表中无已分组路由'); } $old_group_id = $route->group_id; try { $response = $this->put('/api/v1/routes/' . $route->id . '/group', [ 'group_id' => null, ], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('data.group_id', null); } finally { $route->group_id = $old_group_id; $route->save(); } } // ========== 角色路由授权 ========== public function test_admin_can_get_role_route_groups(): void { $admin = $this->getAdmin(); $role = Role::query()->where('name', 'developer')->firstOrFail(); $response = $this->get('/api/v1/roles/' . $role->id . '/route-groups', [], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); } public function test_admin_can_set_role_route_groups(): void { $admin = $this->getAdmin(); $role = Role::query()->where('name', 'developer')->firstOrFail(); $group = RouteGroup::query()->create([ 'name' => 'auth_grp_' . bin2hex(random_bytes(4)), 'label' => '授权测试', ]); try { $response = $this->put('/api/v1/roles/' . $role->id . '/route-groups', [ 'group_ids' => [$group->id], ], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); $body = json_decode($response->getContent(), true); $returned_ids = array_column($body['data'], 'id'); $this->assertContains($group->id, $returned_ids); } finally { // 清理授权 $role->routeGroups()->detach($group->id); $group->delete(); } } public function test_set_admin_route_groups_returns_400(): void { $admin = $this->getAdmin(); $admin_role = Role::query()->where('name', 'administrator')->firstOrFail(); $response = $this->put('/api/v1/roles/' . $admin_role->id . '/route-groups', [ 'group_ids' => [1], ], $this->authHeaders($admin)); $response->assertStatus(400); } public function test_admin_can_get_role_route_overrides(): void { $admin = $this->getAdmin(); $role = Role::query()->where('name', 'accessor')->firstOrFail(); $response = $this->get('/api/v1/roles/' . $role->id . '/route-overrides', [], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); } public function test_admin_can_set_role_route_overrides(): void { $admin = $this->getAdmin(); $role = Role::query()->where('name', 'accessor')->firstOrFail(); $route = Route::query()->first(); if (!$route) { $this->markTestSkipped('routes 表中无数据'); } try { $response = $this->put('/api/v1/roles/' . $role->id . '/route-overrides', [ 'overrides' => [ ['route_id' => $route->id, 'allowed' => true], ], ], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); // 验证数据库 $override = RoleRouteOverride::query() ->where('role_id', $role->id) ->where('route_id', $route->id) ->first(); $this->assertNotNull($override); $this->assertTrue($override->allowed); } finally { RoleRouteOverride::query() ->where('role_id', $role->id) ->where('route_id', $route->id) ->delete(); } } public function test_set_route_overrides_duplicate_route_id_returns_400(): void { $admin = $this->getAdmin(); $role = Role::query()->where('name', 'accessor')->firstOrFail(); $route = Route::query()->first(); if (!$route) { $this->markTestSkipped('routes 表中无数据'); } $response = $this->put('/api/v1/roles/' . $role->id . '/route-overrides', [ 'overrides' => [ ['route_id' => $route->id, 'allowed' => true], ['route_id' => $route->id, 'allowed' => false], ], ], $this->authHeaders($admin)); $response->assertStatus(400); } // ========== 用户数据范围 ========== public function test_admin_can_get_user_data_scope(): void { $admin = $this->getAdmin(); $user = $this->createTestUser('accessor'); $response = $this->get('/api/v1/users/' . $user->id . '/data-scope', [], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); $body = json_decode($response->getContent(), true); $this->assertSame($user->id, $body['data']['user_id']); $this->assertArrayHasKey('scopes', $body['data']); $this->assertArrayHasKey('resolved_store_ids', $body['data']); } public function test_admin_can_set_user_data_scope(): void { $admin = $this->getAdmin(); $user = $this->createTestUser('accessor'); try { $response = $this->put('/api/v1/users/' . $user->id . '/data-scope', [ 'scopes' => [ ['scope_type' => 'company', 'scope_id' => 1], ['scope_type' => 'store', 'scope_id' => 1], ], ], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); // 验证数据库 $scopes = UserDataScope::query()->where('user_id', $user->id)->get(); $this->assertCount(2, $scopes); } finally { UserDataScope::query()->where('user_id', $user->id)->delete(); } } public function test_set_data_scope_with_empty_array_clears_scopes(): void { $admin = $this->getAdmin(); $user = $this->createTestUser('accessor'); // 先设置一些 scope UserDataScope::query()->create([ 'user_id' => $user->id, 'scope_type' => 'store', 'scope_id' => 1, ]); $response = $this->put('/api/v1/users/' . $user->id . '/data-scope', [ 'scopes' => [], ], $this->authHeaders($admin)); $response->assertStatus(200); $scopes = UserDataScope::query()->where('user_id', $user->id)->get(); $this->assertCount(0, $scopes); } public function test_set_data_scope_invalid_scope_type_returns_400(): void { $admin = $this->getAdmin(); $user = $this->createTestUser('accessor'); $response = $this->put('/api/v1/users/' . $user->id . '/data-scope', [ 'scopes' => [ ['scope_type' => 'invalid', 'scope_id' => 1], ], ], $this->authHeaders($admin)); $response->assertStatus(400); } // ========== 角色管理 ========== public function test_admin_can_list_roles(): void { $admin = $this->getAdmin(); $response = $this->get('/api/v1/roles', [], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); $body = json_decode($response->getContent(), true); $this->assertGreaterThanOrEqual(3, count($body['data'])); // administrator, developer, accessor } public function test_admin_can_assign_role(): void { $admin = $this->getAdmin(); $user = $this->createTestUser('accessor'); $developer_role = Role::query()->where('name', 'developer')->firstOrFail(); $old_role_id = $user->role_id; try { $response = $this->put('/api/v1/users/' . $user->id . '/role', [ 'role_id' => $developer_role->id, ], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); $response->assertJsonPath('data.role_id', $developer_role->id); } finally { $user->role_id = $old_role_id; $user->save(); } } public function test_admin_cannot_downgrade_self(): void { $admin = $this->getAdmin(); $accessor_role = Role::query()->where('name', 'accessor')->firstOrFail(); $response = $this->put('/api/v1/users/' . $admin->id . '/role', [ 'role_id' => $accessor_role->id, ], $this->authHeaders($admin)); $response->assertStatus(400); } public function test_assign_nonexistent_role_returns_404(): void { $admin = $this->getAdmin(); $user = $this->createTestUser('accessor'); $response = $this->put('/api/v1/users/' . $user->id . '/role', [ 'role_id' => 999999, ], $this->authHeaders($admin)); $response->assertStatus(404); } // ========== 基础数据接口 ========== public function test_admin_can_get_companies(): void { $admin = $this->getAdmin(); $response = $this->get('/api/v1/companies', [], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); } public function test_admin_can_get_platforms(): void { $admin = $this->getAdmin(); $response = $this->get('/api/v1/platforms', [], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); } public function test_admin_can_get_stores(): void { $admin = $this->getAdmin(); $response = $this->get('/api/v1/stores', [], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); } public function test_stores_support_company_filter(): void { $admin = $this->getAdmin(); $response = $this->get('/api/v1/stores', ['company_id' => 1], $this->authHeaders($admin)); $response->assertStatus(200); $response->assertJsonPath('code', 0); } // ========== 认证拦截 ========== public function test_unauthenticated_returns_401(): void { $response = $this->get('/api/v1/route-groups'); $response->assertStatus(401); $response = $this->get('/api/v1/roles'); $response->assertStatus(401); $response = $this->get('/api/v1/companies'); $response->assertStatus(401); } }