Files
datahub/backend/test/Cases/Integration/Permission/PermissionFlowTest.php
T
2026-03-12 15:27:41 +08:00

404 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace HyperfTest\Cases\Integration\Permission;
use App\Model\Role;
use App\Model\RoleRouteOverride;
use App\Model\Route;
use App\Model\RouteGroup;
use App\Model\Store;
use App\Model\User;
use App\Model\UserDataScope;
use Hyperf\DbConnection\Db;
use HyperfTest\TestCase;
use Qbhy\HyperfAuth\AuthManager;
use function Hyperf\Support\make;
/**
* 权限中间件端到端集成测试
*
* 覆盖 AuthMiddleware → PermissionMiddleware → Controller 完整链路
*
* @internal
* @coversNothing
*/
class PermissionFlowTest extends TestCase
{
protected function getAuthToken(User $user): string
{
$auth = make(AuthManager::class);
return $auth->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' => 'integ_perm_' . $suffix,
'email' => 'integ_perm_' . $suffix . '@example.com',
'password' => 'Pass_' . $suffix,
'status' => 1,
'role_id' => $role->id,
], $overrides));
}
/**
* 为指定角色授权指定路由的路由组
*
* @return array{group: RouteGroup, old_group_id: ?int} 用于清理
*/
protected function authorizeRouteGroup(int $role_id, Route $route): array
{
$group = RouteGroup::query()->create([
'name' => 'integ_test_' . uniqid(),
'label' => '集成测试路由组',
]);
$old_group_id = $route->group_id;
$route->group_id = $group->id;
$route->save();
Db::table('role_route_groups')->insert([
'role_id' => $role_id,
'group_id' => $group->id,
]);
return ['group' => $group, 'old_group_id' => $old_group_id];
}
/**
* 清理路由组授权
*/
protected function cleanupRouteGroup(int $role_id, Route $route, array $auth_data): void
{
Db::table('role_route_groups')
->where('role_id', $role_id)
->where('group_id', $auth_data['group']->id)
->delete();
$route->group_id = $auth_data['old_group_id'];
$route->save();
$auth_data['group']->delete();
}
// ========== 测试用例 ==========
public function test_admin_full_access(): void
{
$user = $this->createTestUser('administrator');
// administrator 可访问全部 UserController 端点
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
$response->assertStatus(200);
$response->assertJsonPath('code', 0);
// 参数化路由
$response = $this->get('/api/v1/users/1', [], $this->authHeaders($user));
$this->assertContains($response->getStatusCode(), [200, 404]);
}
public function test_developer_with_group_auth_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 路由');
}
$auth_data = $this->authorizeRouteGroup($user->role_id, $route);
try {
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
$response->assertStatus(200);
} finally {
$this->cleanupRouteGroup($user->role_id, $route, $auth_data);
}
}
public function test_accessor_without_auth_denied(): void
{
$user = $this->createTestUser('accessor');
// accessor 无任何路由授权 → 403
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
$response->assertStatus(403);
}
public function test_accessor_with_override_access(): 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 路由');
}
// 添加 store scope 以通过数据范围检查
$store = Store::query()->first();
if ($store) {
UserDataScope::query()->create([
'user_id' => $user->id,
'scope_type' => 'store',
'scope_id' => $store->id,
]);
}
// 设置 override 允许访问
RoleRouteOverride::query()->create([
'role_id' => $user->role_id,
'route_id' => $route->id,
'allowed' => true,
]);
try {
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
$response->assertStatus(200);
} finally {
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_override_deny_overrides_group(): 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 路由');
}
// 授权路由组
$auth_data = $this->authorizeRouteGroup($user->role_id, $route);
// 设置 override 为拒绝(优先级高于 group)
RoleRouteOverride::query()->create([
'role_id' => $user->role_id,
'route_id' => $route->id,
'allowed' => false,
]);
try {
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
$response->assertStatus(403);
} finally {
RoleRouteOverride::query()
->where('role_id', $user->role_id)
->where('route_id', $route->id)
->delete();
$this->cleanupRouteGroup($user->role_id, $route, $auth_data);
}
}
public function test_developer_scope_param_validation(): 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 路由');
}
// 授权路由访问
$auth_data = $this->authorizeRouteGroup($user->role_id, $route);
try {
// 传不属于自己的 platform_id → 403
$response = $this->get('/api/v1/users', ['platform_id' => 999999], $this->authHeaders($user));
$response->assertStatus(403);
} finally {
$this->cleanupRouteGroup($user->role_id, $route, $auth_data);
}
}
// ========== 边缘场景 ==========
public function test_accessor_with_invalid_store_id_denied(): 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 scopestore_id=1),然后传不在 bitmap 中的 store_id
$store = Store::query()->first();
if (!$store) {
$this->markTestSkipped('stores 表中无数据');
}
UserDataScope::query()->create([
'user_id' => $user->id,
'scope_type' => 'store',
'scope_id' => $store->id,
]);
try {
// 传不属于自己的 store_id → 403
$response = $this->get('/api/v1/users', ['store_id' => 999999], $this->authHeaders($user));
$response->assertStatus(403);
} finally {
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_accessor_with_invalid_company_id_denied(): 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 = Store::query()->first();
if (!$store) {
$this->markTestSkipped('stores 表中无数据');
}
UserDataScope::query()->create([
'user_id' => $user->id,
'scope_type' => 'store',
'scope_id' => $store->id,
]);
try {
// 传不属于自己的 company_id → 403
$response = $this->get('/api/v1/users', ['company_id' => 999999], $this->authHeaders($user));
$response->assertStatus(403);
} finally {
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_accessor_with_no_scope_data_gets_empty_results(): 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 路由');
}
// 授权路由访问但不设置任何 scope 数据
RoleRouteOverride::query()->create([
'role_id' => $user->role_id,
'route_id' => $route->id,
'allowed' => true,
]);
try {
// accessor 无 scope 数据 → bitmap 为空 → scope_ids=[] → 200 但无数据
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
$response->assertStatus(200);
$response->assertJsonPath('code', 0);
} finally {
RoleRouteOverride::query()
->where('role_id', $user->role_id)
->where('route_id', $route->id)
->delete();
}
}
public function test_developer_with_invalid_store_id_denied(): 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 路由');
}
$auth_data = $this->authorizeRouteGroup($user->role_id, $route);
try {
// developer 传不属于自己的 store_id → 403
$response = $this->get('/api/v1/users', ['store_id' => 999999], $this->authHeaders($user));
$response->assertStatus(403);
} finally {
$this->cleanupRouteGroup($user->role_id, $route, $auth_data);
}
}
public function test_developer_with_no_platforms_gets_empty_scope(): void
{
// 创建 developer 用户但不关联任何 platform
$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 路由');
}
$auth_data = $this->authorizeRouteGroup($user->role_id, $route);
try {
// developer 无维护平台 → platform_ids=[] → scope_ids=[] → 200 但无数据
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
$response->assertStatus(200);
$response->assertJsonPath('code', 0);
} finally {
$this->cleanupRouteGroup($user->role_id, $route, $auth_data);
}
}
public function test_ungrouped_route_denied_for_non_admin(): 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 路由');
}
// 确保路由未分组且无 override
$old_group_id = $route->group_id;
$route->group_id = null;
$route->save();
try {
// 路由已注册但未分组、无 override → 403
$response = $this->get('/api/v1/users', [], $this->authHeaders($user));
$response->assertStatus(403);
} finally {
$route->group_id = $old_group_id;
$route->save();
}
}
}