Files
Gnuboard7/tests/Feature/Api/Admin/Identity/AdminIdentityMessageDefinitionAdminCrudTest.php
T
2026-05-11 11:29:41 +09:00

274 lines
9.5 KiB
PHP

<?php
namespace Tests\Feature\Api\Admin\Identity;
use App\Models\IdentityMessageDefinition;
use App\Models\IdentityMessageTemplate;
use App\Models\IdentityPolicy;
use App\Models\Role;
use App\Models\User;
use Database\Seeders\IdentityMessageDefinitionSeeder;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* 관리자 — IDV 메시지 정의 admin CRUD (#297).
*
* 정책 매핑 정의 운영자 추가/삭제 + 시드 정의 보호 가드 검증.
*/
class AdminIdentityMessageDefinitionAdminCrudTest extends TestCase
{
use RefreshDatabase;
private User $admin;
private string $token;
protected function setUp(): void
{
parent::setUp();
$this->seed(RolePermissionSeeder::class);
$this->seed(IdentityMessageDefinitionSeeder::class);
$this->admin = User::factory()->create(['is_super' => true]);
$adminRole = Role::where('identifier', 'admin')->first();
if ($adminRole) {
$this->admin->roles()->attach($adminRole->id, [
'assigned_at' => now(),
'assigned_by' => null,
]);
}
$this->admin = $this->admin->fresh();
$this->token = $this->admin->createToken('test-token')->plainTextToken;
}
private function authRequest(): static
{
return $this->withHeaders([
'Authorization' => 'Bearer '.$this->token,
'Accept' => 'application/json',
]);
}
private function makeAdminPolicy(string $key = 'admin.test.policy'): IdentityPolicy
{
return IdentityPolicy::create([
'key' => $key,
'scope' => 'route',
'target' => 'api.test.route',
'purpose' => 'sensitive_action',
'grace_minutes' => 0,
'enabled' => true,
'priority' => 100,
'conditions' => [],
'applies_to' => 'both',
'fail_mode' => 'block',
'source_type' => 'admin',
'source_identifier' => 'admin',
]);
}
private function validPayload(string $policyKey = 'admin.test.policy'): array
{
return [
'provider_id' => 'g7:core.mail',
'scope_type' => IdentityMessageDefinition::SCOPE_POLICY,
'scope_value' => $policyKey,
'name' => ['ko' => '커스텀 정책 메시지', 'en' => 'Custom Policy Message'],
'description' => ['ko' => '운영자 정책', 'en' => 'Admin policy'],
'channels' => ['mail'],
'variables' => [['key' => 'code', 'description' => '인증 코드']],
'templates' => [
[
'channel' => 'mail',
'subject' => ['ko' => '인증 코드 안내', 'en' => 'Verification Code'],
'body' => ['ko' => '<p>코드: {code}</p>', 'en' => '<p>Code: {code}</p>'],
],
],
];
}
public function test_admin_creates_definition_for_admin_policy(): void
{
$this->makeAdminPolicy();
$response = $this->authRequest()
->postJson('/api/admin/identity/messages/definitions', $this->validPayload());
$response->assertStatus(201);
$response->assertJsonPath('success', true);
$response->assertJsonPath('data.scope_type', 'policy');
$response->assertJsonPath('data.scope_value', 'admin.test.policy');
$response->assertJsonPath('data.is_default', false);
$response->assertJsonPath('data.is_active', true);
$response->assertJsonPath('data.abilities.can_delete', true);
$this->assertDatabaseHas('identity_message_definitions', [
'provider_id' => 'g7:core.mail',
'scope_type' => 'policy',
'scope_value' => 'admin.test.policy',
'is_default' => false,
'extension_identifier' => 'admin',
]);
$definition = IdentityMessageDefinition::where('scope_value', 'admin.test.policy')->firstOrFail();
$this->assertCount(1, $definition->templates);
$this->assertSame('mail', $definition->templates[0]->channel);
}
public function test_rejects_non_admin_policy_key(): void
{
// admin.test.policy 정책 미생성 → 매칭 실패
$response = $this->authRequest()
->postJson('/api/admin/identity/messages/definitions', $this->validPayload());
$response->assertStatus(422);
$response->assertJsonValidationErrors(['scope_value']);
}
public function test_rejects_core_source_policy_key(): void
{
// 코어 시드 정책(source_type='core')은 매칭 거부
IdentityPolicy::create([
'key' => 'core.seeded.policy',
'scope' => 'route',
'target' => 'api.core.route',
'purpose' => 'sensitive_action',
'grace_minutes' => 0,
'enabled' => true,
'priority' => 0,
'conditions' => [],
'applies_to' => 'both',
'fail_mode' => 'block',
'source_type' => 'core',
'source_identifier' => 'core',
]);
$payload = $this->validPayload('core.seeded.policy');
$response = $this->authRequest()
->postJson('/api/admin/identity/messages/definitions', $payload);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['scope_value']);
}
public function test_rejects_non_policy_scope_type(): void
{
$this->makeAdminPolicy();
foreach (['provider_default', 'purpose'] as $scopeType) {
$payload = $this->validPayload();
$payload['scope_type'] = $scopeType;
$response = $this->authRequest()
->postJson('/api/admin/identity/messages/definitions', $payload);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['scope_type']);
}
}
public function test_rejects_unknown_provider(): void
{
$this->makeAdminPolicy();
$payload = $this->validPayload();
$payload['provider_id'] = 'unknown:provider';
$response = $this->authRequest()
->postJson('/api/admin/identity/messages/definitions', $payload);
$response->assertStatus(422);
$response->assertJsonValidationErrors(['provider_id']);
}
public function test_rejects_duplicate_provider_scope_combination(): void
{
$this->makeAdminPolicy();
// 첫 정의 생성
$this->authRequest()
->postJson('/api/admin/identity/messages/definitions', $this->validPayload())
->assertStatus(201);
// 동일 (provider, scope_type, scope_value) 두 번째 생성 → 422
$response = $this->authRequest()
->postJson('/api/admin/identity/messages/definitions', $this->validPayload());
$response->assertStatus(422);
$response->assertJsonValidationErrors(['scope_value']);
}
public function test_admin_deletes_admin_added_definition(): void
{
$this->makeAdminPolicy();
$this->authRequest()
->postJson('/api/admin/identity/messages/definitions', $this->validPayload())
->assertStatus(201);
$definition = IdentityMessageDefinition::where('scope_value', 'admin.test.policy')->firstOrFail();
$templateId = $definition->templates[0]->id;
$response = $this->authRequest()
->deleteJson('/api/admin/identity/messages/definitions/'.$definition->id);
$response->assertStatus(200);
$response->assertJsonPath('success', true);
$this->assertDatabaseMissing('identity_message_definitions', ['id' => $definition->id]);
// FK cascadeOnDelete 로 자식 templates 도 함께 삭제되어야 함
$this->assertDatabaseMissing('identity_message_templates', ['id' => $templateId]);
}
public function test_rejects_deletion_of_seeded_definition(): void
{
// 시더에서 생성된 default 정의 (provider_default scope) 삭제 시도
$seededDefinition = IdentityMessageDefinition::where('is_default', true)->firstOrFail();
$response = $this->authRequest()
->deleteJson('/api/admin/identity/messages/definitions/'.$seededDefinition->id);
$response->assertStatus(403);
$this->assertDatabaseHas('identity_message_definitions', ['id' => $seededDefinition->id]);
}
public function test_resource_can_delete_is_false_for_seeded_definitions(): void
{
$response = $this->authRequest()
->getJson('/api/admin/identity/messages/definitions');
$response->assertStatus(200);
$rows = $response->json('data.data');
$this->assertNotEmpty($rows);
foreach ($rows as $row) {
if ($row['is_default'] === true) {
$this->assertSame(false, $row['abilities']['can_delete'] ?? null,
"Seeded definition {$row['provider_id']}/{$row['scope_value']} should not be deletable");
}
}
}
public function test_unauthorized_user_cannot_create(): void
{
$this->makeAdminPolicy();
$response = $this->withHeaders(['Accept' => 'application/json'])
->postJson('/api/admin/identity/messages/definitions', $this->validPayload());
$response->assertStatus(401);
}
public function test_returns_404_for_unknown_definition_destroy(): void
{
$response = $this->authRequest()
->deleteJson('/api/admin/identity/messages/definitions/999999');
$response->assertStatus(404);
}
}