Files
Gnuboard7/plugins/_bundled/sirsoft-message_bizppurio/tests/Feature/Notification/NotificationLogLinkTest.php
T
chym1217 11f1d79540 feat(message_bizppurio): 코어 알림 발송 이력 화면에 문자·알림톡 결과 주입
발송 결과(성공/실패·사유·잔액부족·SMS 대체발송)를 코어 "알림 발송 이력" 화면의 각 행에
표시한다. 코어 앱·코어 테이블 무수정 — 연결 표식은 bizppurio_dispatches.notification_log_id
에만 두고, 발송 사이클의 코어 after_log_sent/after_log_failed 훅으로 그 로그 id 를 연결한다.

- 결과 조회는 파라미터 없는 GET /dispatch-results/recent 로 최근 결과 맵을 받아 row.id 매칭
 (목록 로드 순서에 무관 — 빈 배열 전송·타이밍으로 결과가 비던 문제 해소).
- 코어 이력 datagrid 에 결과 컬럼을 layout_extensions 로 주입하고, 행 토글에도 상세 결과를
 얹는다. 메일·사이트내알림 탭에선 컬럼(헤더 포함)을 숨긴다. 다크모드 배지 색 보강.
- 브라우저 검수용 데모 시더(4상태·엣지 케이스 21건) + 전 계층 테스트(백엔드·Vitest 렌더)
 + 시나리오 매니페스트 + API 레퍼런스 동반.
2026-07-28 12:36:26 +09:00

160 lines
6.4 KiB
PHP

