Files

552 lines
17 KiB
PHP
Raw Permalink Normal View History

2026-03-13 09:07:42 +08:00
<?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\User;
use App\Model\UserDataScope;
use Hyperf\DbConnection\Db;
use HyperfTest\TestCase;
use Qbhy\HyperfAuth\AuthManager;
use function Hyperf\Support\make;
/**
* 权限管理 API 集成测试(P3.4)
*
* @internal
* @coversNothing
*/
class PermissionApiTest extends TestCase
{
// ========== Helpers ==========
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' => '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);
}
}