Files
Gnuboard7/tests/Feature/Api/Admin/Identity/AdminIdentityMessageControllerTest.php
HeuJung e7fa1b5535 feat(message_bizppurio): 알림 템플릿 편집 통합·검수 의견·관리탭 개선 및 언어팩 번역 보존 결함군 수정
비즈뿌리오 알림톡 UX 개편 후속:
- 코어/게시판/이커머스 알림 템플릿 [편집] 모달에 확장 채널 편집 영역
 (extension_point 2종 + hidden_template_editor 채널 메타)을 신설하고,
 알림톡 템플릿·문자(SMS)·수신자 규칙을 한 창에서 통합 저장하도록 개정
 (행 하단은 상태 요약 전용, 승인 여부 2단 배지)
- 검수 신청에 검수자 전달 의견(comment) 동봉 — kapi request 로만 전달,
 행에는 저장하지 않음 (FormRequest 신설 + API 문서 백필)
- 관리 탭 모바일 카드뷰·도구줄 코어 관례 통일, 사이트맵 알림 정의
 다국어 라벨 결측 수정 + 코어 정의 라벨 패리티 테스트 신설

언어팩 번역 소실·보존 결함군 (보완 실측 → 커밋 전 점검에서 연쇄 확정,
전부 실패 테스트 선행 후 수정):
- [기본값 복원]이 활성 언어팩 번역(ja)을 영구 소실 → 복원 기본값에
 시딩과 같은 주입기(SSoT)로 팩 로케일 병합 (notification·identity)
- 코어 알림/본인인증 시더의 언어팩 주입 전면 불능 → 주입기가 연관/리스트
 양형태 수용 + identity 시더의 config 복합 키 보존
- 실패한 언어팩 업데이트가 active 팩을 installed 로 방치 → 상태 복원 확장
- 병합·제거의 사용자 수정 보존 판정 사문(연관 전제 isset) → dot-path 판정
 교정, 팩 주입 저장의 user_overrides 오염 차단(시딩 바인딩), 템플릿
 2모델 translatableTrackableFields 선언(beta.4 설계 누락), 같은 프로세스
 활성화 시 시더 번역 필터 stale(캡처 인스턴스 캐시) 교정
- 7.0.9 업그레이드 스텝 신설 — 활성 팩 seed 재동기화로 기설치본의 소실
 번역 자동 복구 (멱등·운영자 수정 보존·팩별 실패 격리)
2026-08-24 14:51:37 +09:00

250 lines
9.1 KiB
PHP

