비즈뿌리오 알림톡 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 재동기화로 기설치본의 소실 번역 자동 복구 (멱등·운영자 수정 보존·팩별 실패 격리)
250 lines
9.1 KiB
PHP
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);
|
|
}
|
|
}
|