assertIdentityPoliciesSyncedForExtension( * extensionType: 'module', * extensionIdentifier: 'sirsoft-board', * declaredKeys: self::DECLARED_KEYS, * syncCallback: fn () => $this->syncBoardIdentityPolicies(), * ); * } * } * ``` * * @since 7.0.0-beta.4 */ trait AssertsIdentityPolicyDeclaration { /** * 선언된 정책이 정확한 source 컨텍스트로 동기화되었는지 검증합니다. * * @param string $extensionType 'module' 또는 'plugin' * @param string $extensionIdentifier 확장 식별자 (예: 'sirsoft-board') * @param list $declaredKeys module.php / plugin.php 의 getIdentityPolicies() 가 반환하는 정책 키 목록 * @param callable $syncCallback 실제 동기화 동작 (보통 ModuleManager::syncModuleIdentityPolicies 의 helper 직접 호출) */ protected function assertIdentityPoliciesSyncedForExtension( string $extensionType, string $extensionIdentifier, array $declaredKeys, callable $syncCallback, ): void { $syncCallback(); foreach ($declaredKeys as $key) { $policy = IdentityPolicy::query()->where('key', $key)->first(); $this->assertNotNull($policy, "정책 {$key} 가 동기화되지 않음"); $this->assertSame($extensionType, $policy->source_type->value, "{$key} 의 source_type 이 {$extensionType} 가 아님"); $this->assertSame($extensionIdentifier, $policy->source_identifier, "{$key} 의 source_identifier 가 {$extensionIdentifier} 가 아님"); $this->assertFalse((bool) $policy->enabled, "정책 {$key} 의 기본값은 enabled=false 여야 함"); } } /** * cleanupStalePolicies 가 선언에 없는 정책을 정리하는지 검증합니다. * * 호출 흐름: syncCallback() → 운영자가 1건 제거 시뮬레이션 → cleanupStalePolicies → * 제거된 키는 DB 에서 삭제 / 유지된 키는 보존. * * @param string $extensionType * @param string $extensionIdentifier * @param list $declaredKeys 전체 선언 키 (마지막 1건이 stale 시뮬레이션 대상) * @param callable $syncCallback 초기 동기화 콜백 */ protected function assertIdentityPolicyStaleCleanup( string $extensionType, string $extensionIdentifier, array $declaredKeys, callable $syncCallback, ): void { $syncCallback(); $reducedKeys = array_slice($declaredKeys, 0, count($declaredKeys) - 1); $removedKey = $declaredKeys[count($declaredKeys) - 1]; $helper = app(IdentityPolicySyncHelper::class); $helper->cleanupStalePolicies($extensionType, $extensionIdentifier, $reducedKeys); $this->assertNull( IdentityPolicy::query()->where('key', $removedKey)->first(), "stale 정책 {$removedKey} 가 정리되지 않음" ); foreach ($reducedKeys as $key) { $this->assertNotNull( IdentityPolicy::query()->where('key', $key)->first(), "유지되어야 할 정책 {$key} 가 잘못 삭제됨" ); } } /** * 운영자 수정값이 모듈 재동기화 후에도 보존되는지 검증합니다. * * @param string $key 검증 대상 정책 key (단일) * @param array $overrides 운영자 수정 값 (예: ['enabled' => true, 'grace_minutes' => 120]) * @param callable $syncCallback 동기화 콜백 (2회 호출됨) */ protected function assertIdentityPolicyUserOverridesPreserved( string $key, array $overrides, callable $syncCallback, ): void { $syncCallback(); $repository = app(\App\Contracts\Repositories\IdentityPolicyRepositoryInterface::class); $repository->updateByKey($key, $overrides, array_keys($overrides)); // 재동기화 (모듈 update 시뮬레이션) $syncCallback(); $policy = IdentityPolicy::query()->where('key', $key)->first()->refresh(); foreach ($overrides as $field => $expected) { $actual = $policy->{$field}; if (is_bool($expected)) { $actual = (bool) $actual; } elseif (is_int($expected)) { $actual = (int) $actual; } $this->assertSame( $expected, $actual, "재동기화 후 user_overrides 의 {$field} 값이 보존되지 않음" ); } $this->assertEqualsCanonicalizing( array_keys($overrides), $policy->user_overrides ?? [], "user_overrides 컬럼이 정확히 기록되지 않음" ); } }