552 lines
17 KiB
PHP
552 lines
17 KiB
PHP
<?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);
|
|
}
|
|
}
|