이슈 본연: API 레퍼런스의 미채움 마커 5종(실측 제외/TODO/필드 없음/대표 에러 없음)
1,194건을 코드에서 읽어 전수 채우고(0건), api:docgen 재생성이 사람이 채운 내용을
손실·열화시키던 멱등성 결함 4종(CRLF, 표 파이프 이스케이프, 에러 서술 보존, 중복
라우트명 키)을 근본 수정. --check 를 실측 제외와 드리프트가 구분되도록 재정의.
ParameterDescriber/ResourceFieldDescriber 에 leaf 폴백·SEO 스코프·공통 사전 확장.
파생 결함(문서 실측 중 발견): ResponseHelper 기본 메시지 키 6종이 존재하지 않는
messages.* 를 가리켜 응답 message 가 번역문 대신 키 문자열로 노출되던 문제를 common.*
으로 정정하고, 코어 7 + gdpr/marketing/pay_kginicis/verification/ecommerce 확장의
호출부와 누락 lang 키(게시판 10키 + 이커머스 category_images 4키 포함)를 ko/en/ja
전수 정정. 중복 그룹 키 2건은 기존 키로 호출부 통합.
보안: GET /api/identity/challenges/{id} 는 권한 가드 없는 공개 폴링 엔드포인트인데
Service::getStatus 가 attempts/max_attempts 를 응답에 담아 남은 시도 횟수를 추론할 수
있었다. 두 필드를 제거하고, 챌린지 화면(admin_basic/basic)의 서버 attempts 참조를
제거(남은 횟수 UI 는 query fallback + verify 실패 시 로컬 증가로 유지).
부수 정리(변경셋에 들어온 board 컨트롤러의 audit 사전 결함): FormRequest 4개 신설로
base Request 주입 제거, CommentController 의 Board 직접 호출을 BoardService 위임으로
전환, PHPDoc @return 보강. board 스위트에서 발견한 stale test 3건(seed SSoT 불일치,
admin 계정 user 역할 누락)도 같은 세션에서 정정.
버전 정렬(공개 release 기준): 코어 7.0.3→7.0.4, sirsoft-board 1.0.1→1.0.2,
verification_kginicis 1.0.0→1.0.1, admin_basic 1.0.1→1.0.4.
308 lines
14 KiB
PHP
308 lines
14 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Support\ApiDoc;
|
|
|
|
use App\Support\ApiDoc\ResourceFieldDescriber;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* 리소스 계약 필드 설명기 단위 테스트.
|
|
*
|
|
* 컬럼 주석이 없는 accessor/computed 필드(status_label, is_owner, *_at 등)의
|
|
* 설명이 코드에서 확인된 의미대로 채워지는지 검증한다.
|
|
*/
|
|
class ResourceFieldDescriberTest extends TestCase
|
|
{
|
|
#[Test]
|
|
public function 표준_메타_필드를_정확_사전으로_설명한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
$this->assertStringContainsString('소유자', $describer->describe('is_owner', 'boolean'));
|
|
$this->assertStringContainsString('작업 불리언 맵', $describer->describe('abilities', 'object'));
|
|
$this->assertStringContainsString('Enum label()', $describer->describe('status_label', 'string'));
|
|
$this->assertStringContainsString('관리자', $describer->describe('is_admin', 'boolean'));
|
|
}
|
|
|
|
#[Test]
|
|
public function 타임스탬프_접미_패턴을_설명한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
$this->assertSame('생성 일시', $describer->describe('created_at', 'string'));
|
|
$this->assertSame('최종 수정 일시', $describer->describe('updated_at', 'string'));
|
|
// 사전에 없는 *_at 는 패턴으로 유추
|
|
$this->assertSame('email verified 일시', $describer->describe('email_verified_at', 'string'));
|
|
}
|
|
|
|
#[Test]
|
|
public function label_variant_접미_패턴을_설명한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
$desc = $describer->describe('priority_label', 'string');
|
|
$this->assertStringContainsString('priority', $desc);
|
|
$this->assertStringContainsString('라벨', $desc);
|
|
|
|
$variant = $describer->describe('priority_variant', 'string');
|
|
$this->assertStringContainsString('변형', $variant);
|
|
}
|
|
|
|
#[Test]
|
|
public function can_접두는_능력_불리언으로_설명한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
$desc = $describer->describe('can_update', 'boolean');
|
|
$this->assertStringContainsString('update', $desc);
|
|
$this->assertStringContainsString('수행 가능', $desc);
|
|
}
|
|
|
|
#[Test]
|
|
public function is_has_접두_불리언을_설명한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
$this->assertStringContainsString('여부', $describer->describe('is_default', 'boolean'));
|
|
$this->assertStringContainsString('여부', $describer->describe('has_children', 'boolean'));
|
|
// boolean 이 아니면 패턴 미적용 (오설명 방지)
|
|
$this->assertNull($describer->describe('is_default', 'string'));
|
|
}
|
|
|
|
#[Test]
|
|
public function count_접미_집계를_설명한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
$this->assertStringContainsString('개수', $describer->describe('user_count', 'integer'));
|
|
// integer/number 가 아니면 미적용
|
|
$this->assertNull($describer->describe('user_count', 'string'));
|
|
}
|
|
|
|
#[Test]
|
|
public function 알_수_없는_필드는_null_을_반환한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
// IDV/marketing 도메인 특이 필드는 제외 (사전/패턴에 없어야 — TODO 로 폴백)
|
|
$this->assertNull($describer->describe('purpose', 'string'));
|
|
$this->assertNull($describer->describe('channels', 'array'));
|
|
$this->assertNull($describer->describe('render_hint', 'string'));
|
|
// type/code/category 는 값 집합이 도메인마다 달라 제외 (역할은 있으나 도메인 종속)
|
|
$this->assertNull($describer->describe('code', 'string'));
|
|
$this->assertNull($describer->describe('type', 'string'));
|
|
$this->assertNull($describer->describe('category', 'string'));
|
|
}
|
|
|
|
#[Test]
|
|
public function 쓰기_응답_공통_관계_실행이력_필드를_설명한다(): void
|
|
{
|
|
// 쓰기 메서드 실측 도입으로 응답에 자주 등장하는 공통 관계/실행이력/토큰 필드는
|
|
// 도메인 무관하게 역할이 고정되므로 설명한다(TODO 방치 금지).
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
$this->assertStringContainsString('역할', $describer->describe('roles', 'array'));
|
|
$this->assertStringContainsString('사용자', $describer->describe('user', 'object'));
|
|
$this->assertStringContainsString('토큰', $describer->describe('token', 'string'));
|
|
$this->assertStringContainsString('실행', $describer->describe('duration', 'integer'));
|
|
$this->assertStringContainsString('종료 코드', $describer->describe('exit_code', 'integer'));
|
|
// status 는 문자열/정수일 때 일반 설명(도메인 라벨은 status_label 참조)
|
|
$this->assertStringContainsString('상태 값', $describer->describe('status', 'string'));
|
|
}
|
|
|
|
#[Test]
|
|
public function 관계_연관_객체_필드를_설명한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
$this->assertStringContainsString('생성자', $describer->describe('creator', 'object'));
|
|
$this->assertStringContainsString('하위', $describer->describe('children', 'array'));
|
|
$this->assertStringContainsString('상위', $describer->describe('parent', 'object'));
|
|
$this->assertStringContainsString('권한', $describer->describe('permissions', 'array'));
|
|
$this->assertStringContainsString('수신자', $describer->describe('recipient', 'object'));
|
|
$this->assertStringContainsString('발신자', $describer->describe('sender', 'object'));
|
|
$this->assertStringContainsString('작성자', $describer->describe('author', 'object'));
|
|
$this->assertStringContainsString('행위', $describer->describe('actor_name', 'string'));
|
|
}
|
|
|
|
#[Test]
|
|
public function 공통_콘텐츠_표시_필드를_설명한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
// 도메인 무관하게 역할이 고정된 공통 콘텐츠/표시 필드 (요청 파라미터 대응물)
|
|
$this->assertStringContainsString('이름', $describer->describe('name', 'object'));
|
|
$this->assertStringContainsString('제목', $describer->describe('title', 'string'));
|
|
$this->assertStringContainsString('본문', $describer->describe('content', 'string'));
|
|
$this->assertStringContainsString('설명', $describer->describe('description', 'object'));
|
|
$this->assertStringContainsString('slug', $describer->describe('slug', 'string'));
|
|
$this->assertStringContainsString('라벨', $describer->describe('label', 'string'));
|
|
$this->assertStringContainsString('아이콘', $describer->describe('icon', 'string'));
|
|
$this->assertStringContainsString('썸네일', $describer->describe('thumbnail', 'string'));
|
|
$this->assertStringContainsString('IP', $describer->describe('ip_address', 'string'));
|
|
}
|
|
|
|
#[Test]
|
|
public function sort_order_는_타입으로_분기한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
// 정수: 표시 정렬 순서 값
|
|
$this->assertStringContainsString('정렬 순서', $describer->describe('sort_order', 'integer'));
|
|
// 문자열: 정렬 방향일 수 있어 도메인 특이 → null (오설명 방지)
|
|
$this->assertNull($describer->describe('sort_order', 'string'));
|
|
}
|
|
|
|
#[Test]
|
|
public function 시스템_집계_필드를_설명한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
$this->assertStringContainsString('전체', $describer->describe('total', 'integer'));
|
|
$this->assertStringContainsString('사용자', $describer->describe('total_users', 'object'));
|
|
$this->assertStringContainsString('상대 시각', $describer->describe('time', 'string'));
|
|
$this->assertStringContainsString('서버', $describer->describe('server_time', 'string'));
|
|
$this->assertStringContainsString('순번', $describer->describe('number', 'integer'));
|
|
}
|
|
|
|
#[Test]
|
|
public function 확장_버전_로케일_필드를_설명한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
$this->assertStringContainsString('타입', $describer->describe('extension_type', 'string'));
|
|
$this->assertStringContainsString('식별자', $describer->describe('extension_identifier', 'string'));
|
|
$this->assertStringContainsString('이름', $describer->describe('extension_name', 'string'));
|
|
$this->assertStringContainsString('GitHub', $describer->describe('github_url', 'string'));
|
|
$this->assertStringContainsString('변경 이력', $describer->describe('changelog', 'string'));
|
|
$this->assertStringContainsString('코어', $describer->describe('current_core_version', 'string'));
|
|
$this->assertStringContainsString('로케일', $describer->describe('locales', 'array'));
|
|
$this->assertStringContainsString('표시명', $describer->describe('locale_names', 'object'));
|
|
$this->assertStringContainsString('차단', $describer->describe('install_blocked_reason', 'string'));
|
|
}
|
|
|
|
#[Test]
|
|
public function raw_접미_패턴은_원본_값으로_설명한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
$nameRaw = $describer->describe('name_raw', 'string');
|
|
$this->assertStringContainsString('name', $nameRaw);
|
|
$this->assertStringContainsString('원본', $nameRaw);
|
|
|
|
$descRaw = $describer->describe('description_raw', 'string');
|
|
$this->assertStringContainsString('description', $descRaw);
|
|
$this->assertStringContainsString('원본', $descRaw);
|
|
}
|
|
|
|
#[Test]
|
|
public function formatted_접미는_표시용_포맷_문자열로_설명한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
// 근거: size_formatted accessor, formatCurrencyPrice/formatFileSize/
|
|
// formatCreatedAtFormat — 통화·용량·일시 포맷 파생
|
|
$created = $describer->describe('created_at_formatted', 'string');
|
|
$this->assertStringContainsString('created_at', $created);
|
|
$this->assertStringContainsString('포맷', $created);
|
|
|
|
$price = $describer->describe('selling_price_formatted', 'string');
|
|
$this->assertStringContainsString('selling_price', $price);
|
|
$this->assertStringContainsString('포맷', $price);
|
|
}
|
|
|
|
#[Test]
|
|
public function localized_접두_접미는_로케일_해석_값으로_설명한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
// 근거: getLocalizedName() / getLocalizedOptionName()
|
|
$localizedName = $describer->describe('localized_name', 'string');
|
|
$this->assertStringContainsString('name', $localizedName);
|
|
$this->assertStringContainsString('로케일', $localizedName);
|
|
|
|
$optionLocalized = $describer->describe('option_name_localized', 'string');
|
|
$this->assertStringContainsString('option name', $optionLocalized);
|
|
$this->assertStringContainsString('로케일', $optionLocalized);
|
|
}
|
|
|
|
#[Test]
|
|
public function url_접미는_리소스_url로_설명한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
// 근거: thumbnail_url => download_url / getThumbnailUrl()
|
|
$thumb = $describer->describe('thumbnail_url', 'string');
|
|
$this->assertStringContainsString('thumbnail', $thumb);
|
|
$this->assertStringContainsString('URL', $thumb);
|
|
}
|
|
|
|
#[Test]
|
|
public function id_접미는_참조_식별자로_설명한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
// 응답의 *_id 는 예외 없이 연관 리소스 참조 식별자다 (parent_id/user_id 등)
|
|
$parent = $describer->describe('parent_id', 'integer');
|
|
$this->assertStringContainsString('parent', $parent);
|
|
$this->assertStringContainsString('식별자', $parent);
|
|
|
|
$ids = $describer->describe('permission_ids', 'array');
|
|
$this->assertStringContainsString('permission', $ids);
|
|
$this->assertStringContainsString('배열', $ids);
|
|
|
|
// 예외: login_id 는 참조 식별자가 아니라 로그인 계정 아이디(문자열) → TODO 유지
|
|
$this->assertNull($describer->describe('user_login_id', 'string'));
|
|
}
|
|
|
|
#[Test]
|
|
public function depth_는_계층_트리_깊이로_설명한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
// 근거: CommentResource/CategoryResource 의 depth (children/parent 트리)
|
|
$depth = $describer->describe('depth', 'integer');
|
|
$this->assertStringContainsString('깊이', $depth);
|
|
// 정수가 아니면 미적용 (오설명 방지)
|
|
$this->assertNull($describer->describe('depth', 'string'));
|
|
}
|
|
|
|
#[Test]
|
|
public function 중첩_응답_필드를_leaf_세그먼트로_설명한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
// 중첩 응답 필드(settings.general.site_url 등)는 leaf 가 의미를 결정한다.
|
|
$this->assertStringContainsString('생성 일시', $describer->describe('meta.created_at', 'string'));
|
|
$this->assertStringContainsString('개수', $describer->describe('stats.total', 'integer'));
|
|
|
|
// 회귀: 전체명 접미 매칭이 부모 세그먼트를 설명문에 흘리던 문제
|
|
$userId = $describer->describe('order.user_id', 'integer');
|
|
$this->assertStringNotContainsString('order.', $userId);
|
|
$this->assertStringContainsString('식별자', $userId);
|
|
}
|
|
|
|
#[Test]
|
|
public function 로케일_접미_응답_필드를_부모_설명_기준으로_설명한다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
// 다국어 필드(name.ko)는 부모 필드의 로케일별 값이다.
|
|
$ko = $describer->describe('name.ko', 'string');
|
|
$this->assertStringContainsString('이름', $ko);
|
|
$this->assertStringContainsString('ko', $ko);
|
|
|
|
// 부모가 도메인 특이면 로케일 접미도 TODO 로 남는다.
|
|
$this->assertNull($describer->describe('refund_priority.ko', 'string'));
|
|
}
|
|
|
|
#[Test]
|
|
public function 도메인_특이_중첩_응답_필드는_null_로_남는다(): void
|
|
{
|
|
$describer = new ResourceFieldDescriber;
|
|
|
|
$this->assertNull($describer->describe('order.refund_priority', 'string'));
|
|
}
|
|
}
|