<?php
namespace Tests\Feature\Api\Admin\Identity;
use App\Models\IdentityMessageDefinition;
use App\Models\IdentityMessageTemplate;
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 메시지 정의/템플릿 컨트롤러 통합 테스트.
*
* golden path + 권한 경계 + user_overrides 기록 검증.
*/
class AdminIdentityMessageControllerTest 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',
]);
}
public function test_index_returns_paginated_definitions(): void
{
$response = $this->authRequest()->getJson('/api/admin/identity/messages/definitions');
$response->assertStatus(200)
->assertJsonStructure(['success', 'data' => ['data', 'pagination']]);
$this->assertGreaterThanOrEqual(5, count($response->json('data.data')));
}
public function test_index_filter_by_scope_type(): void
{
$response = $this->authRequest()->getJson('/api/admin/identity/messages/definitions?scope_type=purpose');
$response->assertStatus(200);
$items = $response->json('data.data');
foreach ($items as $item) {
$this->assertSame(IdentityMessageDefinition::SCOPE_PURPOSE, $item['scope_type']);
}
}
public function test_show_returns_definition_with_templates(): void
{
$definition = IdentityMessageDefinition::where('scope_value', 'signup')->firstOrFail();
$response = $this->authRequest()->getJson('/api/admin/identity/messages/definitions/'.$definition->id);
$response->assertStatus(200)
->assertJsonPath('data.id', $definition->id)
->assertJsonStructure(['data' => ['templates']]);
}
public function test_template_update_records_user_overrides(): void
{
$template = IdentityMessageTemplate::whereHas('definition', fn ($q) => $q
->where('scope_value', 'signup')
)->firstOrFail();
$response = $this->authRequest()->patchJson(
'/api/admin/identity/messages/templates/'.$template->id,
[
'subject' => ['ko' => '운영자 제목', 'en' => 'Custom Subject'],
'body' => ['ko' => '본문 {code}', 'en' => 'Body {code}'],
'is_active' => true,
],
);
$response->assertStatus(200);
$template->refresh();
$this->assertSame('운영자 제목', $template->subject['ko']);
// subject/body 는 translatableTrackableFields — 수정이 dot-path sub-key 단위로
// 기록되어야 시더 재실행·언어팩 병합이 수정된 로케일만 보존한다 (#597 교정)
$this->assertContains('subject.ko', $template->user_overrides ?? []);
$this->assertContains('subject.en', $template->user_overrides ?? []);
$this->assertFalse($template->is_default, '운영자 편집 후 is_default 가 false 로 전환되어야 합니다.');
}
public function test_template_update_requires_admin_permission(): void
{
$template = IdentityMessageTemplate::first();
$regularUser = User::factory()->create(['is_super' => false]);
$regularToken = $regularUser->createToken('regular')->plainTextToken;
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$regularToken,
'Accept' => 'application/json',
])->patchJson(
'/api/admin/identity/messages/templates/'.$template->id,
['body' => ['ko' => '본문', 'en' => 'Body']],
);
$response->assertStatus(403);
}
public function test_template_update_validates_body_required(): void
{
$template = IdentityMessageTemplate::first();
$response = $this->authRequest()->patchJson(
'/api/admin/identity/messages/templates/'.$template->id,
['subject' => ['ko' => '제목', 'en' => 'Subject']],
);
$response->assertStatus(422);
}
public function test_template_preview_renders_variables(): void
{
$template = IdentityMessageTemplate::whereHas('definition', fn ($q) => $q
->where('scope_value', 'signup')
)->firstOrFail();
$response = $this->authRequest()->postJson(
'/api/admin/identity/messages/templates/preview',
[
'template_id' => $template->id,
'data' => [
'code' => '999111',
'expire_minutes' => 15,
'app_name' => 'TestApp',
'site_url' => 'https://test.example',
],
'locale' => 'ko',
],
);
$response->assertStatus(200);
$body = $response->json('data.body');
$this->assertStringContainsString('999111', $body);
$this->assertStringContainsString('15', $body);
$this->assertStringContainsString('TestApp', $body);
$this->assertStringNotContainsString('{code}', $body);
}
public function test_template_reset_restores_defaults_and_clears_overrides(): void
{
$template = IdentityMessageTemplate::whereHas('definition', fn ($q) => $q
->where('scope_value', 'signup')
)->firstOrFail();
$template->update([
'subject' => ['ko' => '커스텀', 'en' => 'Custom'],
'body' => ['ko' => '커스텀 본문', 'en' => 'Custom body'],
]);
$template->refresh();
$this->assertNotEmpty($template->user_overrides);
$response = $this->authRequest()->postJson(
'/api/admin/identity/messages/templates/'.$template->id.'/reset',
);
$response->assertStatus(200);
$template->refresh();
$this->assertNotSame('커스텀', $template->subject['ko'], '시더 기본값으로 복원되어야 합니다.');
$this->assertSame([], $template->user_overrides ?? [], 'reset 후 user_overrides 비워져야 합니다.');
$this->assertTrue($template->is_default);
}
public function test_template_reset_restores_definition_is_default_flag(): void
{
// 시드 정의(is_default=true)의 템플릿을 운영자가 편집하면 definition.is_default 가 false 로 내려간다.
$template = IdentityMessageTemplate::whereHas('definition', fn ($q) => $q
->where('scope_value', 'signup')
)->firstOrFail();
$definitionId = $template->definition_id;
$this->assertTrue(
IdentityMessageDefinition::findOrFail($definitionId)->is_default,
'시드 정의는 초기 상태에서 is_default=true 여야 합니다.'
);
// 운영자 편집 → definition.is_default false 로 하강
$this->authRequest()->patchJson(
'/api/admin/identity/messages/templates/'.$template->id,
[
'subject' => ['ko' => '커스텀', 'en' => 'Custom'],
'body' => ['ko' => '커스텀 본문', 'en' => 'Custom body'],
],
)->assertStatus(200);
$this->assertFalse(
IdentityMessageDefinition::findOrFail($definitionId)->is_default,
'편집 후 definition.is_default 가 false 로 전환되어야 합니다 (결함 C 전제).'
);
// reset → definition.is_default 가 true 로 복원되어야 한다 ('기본' 배지/reset 버튼 노출 조건 정상화)
$this->authRequest()->postJson(
'/api/admin/identity/messages/templates/'.$template->id.'/reset',
)->assertStatus(200);
$this->assertTrue(
IdentityMessageDefinition::findOrFail($definitionId)->is_default,
'reset 후 시드 정의의 definition.is_default 가 true 로 복원되어야 합니다 (결함 C).'
);
}
public function test_definition_toggle_active_flips_state(): void
{
$definition = IdentityMessageDefinition::where('scope_value', 'signup')->firstOrFail();
$original = $definition->is_active;
$response = $this->authRequest()->patchJson(
'/api/admin/identity/messages/definitions/'.$definition->id.'/toggle-active',
);
$response->assertStatus(200);
$definition->refresh();
$this->assertSame(! $original, $definition->is_active);
}
}