Files
Gnuboard7/tests/Feature/Identity/CoreIdentityPolicyDeclarationTest.php
2026-05-11 11:29:41 +09:00

196 lines
7.0 KiB
PHP

<?php
namespace Tests\Feature\Identity;
use App\Models\IdentityPolicy;
use Database\Seeders\IdentityPolicySeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* 코어 IDV 정책 선언 형상 회귀 (D1).
*
* `IdentityPolicySeeder` 가 `config/core.php` 의 9개 정책을 DB 로 동기화한 결과가
* 선언과 정확히 일치하는지 검증한다. 회귀 시 — 예: purpose 오타, applies_to 변경,
* grace_minutes 변경, conditions 누락 — 즉시 fail.
*/
class CoreIdentityPolicyDeclarationTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->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',
],
],
];
}
}