Files
Gnuboard7/tests/Unit/Repositories/IdentityPolicyRepositoryActiveExtensionScopeTest.php
T
2026-07-01 10:30:32 +09:00

243 lines
8.1 KiB
PHP

<?php
namespace Tests\Unit\Repositories;
use App\Contracts\Repositories\IdentityPolicyRepositoryInterface;
use App\Enums\ExtensionStatus;
use App\Models\IdentityPolicy;
use App\Models\Module;
use App\Models\Plugin;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* IdentityPolicyRepository 의 비활성 확장 정책 제외(applyActiveExtensionScope) 검증.
*
* 회귀 배경: 모듈/플러그인을 비활성화해도 그 확장이 선언한 IDV 정책이 enforce 쿼리에
* 그대로 남아 계속 강제되던 결함. enforce 두 경로(resolveByScopeTarget=hook,
* getRouteScopeIndex=route)가 source_type/source_identifier 기준으로 비활성 확장
* 정책을 제외하는지, 그리고 재활성화 시 다시 포함되는지(3-state)를 검증한다.
*
* @group identity
* @group unit
*/
class IdentityPolicyRepositoryActiveExtensionScopeTest extends TestCase
{
use RefreshDatabase;
private IdentityPolicyRepositoryInterface $repository;
protected function setUp(): void
{
parent::setUp();
$this->repository = app(IdentityPolicyRepositoryInterface::class);
}
/**
* 모듈 레코드를 생성합니다.
*/
private function makeModule(string $identifier, string $status): Module
{
return Module::create([
'identifier' => $identifier,
'vendor' => 'sirsoft',
'name' => ['ko' => $identifier, 'en' => $identifier],
'version' => '1.0.0',
'status' => $status,
]);
}
/**
* 플러그인 레코드를 생성합니다.
*/
private function makePlugin(string $identifier, string $status): Plugin
{
return Plugin::create([
'identifier' => $identifier,
'vendor' => 'sirsoft',
'name' => ['ko' => $identifier, 'en' => $identifier],
'version' => '1.0.0',
'status' => $status,
]);
}
/**
* 정책을 생성합니다.
*/
private function makePolicy(array $overrides = []): IdentityPolicy
{
return IdentityPolicy::create(array_merge([
'key' => 'test.policy.'.uniqid(),
'scope' => 'hook',
'target' => 'test.hook.target',
'purpose' => 'sensitive_action',
'grace_minutes' => 5,
'enabled' => true,
'priority' => 0,
'source_type' => 'core',
'source_identifier' => 'core',
'applies_to' => 'both',
'fail_mode' => 'block',
], $overrides));
}
/**
* hook scope: 활성 모듈의 정책은 enforce 대상에 포함된다
*/
public function test_hook_scope_includes_active_module_policy(): void
{
$this->makeModule('sirsoft-fake', ExtensionStatus::Active->value);
$this->makePolicy([
'scope' => 'hook',
'target' => 'fake.hook',
'source_type' => 'module',
'source_identifier' => 'sirsoft-fake',
]);
$result = $this->repository->resolveByScopeTarget('hook', 'fake.hook');
$this->assertCount(1, $result);
}
/**
* hook scope: 비활성 모듈의 정책은 enforce 대상에서 제외된다 (회귀)
*/
public function test_hook_scope_excludes_inactive_module_policy(): void
{
$this->makeModule('sirsoft-fake', ExtensionStatus::Inactive->value);
$this->makePolicy([
'scope' => 'hook',
'target' => 'fake.hook',
'source_type' => 'module',
'source_identifier' => 'sirsoft-fake',
]);
$result = $this->repository->resolveByScopeTarget('hook', 'fake.hook');
$this->assertCount(0, $result);
}
/**
* hook scope: 모듈 재활성화 시 정책이 다시 enforce 대상에 포함된다 (3-state)
*/
public function test_hook_scope_reincludes_policy_after_reactivation(): void
{
$module = $this->makeModule('sirsoft-fake', ExtensionStatus::Inactive->value);
$this->makePolicy([
'scope' => 'hook',
'target' => 'fake.hook',
'source_type' => 'module',
'source_identifier' => 'sirsoft-fake',
]);
// 비활성: 제외
$this->assertCount(0, $this->repository->resolveByScopeTarget('hook', 'fake.hook'));
// 재활성화: 다시 포함
$module->update(['status' => ExtensionStatus::Active->value]);
$this->assertCount(1, $this->repository->resolveByScopeTarget('hook', 'fake.hook'));
}
/**
* hook scope: 비활성 플러그인의 정책도 제외된다
*/
public function test_hook_scope_excludes_inactive_plugin_policy(): void
{
$this->makePlugin('sirsoft-fakeplugin', ExtensionStatus::Inactive->value);
$this->makePolicy([
'scope' => 'hook',
'target' => 'fakeplugin.hook',
'source_type' => 'plugin',
'source_identifier' => 'sirsoft-fakeplugin',
]);
$result = $this->repository->resolveByScopeTarget('hook', 'fakeplugin.hook');
$this->assertCount(0, $result);
}
/**
* core/admin 정책은 확장 상태와 무관하게 항상 포함된다 (비파괴)
*/
public function test_hook_scope_always_includes_core_and_admin_policies(): void
{
$this->makePolicy([
'scope' => 'hook',
'target' => 'core.hook',
'source_type' => 'core',
'source_identifier' => 'core',
]);
$this->makePolicy([
'scope' => 'hook',
'target' => 'admin.hook',
'source_type' => 'admin',
'source_identifier' => 'admin',
]);
$this->assertCount(1, $this->repository->resolveByScopeTarget('hook', 'core.hook'));
$this->assertCount(1, $this->repository->resolveByScopeTarget('hook', 'admin.hook'));
}
/**
* route scope: 활성 모듈의 정책은 라우트 인덱스에 포함된다
*/
public function test_route_scope_includes_active_module_policy(): void
{
$this->makeModule('sirsoft-fake', ExtensionStatus::Active->value);
$this->makePolicy([
'scope' => 'route',
'target' => 'api.fake.delete',
'source_type' => 'module',
'source_identifier' => 'sirsoft-fake',
]);
IdentityPolicy::flushRouteScopeCache();
$index = $this->repository->getRouteScopeIndex();
$this->assertArrayHasKey('api.fake.delete', $index);
$this->assertCount(1, $index['api.fake.delete']);
}
/**
* route scope: 비활성 모듈의 정책은 라우트 인덱스에서 제외된다 (회귀)
*/
public function test_route_scope_excludes_inactive_module_policy(): void
{
$this->makeModule('sirsoft-fake', ExtensionStatus::Inactive->value);
$this->makePolicy([
'scope' => 'route',
'target' => 'api.fake.delete',
'source_type' => 'module',
'source_identifier' => 'sirsoft-fake',
]);
IdentityPolicy::flushRouteScopeCache();
$index = $this->repository->getRouteScopeIndex();
$this->assertArrayNotHasKey('api.fake.delete', $index);
}
/**
* route scope: 모듈 재활성화 시 라우트 인덱스에 다시 포함된다 (3-state, 캐시 무효화 동반)
*/
public function test_route_scope_reincludes_policy_after_reactivation(): void
{
$module = $this->makeModule('sirsoft-fake', ExtensionStatus::Inactive->value);
$this->makePolicy([
'scope' => 'route',
'target' => 'api.fake.delete',
'source_type' => 'module',
'source_identifier' => 'sirsoft-fake',
]);
// 비활성: 제외
IdentityPolicy::flushRouteScopeCache();
$this->assertArrayNotHasKey('api.fake.delete', $this->repository->getRouteScopeIndex());
// 재활성화 + 캐시 무효화: 다시 포함
$module->update(['status' => ExtensionStatus::Active->value]);
IdentityPolicy::flushRouteScopeCache();
$this->assertArrayHasKey('api.fake.delete', $this->repository->getRouteScopeIndex());
}
}