코드에만 존재하던 REST API 계약(약 663 엔드포인트)을 코어/확장 책임별로
분리된 마크다운 레퍼런스로 전면 문서화한다. 674개 규모에서 수기 문서는
반드시 drift 하므로 "코드 추출 → 스캐폴딩 → 사람이 서술 채움 → 하네스가
커버리지 강제" 하이브리드로 구성했다.
추출 파이프라인 (app/Support/ApiDoc):
- ApiRouteInventory / FormRequestIntrospector / ApiEndpointProbe(실측 HTTP) /
ResponseSchemaInferrer / ApiDocScaffolder / ColumnCommentResolver /
ResourceFieldDescriber / ParameterDescriber
- api:docgen 커맨드(--scope/--seed/--check/--dry-run/--base-url/--user) +
응답/파라미터 in-place 백필 커맨드 2종(재생성 없이 TODO 셀만 치환, 멱등)
- ApiDocSampleSeeder 계약 + 코어/확장 시더로 실측용 완전 샘플 멱등 생성
문서화 (실측 기반, GET read-only 실호출):
- 코어 291엔드포인트 35파일(docs/backend/api) + 규정 docs/backend/api-documentation.md
- 확장: ecommerce(231)·board(80)·page(17)·hello_module(2)·pay_kginicis(22)·
gdpr(15)·ckeditor5(2)·marketing(2)·verification_kginicis(1)
- 표준 4구성(헤더·요청 파라미터·응답 필드·에러 표) + 엔드포인트 용도 서술
- 파라미터 용도·응답 필드 설명 셀 전수 채움(도메인 지식 수기)
하네스:
- audit 룰 api-doc-coverage — API 표면(라우트/컨트롤러/FormRequest/Resource)
변경 시 대응 문서 미동반이면 차단. 전 대상 문서 완비로 error 승격
- file-rules 리마인더(컨트롤러/라우트 편집 시), coverage.json, dev-dashboard 카드
- docs/backend/routing.md 확장 공개 API URL 스킴 정정(/api/modules|plugins/{id})
- /AGENTS.md/docs-index 동기
171 lines
6.4 KiB
PHP
171 lines
6.4 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Console;
|
|
|
|
use App\Console\Commands\ApiDocgenCommand;
|
|
use App\Models\User;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use ReflectionMethod;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* ApiDocgenCommand 확장 지원 로직 단위 테스트.
|
|
*
|
|
* 확장 컨트롤러 FQCN 에서 베이스 네임스페이스 추출, 도메인-파라미터 매칭
|
|
* (미바인딩 path param 폴백 판정)의 순수 로직을 HTTP 의존 없이 검증한다.
|
|
*/
|
|
class ApiDocgenExtensionDiscoveryTest extends TestCase
|
|
{
|
|
/**
|
|
* 커맨드의 private 메서드를 리플렉션으로 호출합니다.
|
|
*
|
|
* @param string $method 메서드명
|
|
* @param array<int, mixed> $args 인자
|
|
* @return mixed 반환값
|
|
*/
|
|
private function invoke(string $method, array $args): mixed
|
|
{
|
|
$command = app(ApiDocgenCommand::class);
|
|
$ref = new ReflectionMethod($command, $method);
|
|
$ref->setAccessible(true);
|
|
|
|
return $ref->invokeArgs($command, $args);
|
|
}
|
|
|
|
#[Test]
|
|
public function 확장_컨트롤러_fqc_n에서_베이스_네임스페이스를_추출한다(): void
|
|
{
|
|
$base = $this->invoke('extensionBaseNamespace', [
|
|
'Modules\\Sirsoft\\Page\\Http\\Controllers\\Admin\\PageController',
|
|
]);
|
|
|
|
$this->assertSame('Modules\\Sirsoft\\Page', $base);
|
|
}
|
|
|
|
#[Test]
|
|
public function http_controllers_없이_controllers_규약을_쓰는_확장도_베이스를_추출한다(): void
|
|
{
|
|
// pay_kginicis 는 `\Http\Controllers\` 대신 `\Controllers\` 배치를 쓴다.
|
|
// 이 경우도 시더 발견을 위해 베이스 네임스페이스를 추출해야 한다.
|
|
$base = $this->invoke('extensionBaseNamespace', [
|
|
'Plugins\\Sirsoft\\PayKginicis\\Controllers\\PaymentSignatureController',
|
|
]);
|
|
|
|
$this->assertSame('Plugins\\Sirsoft\\PayKginicis', $base);
|
|
}
|
|
|
|
#[Test]
|
|
public function http_controllers_규약이_controllers_보다_우선한다(): void
|
|
{
|
|
// 두 세그먼트가 공존할 수 있는 경우 `\Http\Controllers\` 를 우선 판정한다.
|
|
$base = $this->invoke('extensionBaseNamespace', [
|
|
'Modules\\Sirsoft\\Board\\Http\\Controllers\\Admin\\BoardController',
|
|
]);
|
|
|
|
$this->assertSame('Modules\\Sirsoft\\Board', $base);
|
|
}
|
|
|
|
#[Test]
|
|
public function 클로저_또는_컨트롤러_규약_밖이면_null을_반환한다(): void
|
|
{
|
|
$this->assertNull($this->invoke('extensionBaseNamespace', [null]));
|
|
$this->assertNull($this->invoke('extensionBaseNamespace', ['SomeRandomClass']));
|
|
}
|
|
|
|
#[Test]
|
|
public function 파라미터명이_도메인_단수형과_일치하면_매칭이다(): void
|
|
{
|
|
// pages/{page} → 도메인 단수 리소스 폴백 대상
|
|
$this->assertTrue($this->invoke('paramMatchesDomain', ['page', 'pages']));
|
|
$this->assertTrue($this->invoke('paramMatchesDomain', ['pages', 'pages']));
|
|
}
|
|
|
|
#[Test]
|
|
public function 보조_문자열_파라미터는_도메인과_매칭되지_않는다(): void
|
|
{
|
|
// {slug}/{hash}/{versionId} 는 route key 가 달라 폴백 금지 (잘못된 404 회피)
|
|
$this->assertFalse($this->invoke('paramMatchesDomain', ['slug', 'pages']));
|
|
$this->assertFalse($this->invoke('paramMatchesDomain', ['hash', 'pages']));
|
|
$this->assertFalse($this->invoke('paramMatchesDomain', ['versionId', 'pages']));
|
|
}
|
|
|
|
#[Test]
|
|
public function 도메인_샘플의_path_params_맵으로_문자열_파라미터를_치환한다(): void
|
|
{
|
|
// board 처럼 route-model binding 없는 slug/id 문자열 param 을
|
|
// 도메인 대표 샘플의 path_params 맵(param 명 => 실제 값)으로 정확 일치 치환.
|
|
$route = [
|
|
'uri' => 'api/modules/sirsoft-board/boards/{slug}/posts/{id}',
|
|
'method' => 'GET',
|
|
'domain_group' => 'boards',
|
|
'path_params' => ['slug', 'id'],
|
|
'path_bindings' => [],
|
|
];
|
|
$sampleMap = [
|
|
'boards' => [
|
|
'model' => User::class,
|
|
'key' => 'id',
|
|
'value' => '1',
|
|
'path_params' => ['slug' => 'qna', 'id' => '123'],
|
|
],
|
|
];
|
|
|
|
$uri = $this->invoke('resolvePathParams', [$route, $sampleMap]);
|
|
|
|
// slug/id 치환 후 중괄호가 모두 사라진 GET 은 목록 대표 샘플 확보용 per_page 가 부착됨
|
|
$this->assertSame('api/modules/sirsoft-board/boards/qna/posts/123?per_page=25', $uri);
|
|
}
|
|
|
|
#[Test]
|
|
public function path_params에_없는_파라미터는_치환하지_않고_원본을_유지한다(): void
|
|
{
|
|
// path_params 맵에 없는 param(hash) 은 미치환 → URI 에 남아 프로브가 실측 제외.
|
|
$route = [
|
|
'uri' => 'api/modules/sirsoft-board/boards/{slug}/attachment/{hash}',
|
|
'method' => 'GET',
|
|
'domain_group' => 'boards',
|
|
'path_params' => ['slug', 'hash'],
|
|
'path_bindings' => [],
|
|
];
|
|
$sampleMap = [
|
|
'boards' => [
|
|
'model' => User::class,
|
|
'key' => 'id',
|
|
'value' => '1',
|
|
'path_params' => ['slug' => 'qna'],
|
|
],
|
|
];
|
|
|
|
$uri = $this->invoke('resolvePathParams', [$route, $sampleMap]);
|
|
|
|
// slug 는 치환되고 hash 는 미치환 → 중괄호가 남아 실측 대상에서 제외됨
|
|
$this->assertStringContainsString('boards/qna/attachment/{hash}', $uri);
|
|
}
|
|
|
|
#[Test]
|
|
public function path_params가_없는_기존_확장은_route_key_치환만_적용한다(): void
|
|
{
|
|
// page/gdpr 등 path_params 미제공 확장은 기존 동작 유지(회귀 방지):
|
|
// 문자열 param 은 미치환 → 원본 중괄호 유지.
|
|
$route = [
|
|
'uri' => 'api/modules/sirsoft-page/pages/{slug}',
|
|
'method' => 'GET',
|
|
'domain_group' => 'pages',
|
|
'path_params' => ['slug'],
|
|
'path_bindings' => [],
|
|
];
|
|
$sampleMap = [
|
|
'pages' => [
|
|
'model' => User::class,
|
|
'key' => 'slug',
|
|
'value' => 'sample',
|
|
],
|
|
];
|
|
|
|
$uri = $this->invoke('resolvePathParams', [$route, $sampleMap]);
|
|
|
|
// path_params 미제공 + paramMatchesDomain('slug','pages')=false → 미치환
|
|
$this->assertStringContainsString('pages/{slug}', $uri);
|
|
}
|
|
}
|