Files
Gnuboard7/tests/Unit/Extension/Helpers/IdentityPolicySyncHelperTest.php
T
2026-05-11 11:29:41 +09:00

225 lines
8.0 KiB
PHP

<?php
namespace Tests\Unit\Extension\Helpers;
use App\Extension\Helpers\IdentityPolicySyncHelper;
use App\Models\IdentityPolicy;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* IdentityPolicySyncHelper 테스트.
*
* 알림 시스템 NotificationSyncHelper 동형 — upsert + user_overrides 보존 + cleanupStale 을 검증.
*/
class IdentityPolicySyncHelperTest extends TestCase
{
use RefreshDatabase;
private IdentityPolicySyncHelper $helper;
protected function setUp(): void
{
parent::setUp();
$this->helper = $this->app->make(IdentityPolicySyncHelper::class);
}
public function test_sync_policy_creates_new_policy_on_first_run(): void
{
$policy = $this->helper->syncPolicy([
'key' => 'test.p1',
'scope' => 'route',
'target' => 'api.test.one',
'purpose' => 'sensitive_action',
'grace_minutes' => 0,
'enabled' => true,
'source_type' => 'core',
'source_identifier' => 'core',
]);
$this->assertSame('test.p1', $policy->key);
$this->assertSame('route', $policy->scope->value);
$this->assertTrue($policy->enabled);
}
public function test_sync_policy_updates_fields_not_in_user_overrides(): void
{
$this->helper->syncPolicy([
'key' => 'test.p2',
'scope' => 'route',
'target' => 'api.test.two',
'purpose' => 'sensitive_action',
'grace_minutes' => 0,
'enabled' => true,
'source_type' => 'core',
'source_identifier' => 'core',
]);
// 운영자가 enabled 를 수정했다고 가정 — user_overrides 에 기록
$policy = IdentityPolicy::where('key', 'test.p2')->first();
$policy->user_overrides = ['enabled'];
$policy->enabled = false;
$policy->save();
// 재시딩 — enabled 는 user_overrides 에 있으므로 보존, target 은 갱신
$this->helper->syncPolicy([
'key' => 'test.p2',
'scope' => 'route',
'target' => 'api.test.two_changed',
'purpose' => 'sensitive_action',
'grace_minutes' => 0,
'enabled' => true, // 여기서 true 로 재설정해도
'source_type' => 'core',
'source_identifier' => 'core',
]);
$policy->refresh();
$this->assertSame('api.test.two_changed', $policy->target, 'target 은 갱신되어야 함');
$this->assertFalse($policy->enabled, 'enabled 는 user_overrides 에 있어 보존되어야 함');
}
/**
* conditions 운영자 편집값 보존 (B안 리팩토링).
*
* 회원가입 단계 등 정책 조건은 운영자가 화면에서 수정 후 user_overrides 에 'conditions' 가 추가되며,
* 모듈 재sync 시 declared default 가 운영자 값을 덮어쓰지 않아야 한다.
*/
public function test_sync_policy_preserves_conditions_in_user_overrides(): void
{
$this->helper->syncPolicy([
'key' => 'test.signup.before_submit',
'scope' => 'route',
'target' => 'api.auth.register',
'purpose' => 'signup',
'conditions' => ['signup_stage' => 'before_submit'],
'source_type' => 'module',
'source_identifier' => 'sirsoft-board',
]);
$policy = IdentityPolicy::where('key', 'test.signup.before_submit')->first();
$policy->user_overrides = ['conditions'];
$policy->conditions = ['signup_stage' => 'after_create'];
$policy->save();
$this->helper->syncPolicy([
'key' => 'test.signup.before_submit',
'scope' => 'route',
'target' => 'api.auth.register',
'purpose' => 'signup',
'conditions' => ['signup_stage' => 'before_submit'],
'source_type' => 'module',
'source_identifier' => 'sirsoft-board',
]);
$policy->refresh();
$this->assertSame(
['signup_stage' => 'after_create'],
$policy->conditions,
'conditions 는 user_overrides 에 등록되어 있어 declared default 로 덮어써지면 안 됨',
);
}
/**
* trackable 5필드(enabled / grace_minutes / provider_id / fail_mode / conditions) 각각에 대해
* user_overrides 등록 시 syncPolicy 가 운영자 값을 보존하는지 매트릭스 검증.
*
* @dataProvider trackableFieldProvider
*/
public function test_sync_preserves_each_trackable_field_in_user_overrides(string $field, mixed $declared, mixed $override): void
{
$key = "test.matrix.{$field}";
$base = [
'key' => $key,
'scope' => 'route',
'target' => "api.test.{$field}",
'purpose' => 'sensitive_action',
'grace_minutes' => 0,
'enabled' => true,
'provider_id' => null,
'fail_mode' => 'block',
'conditions' => null,
'source_type' => 'module',
'source_identifier' => 'sirsoft-board',
];
$base[$field] = $declared;
$this->helper->syncPolicy($base);
// 운영자 변경 시뮬레이션
$policy = IdentityPolicy::where('key', $key)->first();
$policy->user_overrides = [$field];
$policy->{$field} = $override;
$policy->save();
// 재시딩 — declared 가 운영자 값을 덮어쓰면 안 됨
$this->helper->syncPolicy($base);
$policy->refresh();
$actual = $field === 'conditions' ? $policy->conditions : $policy->{$field};
// enum cast 된 분류 컬럼은 enum 객체이므로 ->value 비교
if ($actual instanceof \BackedEnum) {
$actual = $actual->value;
}
$this->assertSame(
$override,
$actual,
"trackable 필드 '{$field}' 가 user_overrides 에 등록되어 있어 declared default 로 덮어써지면 안 됨",
);
}
public static function trackableFieldProvider(): array
{
return [
'enabled' => ['enabled', true, false],
'grace_minutes' => ['grace_minutes', 0, 60],
'provider_id' => ['provider_id', null, 'g7:core.mail'],
'fail_mode' => ['fail_mode', 'block', 'log_only'],
'conditions' => ['conditions', null, ['signup_stage' => 'after_create']],
];
}
public function test_cleanup_stale_removes_policies_not_in_current_keys(): void
{
$this->helper->syncPolicy([
'key' => 'test.stale.a',
'scope' => 'route',
'target' => 'a',
'purpose' => 'sensitive_action',
'source_type' => 'module',
'source_identifier' => 'some-module',
]);
$this->helper->syncPolicy([
'key' => 'test.stale.b',
'scope' => 'route',
'target' => 'b',
'purpose' => 'sensitive_action',
'source_type' => 'module',
'source_identifier' => 'some-module',
]);
// b 만 현재 선언 — a 는 stale 로 삭제되어야 함
$removed = $this->helper->cleanupStalePolicies('module', 'some-module', ['test.stale.b']);
$this->assertSame(1, $removed);
$this->assertNull(IdentityPolicy::where('key', 'test.stale.a')->first());
$this->assertNotNull(IdentityPolicy::where('key', 'test.stale.b')->first());
}
public function test_cleanup_stale_preserves_other_sources(): void
{
$this->helper->syncPolicy([
'key' => 'test.admin.one',
'scope' => 'route',
'target' => 'a',
'purpose' => 'sensitive_action',
'source_type' => 'admin',
'source_identifier' => 'admin',
]);
// core source cleanup 은 admin source 정책에 영향 없어야 함
$this->helper->cleanupStalePolicies('core', 'core', []);
$this->assertNotNull(IdentityPolicy::where('key', 'test.admin.one')->first());
}
}