update test
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace HyperfTest\Cases\Unit\Middleware;
|
||||
|
||||
use App\Model\Role;
|
||||
use App\Model\Route;
|
||||
use App\Model\RouteGroup;
|
||||
use App\Model\RoleRouteOverride;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
class PermissionMiddlewareTest 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' => '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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace HyperfTest\Cases\Unit\Model;
|
||||
|
||||
use App\Model\Role;
|
||||
use App\Model\Route;
|
||||
use App\Model\RouteGroup;
|
||||
use App\Model\RoleRouteOverride;
|
||||
use Hyperf\DbConnection\Db;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
class RoutePermissionModelTest extends TestCase
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace HyperfTest\Cases\Unit\Service;
|
||||
|
||||
use App\Model\Platform;
|
||||
use App\Model\Store;
|
||||
use App\Model\User;
|
||||
use App\Model\UserDataScope;
|
||||
use App\Service\ScopeBitmapService;
|
||||
use Hyperf\DbConnection\Db;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
class ScopeBitmapServiceTest extends TestCase
|
||||
{
|
||||
protected ScopeBitmapService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace HyperfTest\Cases\Unit\Service;
|
||||
|
||||
use App\Model\Role;
|
||||
use App\Model\User;
|
||||
use App\Service\ScopeBitmapService;
|
||||
use App\Service\ScopeTableManager;
|
||||
use Hyperf\DbConnection\Db;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
class ScopeTableManagerTest extends TestCase
|
||||
{
|
||||
protected ScopeTableManager $manager;
|
||||
protected ScopeBitmapService $bitmapService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user