<?php
namespace Plugins\Sirsoft\MessageBizppurio\Tests\Feature\Notification;
use App\Extension\HookManager;
use App\Services\NotificationLogService;
use Plugins\Sirsoft\MessageBizppurio\Listeners\LinkNotificationLogListener;
use Plugins\Sirsoft\MessageBizppurio\Models\BizppurioDispatch;
use Plugins\Sirsoft\MessageBizppurio\Services\DispatchLinkContext;
use Plugins\Sirsoft\MessageBizppurio\Tests\PluginTestCase;
/**
* 코어 알림 로그 ↔ 비즈뿌리오 dispatch 연결 통합 테스트 (A-2 연결고리).
*
* Hook 도메인 규정(실제 훅 체인 관찰, mock 금지)에 따라, 코어 NotificationLogService 를 실제로
* 호출해 core.notification_log.after_log_sent / after_log_failed 훅이 발화하고, 우리
* LinkNotificationLogListener 가 발송 사이클 refkey 로 dispatch 에 코어 로그 id 를 연결하는지
* 관찰한다(관찰 가능한 상태 변화 = dispatch.notification_log_id).
*
* 매칭은 복합키가 아니라 DispatchLinkContext 의 발송 사이클 refkey 직접 표식(A안)이다.
*/
class NotificationLogLinkTest extends PluginTestCase
{
protected function setUp(): void
{
parent::setUp();
// 테스트 환경에는 플러그인이 활성 등록되지 않으므로, A-2 연결 리스너를 HookManager 에
// 명시 등록해 실제 훅 체인(after_log_sent/after_log_failed)이 이 리스너로 흐르게 한다.
// getSubscribedHooks() 를 그대로 반영해 실제 발화 경로를 검증한다(리스너 직접 호출 아님).
foreach (LinkNotificationLogListener::getSubscribedHooks() as $hook => $config) {
HookManager::addAction(
$hook,
fn (...$args) => app(LinkNotificationLogListener::class)->{$config['method']}(...$args),
$config['priority'] ?? 10,
);
}
}
/**
* 발송 사이클을 흉내내 pending dispatch 를 만들고 refkey 를 컨텍스트에 남깁니다.
*
* 채널 드라이버 send() 의 dispatch 생성 + linkContext->remember() 와 동일한 상태를 만든다.
*
* @param string $channel 발송 채널
* @return string 부여한 refkey
*/
private function beginDispatchCycle(string $channel = 'sms'): string
{
$refkey = 'rk_'.uniqid();
BizppurioDispatch::create([
'refkey' => $refkey,
'channel' => $channel,
'to_number' => '01011112222',
'content' => '본문',
'notification_type' => 'welcome',
'status' => 'pending',
'source' => 'auto',
'sent_at' => now(),
]);
// 발송 사이클과 동일한 싱글턴 인스턴스에 refkey 를 남긴다.
app(DispatchLinkContext::class)->remember($refkey);
return $refkey;
}
public function test_발송_성공_로그가_dispatch에_연결된다(): void
{
$refkey = $this->beginDispatchCycle();
// 코어 로그 서비스로 실제 로그 생성 → after_log_sent 훅 발화 → 우리 리스너가 연결
$log = app(NotificationLogService::class)->logSent([
'channel' => 'sms',
'notification_type' => 'welcome',
'extension_type' => 'core',
'extension_identifier' => 'core',
'recipient_identifier' => '01011112222',
'recipient_name' => '홍길동',
'source' => 'notification',
'sent_at' => now(),
]);
$dispatch = BizppurioDispatch::query()->where('refkey', $refkey)->first();
$this->assertNotNull($dispatch);
$this->assertSame((int) $log->id, (int) $dispatch->notification_log_id);
}
public function test_발송_실패_로그도_dispatch에_연결된다(): void
{
$refkey = $this->beginDispatchCycle('alimtalk');
$log = app(NotificationLogService::class)->logFailed([
'channel' => 'alimtalk',
'notification_type' => 'welcome',
'extension_type' => 'core',
'extension_identifier' => 'core',
'recipient_identifier' => '01011112222',
'error_message' => 'boom',
'source' => 'notification',
'sent_at' => now(),
]);
$dispatch = BizppurioDispatch::query()->where('refkey', $refkey)->first();
$this->assertSame((int) $log->id, (int) $dispatch->notification_log_id);
}
public function test_비_비즈뿌리오_로그는_연결되지_않는다(): void
{
// remember() 를 부르지 않은 상태(mail/database 등 비-비즈뿌리오 발송 사이클)
$log = app(NotificationLogService::class)->logSent([
'channel' => 'mail',
'notification_type' => 'welcome',
'extension_type' => 'core',
'extension_identifier' => 'core',
'recipient_identifier' => 'a@b.com',
'source' => 'notification',
'sent_at' => now(),
]);
// 어떤 dispatch 도 이 로그에 연결되지 않아야 한다.
$linked = BizppurioDispatch::query()->where('notification_log_id', $log->id)->exists();
$this->assertFalse($linked);
}
public function test_한_refkey는_한_로그에만_연결된다_consume_후_비워짐(): void
{
$refkey = $this->beginDispatchCycle();
// 첫 로그 → 연결됨
$first = app(NotificationLogService::class)->logSent([
'channel' => 'sms',
'notification_type' => 'welcome',
'extension_type' => 'core',
'extension_identifier' => 'core',
'recipient_identifier' => '01011112222',
'source' => 'notification',
'sent_at' => now(),
]);
// 같은 사이클에서 refkey 를 다시 남기지 않은 채 둘째 로그 발생(다른 채널) → 연결 안 됨
$second = app(NotificationLogService::class)->logSent([
'channel' => 'database',
'notification_type' => 'welcome',
'extension_type' => 'core',
'extension_identifier' => 'core',
'recipient_identifier' => '01011112222',
'source' => 'notification',
'sent_at' => now(),
]);
$dispatch = BizppurioDispatch::query()->where('refkey', $refkey)->first();
$this->assertSame((int) $first->id, (int) $dispatch->notification_log_id);
$this->assertFalse(
BizppurioDispatch::query()->where('notification_log_id', $second->id)->exists()
);
}
}