seed(IdentityPolicySeeder::class); } public function test_core_seeds_exactly_nine_core_policies(): void { $count = IdentityPolicy::query() ->where('source_type', 'core') ->where('source_identifier', 'core') ->count(); $this->assertSame(9, $count, '코어 정책은 정확히 9건이어야 함 (config/core.php 의 identity_policies 블록)'); } /** * @dataProvider coreDeclarationProvider */ public function test_core_policy_matches_declaration(string $key, array $expected): void { $policy = IdentityPolicy::where('key', $key)->first(); $this->assertNotNull($policy, "정책 '{$key}' 가 DB 에 없음"); foreach ($expected as $field => $value) { $actual = $policy->{$field}; // enum cast 된 분류 컬럼(scope/fail_mode/applies_to/source_type) 은 enum 객체이므로 ->value 비교 if ($actual instanceof \BackedEnum) { $actual = $actual->value; } // 배열(conditions 등)은 키 순서를 강요하지 않고 의미 비교, 스칼라는 strict 비교 if (is_array($value)) { $this->assertEquals( $value, $actual, sprintf("정책 '%s' 의 %s 필드가 선언과 일치해야 함", $key, $field), ); } else { $this->assertSame( $value, $actual, sprintf("정책 '%s' 의 %s 필드가 선언과 일치해야 함 (선언: %s / 실제: %s)", $key, $field, json_encode($value), json_encode($actual), ), ); } } $this->assertSame('core', $policy->source_type->value); $this->assertSame('core', $policy->source_identifier); } public static function coreDeclarationProvider(): array { return [ 'signup_before_submit' => [ 'core.auth.signup_before_submit', [ 'scope' => 'route', 'target' => 'api.auth.register', 'purpose' => 'signup', 'grace_minutes' => 0, 'enabled' => false, 'applies_to' => 'self', 'fail_mode' => 'block', 'priority' => 110, 'conditions' => ['signup_stage' => 'before_submit', 'http_method' => ['POST']], ], ], 'signup_after_create' => [ 'core.auth.signup_after_create', [ 'scope' => 'hook', 'target' => 'core.auth.after_register', 'purpose' => 'signup', 'grace_minutes' => 0, 'enabled' => false, 'applies_to' => 'self', 'fail_mode' => 'block', 'priority' => 100, 'conditions' => ['signup_stage' => 'after_create'], ], ], 'password_reset' => [ 'core.auth.password_reset', [ 'scope' => 'hook', 'target' => 'core.auth.before_reset_password', 'purpose' => 'password_reset', 'grace_minutes' => 0, 'enabled' => false, 'applies_to' => 'both', 'fail_mode' => 'block', ], ], 'password_change' => [ 'core.profile.password_change', [ 'scope' => 'route', 'target' => 'api.me.password', 'purpose' => 'sensitive_action', 'grace_minutes' => 5, 'enabled' => true, 'applies_to' => 'self', 'fail_mode' => 'block', ], ], 'contact_change' => [ 'core.profile.contact_change', [ 'scope' => 'hook', 'target' => 'core.user.before_update', 'purpose' => 'sensitive_action', 'grace_minutes' => 5, 'enabled' => true, 'applies_to' => 'self', 'fail_mode' => 'block', 'conditions' => ['changed_fields' => ['email', 'phone', 'mobile']], ], ], 'account_withdraw' => [ 'core.account.withdraw', [ 'scope' => 'route', 'target' => 'api.me.destroy', 'purpose' => 'sensitive_action', 'grace_minutes' => 0, 'enabled' => true, 'applies_to' => 'self', 'fail_mode' => 'block', ], ], 'app_key_regenerate' => [ 'core.admin.app_key_regenerate', [ 'scope' => 'route', 'target' => 'api.admin.settings.regenerate-app-key', 'purpose' => 'sensitive_action', 'grace_minutes' => 0, 'enabled' => false, 'applies_to' => 'admin', 'fail_mode' => 'block', ], ], 'user_delete' => [ 'core.admin.user_delete', [ 'scope' => 'hook', 'target' => 'core.user.before_delete', 'purpose' => 'sensitive_action', 'grace_minutes' => 0, 'enabled' => false, 'applies_to' => 'admin', 'fail_mode' => 'block', ], ], 'extension_uninstall' => [ 'core.admin.extension_uninstall', [ 'scope' => 'route', 'target' => 'api.admin.{modules,plugins}.uninstall', 'purpose' => 'sensitive_action', 'grace_minutes' => 0, 'enabled' => false, 'applies_to' => 'admin', 'fail_mode' => 'block', ], ], ]; } }