토스페이먼츠 플러그인을 프로덕션 수준으로 완성한다 (계획서 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(룰 미구현)라 규정이 있어도 검출되지 않았고, 실제로 이번 신설 테스트가 그 사각에 빠졌다. 룰을 구현해 봉인한다.
124 lines
5.0 KiB
PHP
124 lines
5.0 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Support\ApiDoc;
|
|
|
|
use App\Support\ApiDoc\ApiRouteInventory;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* ApiRouteInventory 확장 web 라우트 수집 단위 테스트.
|
|
*
|
|
* PG 콜백·웹훅은 외부 시스템(PG사 서버/브라우저 리다이렉트)이 호출하는 machine-facing
|
|
* 엔드포인트라 API 레퍼런스 대상이다. 그러나 이들은 CSRF·세션 특성상 `api.php` 가 아니라
|
|
* `web.php` 에 등록되므로(`/plugins/{id}/...`, name `web.plugins.{id}.*`), `api/` prefix
|
|
* 만 수집하던 기존 규칙에서는 영구히 무문서 상태였다.
|
|
*
|
|
* 확장 소유가 확정되는 web 라우트(`{modules|plugins}/{vendor-id}/...`)를 수집 대상에
|
|
* 포함하는지 검증한다.
|
|
*/
|
|
class ApiRouteInventoryWebRouteTest extends TestCase
|
|
{
|
|
#[Test]
|
|
public function 플러그인_웹훅_web_라우트가_수집된다(): void
|
|
{
|
|
$routes = app(ApiRouteInventory::class)->collect('plugin:sirsoft-tosspayments');
|
|
|
|
$names = array_column($routes, 'name');
|
|
|
|
$this->assertContains('web.plugins.sirsoft-tosspayments.webhook.deposit', $names);
|
|
$this->assertContains('web.plugins.sirsoft-tosspayments.webhook.payment-status', $names);
|
|
}
|
|
|
|
#[Test]
|
|
public function 플러그인_결제_콜백_web_라우트가_수집된다(): void
|
|
{
|
|
$routes = app(ApiRouteInventory::class)->collect('plugin:sirsoft-tosspayments');
|
|
|
|
$names = array_column($routes, 'name');
|
|
|
|
$this->assertContains('web.plugins.sirsoft-tosspayments.payment.success', $names);
|
|
$this->assertContains('web.plugins.sirsoft-tosspayments.payment.fail', $names);
|
|
}
|
|
|
|
#[Test]
|
|
public function 수집된_web_라우트는_확장_소유와_uri_를_정확히_보존한다(): void
|
|
{
|
|
$routes = app(ApiRouteInventory::class)->collect('plugin:sirsoft-tosspayments');
|
|
|
|
$deposit = collect($routes)->firstWhere('name', 'web.plugins.sirsoft-tosspayments.webhook.deposit');
|
|
|
|
$this->assertNotNull($deposit);
|
|
$this->assertSame('POST', $deposit['method']);
|
|
$this->assertSame('/plugins/sirsoft-tosspayments/webhook/deposit', $deposit['uri']);
|
|
$this->assertSame('plugin', $deposit['owner']['type']);
|
|
$this->assertSame('sirsoft-tosspayments', $deposit['owner']['id']);
|
|
$this->assertSame(
|
|
'Plugins\\Sirsoft\\Tosspayments\\Controllers\\WebhookController',
|
|
$deposit['controller']
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function web_라우트도_도메인_그룹으로_분류된다(): void
|
|
{
|
|
$routes = app(ApiRouteInventory::class)->collect('plugin:sirsoft-tosspayments');
|
|
|
|
$byName = collect($routes)->keyBy('name');
|
|
|
|
// web.plugins.{id}. 를 걷어낸 첫 세그먼트가 도메인이다.
|
|
$this->assertSame('webhook', $byName['web.plugins.sirsoft-tosspayments.webhook.deposit']['domain_group']);
|
|
$this->assertSame('payment', $byName['web.plugins.sirsoft-tosspayments.payment.success']['domain_group']);
|
|
}
|
|
|
|
#[Test]
|
|
public function 코어_web_라우트는_수집되지_않는다(): void
|
|
{
|
|
// 확장 소유가 확정되지 않는 web 라우트(코어 화면/블레이드 등)는 API 문서 대상이 아니다.
|
|
$routes = app(ApiRouteInventory::class)->collect('core');
|
|
|
|
foreach ($routes as $route) {
|
|
$this->assertStringStartsWith(
|
|
'/api/',
|
|
$route['uri'],
|
|
"코어 범위에는 api/ 라우트만 수집되어야 한다: {$route['uri']}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[Test]
|
|
public function 확장의_관리자_화면_web_라우트는_수집되지_않는다(): void
|
|
{
|
|
// hello_module 의 admin CRUD 는 사람이 브라우저로 여는 화면(web.modules.{id}.admin.*)이라
|
|
// machine-facing 엔드포인트가 아니므로 API 레퍼런스 대상이 아니다.
|
|
$routes = app(ApiRouteInventory::class)->collect('module:gnuboard7-hello_module');
|
|
|
|
$names = array_column($routes, 'name');
|
|
|
|
$this->assertNotContains('web.modules.gnuboard7-hello_module.admin.memos.index', $names);
|
|
$this->assertNotContains('web.modules.gnuboard7-hello_module.admin.memos.store', $names);
|
|
|
|
foreach ($routes as $route) {
|
|
$this->assertStringNotContainsString(
|
|
'/admin/',
|
|
$route['uri'],
|
|
"확장 관리자 화면 web 라우트가 수집되면 안 된다: {$route['uri']}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[Test]
|
|
public function 타_플러그인_web_라우트는_범위에_섞이지_않는다(): void
|
|
{
|
|
$routes = app(ApiRouteInventory::class)->collect('plugin:sirsoft-tosspayments');
|
|
|
|
foreach ($routes as $route) {
|
|
$this->assertSame('sirsoft-tosspayments', $route['owner']['id']);
|
|
}
|
|
|
|
// KG 도 web 웹훅을 갖지만 토스 범위에는 포함되지 않는다.
|
|
$names = array_column($routes, 'name');
|
|
$this->assertNotContains('web.plugins.sirsoft-pay_kginicis.payment.vbank-notify', $names);
|
|
}
|
|
}
|