service = app(NotificationTemplateService::class); } /** * resolve()가 활성 템플릿을 반환하는지 확인 */ public function test_resolve_returns_active_template(): void { $definition = NotificationDefinition::create([ 'type' => 'test_resolve', 'hook_prefix' => 'core.test', 'extension_type' => 'core', 'extension_identifier' => 'core', 'name' => ['ko' => '테스트'], 'variables' => [], 'channels' => ['mail'], 'hooks' => [], 'is_active' => true, 'is_default' => true, ]); NotificationTemplate::create([ 'definition_id' => $definition->id, 'channel' => 'mail', 'subject' => ['ko' => '제목', 'en' => 'Subject'], 'body' => ['ko' => '본문', 'en' => 'Body'], 'is_active' => true, 'is_default' => true, ]); $this->service->invalidateCache('test_resolve', 'mail'); $result = $this->service->resolve('test_resolve', 'mail'); $this->assertNotNull($result); $this->assertEquals('mail', $result->channel); } /** * resolve()가 비활성 템플릿을 반환하지 않는지 확인 */ public function test_resolve_returns_null_for_inactive_template(): void { $definition = NotificationDefinition::create([ 'type' => 'test_inactive', 'hook_prefix' => 'core.test', 'extension_type' => 'core', 'extension_identifier' => 'core', 'name' => ['ko' => '테스트'], 'variables' => [], 'channels' => ['mail'], 'hooks' => [], 'is_active' => true, 'is_default' => true, ]); NotificationTemplate::create([ 'definition_id' => $definition->id, 'channel' => 'mail', 'subject' => ['ko' => '제목'], 'body' => ['ko' => '본문'], 'is_active' => false, 'is_default' => true, ]); $this->service->invalidateCache('test_inactive', 'mail'); $result = $this->service->resolve('test_inactive', 'mail'); $this->assertNull($result); } /** * updateTemplate()이 템플릿을 수정하는지 확인 */ public function test_update_template(): void { $definition = NotificationDefinition::create([ 'type' => 'test_update', 'hook_prefix' => 'core.test', 'extension_type' => 'core', 'extension_identifier' => 'core', 'name' => ['ko' => '테스트'], 'variables' => [], 'channels' => ['mail'], 'hooks' => [], 'is_active' => true, 'is_default' => true, ]); $template = NotificationTemplate::create([ 'definition_id' => $definition->id, 'channel' => 'mail', 'subject' => ['ko' => '원본 제목'], 'body' => ['ko' => '원본 본문'], 'is_active' => true, 'is_default' => true, ]); $updated = $this->service->updateTemplate($template, [ 'subject' => ['ko' => '수정된 제목'], 'body' => ['ko' => '수정된 본문'], ]); $this->assertEquals(['ko' => '수정된 제목'], $updated->subject); $this->assertEquals(['ko' => '수정된 본문'], $updated->body); } /** * toggleActive()가 활성 상태를 반전하는지 확인 */ public function test_toggle_active(): void { $definition = NotificationDefinition::create([ 'type' => 'test_toggle', 'hook_prefix' => 'core.test', 'extension_type' => 'core', 'extension_identifier' => 'core', 'name' => ['ko' => '테스트'], 'variables' => [], 'channels' => ['mail'], 'hooks' => [], 'is_active' => true, 'is_default' => true, ]); $template = NotificationTemplate::create([ 'definition_id' => $definition->id, 'channel' => 'mail', 'subject' => ['ko' => '제목'], 'body' => ['ko' => '본문'], 'is_active' => true, 'is_default' => true, ]); $result = $this->service->toggleActive($template); $this->assertFalse($result->is_active); } /** * getDefaultTemplateData()가 click_url과 recipients를 포함하는지 확인 (복원 누락 회귀) * * config/core.php 의 welcome/database 채널은 recipients 와 click_url='/mypage' 를 가진다. * 복원 데이터에 두 필드가 누락되면 관리자 수정값이 잔존하므로 추출 화이트리스트를 검증한다. */ public function test_get_default_template_data_includes_click_url_and_recipients(): void { $data = $this->service->getDefaultTemplateData('welcome', 'database'); $this->assertArrayHasKey('click_url', $data); $this->assertArrayHasKey('recipients', $data); $this->assertEquals('/mypage', $data['click_url']); $this->assertEquals([['type' => 'trigger_user']], $data['recipients']); } /** * config에 click_url이 없는 채널은 null로 복원되는지 확인 (정상값 비파괴) * * welcome/mail 채널은 recipients 만 있고 click_url 이 없으므로 null 이어야 한다. */ public function test_get_default_template_data_null_click_url_when_absent_in_config(): void { $data = $this->service->getDefaultTemplateData('welcome', 'mail'); $this->assertArrayHasKey('click_url', $data); $this->assertNull($data['click_url']); $this->assertEquals([['type' => 'trigger_user']], $data['recipients']); } /** * resetToDefault()가 관리자 수정한 click_url/recipients를 기본값으로 복원하는지 확인 (회귀) * * 관리자가 click_url/recipients 를 수정한 뒤 복원하면 config 기본값으로 돌아가야 한다. * 수정 전에는 subject/body 만 복원되고 두 필드는 수정본이 잔존하는 결함이 있었다. */ public function test_reset_to_default_restores_click_url_and_recipients(): void { // welcome 은 코어 알림 정의라 config/core.php 를 SSoT 로 부팅 시 이미 동기화되어 있다. // 이 테스트는 그 정의에 템플릿을 매달아 복원 동작을 보므로, 있으면 그대로 쓴다. $definition = NotificationDefinition::firstOrCreate( ['type' => 'welcome'], [ 'hook_prefix' => 'core.auth', 'extension_type' => 'core', 'extension_identifier' => 'core', 'name' => ['ko' => '회원가입 환영'], 'variables' => [], 'channels' => ['database'], 'hooks' => [], 'is_active' => true, 'is_default' => true, ] ); // 관리자가 click_url/recipients 를 임의 값으로 수정한 상태. // 코어 템플릿이 이미 동기화되어 있으면 그 행을 수정본으로 만든다 (실제 시나리오와 동일). $template = NotificationTemplate::updateOrCreate( ['definition_id' => $definition->id, 'channel' => 'database'], [ 'subject' => ['ko' => '수정된 제목'], 'body' => ['ko' => '수정된 본문'], 'click_url' => '/custom-edited-url', 'recipients' => [['type' => 'admin']], 'is_active' => true, 'is_default' => false, 'user_overrides' => ['click_url' => true, 'recipients' => true], ] ); $defaultData = $this->service->getDefaultTemplateData('welcome', 'database'); $restored = $this->service->resetToDefault($template, $defaultData); $this->assertEquals('/mypage', $restored->click_url); $this->assertEquals([['type' => 'trigger_user']], $restored->recipients); $this->assertNull($restored->user_overrides); $this->assertTrue($restored->is_default); } /** * getPreview()가 변수를 치환하는지 확인 */ public function test_get_preview_replaces_variables(): void { $result = $this->service->getPreview([ 'subject' => ['ko' => '{name}님 환영합니다'], 'body' => ['ko' => '

{app_name}입니다

'], 'locale' => 'ko', 'variables' => [ 'name' => '홍길동', 'app_name' => 'G7', ], ]); $this->assertEquals('홍길동님 환영합니다', $result['subject']); $this->assertEquals('

G7입니다

', $result['body']); } }