Files
Gnuboard7/tests/Unit/Support/ApiDoc/ApiRouteInventoryFallbackTest.php
HeuJung a9f85ff92b feat(tosspayments,ecommerce,core): 토스 주문서형 결제수단·가상계좌·에스크로 + API 문서 web 라우트 확장
토스페이먼츠 플러그인을 프로덕션 수준으로 완성한다 (계획서 S4).

주문서형 결제(9종 동적등록)
- 플러그인이 filter_available_payment_methods 로 결제수단을 주입하면 체크아웃에
 독립 항목으로 노출된다. 서버 전송 시에는 core_payment_method 로 번역해 보낸다 —
 코어 PaymentMethodEnum 은 toss_* 를 거부하므로 번역 없이는 주문 자체가 422 로 실패했다.
 번역 근거인 core_payment_method 가 설정 병합/스냅샷의 화이트리스트에서 탈락하던 것을
 provider-agnostic 하게 보존하도록 고쳤다 (토스 전용 분기 없음).

가상계좌·웹훅·에스크로
- 입금통보/결제상태 웹훅 2종 신설. secret 대조(hash_equals)·리플레이 멱등·CSRF 면제.
- 에스크로 3상태(off/on/buyer_choice). escrowProducts 를 가상계좌·계좌이체 양쪽
 SDK 페이로드에 싣는다 — 서버가 조립해도 SDK 로 전달되지 않으면 토스가 결제를 거부한다.
 E2E 가 SDK 경계를 직접 캡처해 이 계약의 나머지 절반을 잠근다.
- 관리자 설정 저장 시 입금기한·에스크로 값 범위를 서버에서 검증한다 (UI max 는 클라 힌트라
 API 직접 호출을 막지 못한다).

레이아웃 리스너 실행 순서
- KG 는 no-PG 리스트를 통짜 리터럴로 str_replace 하므로 토스가 먼저 append 하면 KG 의
 매치가 깨진다. HookManager 는 ksort 오름차순이므로 토스 priority 를 30 으로 두어
 "KG(20) 먼저" 를 플러그인 로드 순서와 무관한 불변식으로 고정했다.

코어 — API 문서 생성기가 확장 web 라우트도 수집
- PG 콜백·웹훅은 CSRF/세션 특성상 web.php 에 등록되지만 외부 시스템이 호출하는
 machine-facing 엔드포인트라 API 레퍼런스 대상이다. api/ prefix 만 수집하던 탓에
 영구 무문서였다. 확장 소유 web 라우트를 수집하되 관리자 화면(admin 컨텍스트)은 제외한다.
 코어 라우트 수집 결과는 291건으로 불변.

하네스 — 확장 테스트 베이스클래스 룰 신설
- coverage.json 의 test-extension-base-class 가 status:todo(룰 미구현)라 규정이 있어도
 검출되지 않았고, 실제로 이번 신설 테스트가 그 사각에 빠졌다. 룰을 구현해 봉인한다.
2026-08-06 12:46:48 +09:00

78 lines
3.4 KiB
PHP

<?php
namespace Tests\Unit\Support\ApiDoc;
use App\Support\ApiDoc\ApiRouteInventory;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
/**
* ApiRouteInventory 비활성/미설치 확장 폴백 수집 단위 테스트.
*
* route:list 는 활성 확장만 노출하므로, 명시 범위(module:{id}/plugin:{id})로
* 지정한 확장이 미설치여서 등록 라우트가 0건일 때 번들 라우트 파일(src/routes/api.php)을
* 프로바이더와 동일한 prefix/name 규약으로 로드해 수집하는지 검증한다.
*
* gnuboard7-hello_module 은 학습용 번들 샘플로 개발 DB 에 미설치 상태이므로,
* route:list 에는 나타나지 않지만 번들 라우트 파일 폴백으로는 수집되어야 한다.
*/
class ApiRouteInventoryFallbackTest extends TestCase
{
#[Test]
public function 미설치_모듈_범위는_번들_라우트_파일에서_폴백_수집한다(): void
{
$routes = app(ApiRouteInventory::class)->collect('module:gnuboard7-hello_module');
// api.php 의 공개 memos GET 2건(index/show)이 폴백으로 수집된다.
$this->assertNotEmpty($routes, '미설치 모듈의 번들 라우트가 폴백으로 수집되어야 한다');
$names = array_column($routes, 'name');
$this->assertContains('api.modules.gnuboard7-hello_module.memos.index', $names);
$this->assertContains('api.modules.gnuboard7-hello_module.memos.show', $names);
}
#[Test]
public function 폴백_수집_라우트는_프로바이더와_동일한_ur_l_규약을_따른다(): void
{
$routes = app(ApiRouteInventory::class)->collect('module:gnuboard7-hello_module');
$index = collect($routes)->firstWhere('name', 'api.modules.gnuboard7-hello_module.memos.index');
$this->assertNotNull($index);
$this->assertSame('GET', $index['method']);
$this->assertSame('/api/modules/gnuboard7-hello_module/memos', $index['uri']);
$this->assertSame('module', $index['owner']['type']);
$this->assertSame('gnuboard7-hello_module', $index['owner']['id']);
$this->assertSame('memos', $index['domain_group']);
$this->assertSame(
'Modules\\Gnuboard7\\HelloModule\\Http\\Controllers\\Api\\MemoController',
$index['controller']
);
}
#[Test]
public function web_admin_라우트는_ap_i_문서_대상에서_제외된다(): void
{
// 확장 web 라우트 중 PG 콜백·웹훅은 문서 대상이지만(machine-facing), hello_module 의
// admin CRUD 는 사람이 브라우저로 여는 관리자 화면(web.modules.{id}.admin.*)이라 제외된다.
// hello_module 의 web.php 는 admin 화면뿐이므로 결과적으로 api/ 라우트만 남는다.
$routes = app(ApiRouteInventory::class)->collect('module:gnuboard7-hello_module');
foreach ($routes as $route) {
$this->assertStringStartsWith('/api/', $route['uri']);
}
$names = array_column($routes, 'name');
$this->assertNotContains('api.modules.gnuboard7-hello_module.admin.memos.store', $names);
$this->assertNotContains('web.modules.gnuboard7-hello_module.admin.memos.store', $names);
}
#[Test]
public function 라우트_파일이_없는_범위는_빈_배열을_반환한다(): void
{
$routes = app(ApiRouteInventory::class)->collect('module:nonexistent-module-xyz');
$this->assertSame([], $routes);
}
}