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()); } }