Files
Gnuboard7/tests/Feature/Template/TemplateLanguageServingTest.php
HeuJung b4ed33aa44 feat(core): 정적 게시 수명주기 보완 — 빌드 파괴·권한·무결성
https://github.com/gnuboard/g7/issues/122 제보자의 추가 지적 2건(빌드가 게시본을
지움 / CLI 최초 게시 후 웹 재생성 불가)에 대응하고, 같은 근본 원인을 공유하는
결함을 전수조사로 함께 고친다.

- 빌드가 자기 산출물만 교체한다: 루트 vite `emptyOutDir: false`
 기본값 true 는 폴백 없는 코어 3번들과 배달된 immutable URL 의 게시본을 함께 지웠다
- 게시 트리 권한을 웹이 이어받는다: 병합 전 프리플라이트 + 부모 그룹 상속·g+w
 종전 소유권 정상화는 root 축만 덮어 비-root CLI 계정은 무방비였다
- 갱신 → 생성 → 완전성 확인을 한 묶음으로: 기록 바이트 대조, .old 원자 스왑,
 rename 일시 거부 재시도, 프론트 JSON 파싱 검증
- 실패를 억제하고 기록한다: 버전 한정 실패 마커 + 사유별 대시보드 알림
 + ext-static:status 점검 커맨드
- 파생: catch-all 제외 목록을 에셋 화이트리스트 합집합에서 파생(mjs·webp·otf 누락),
 번들 디스크 쓰기 fail-soft·빈 번들 503, 활성 디렉토리 prune 회피
2026-08-28 08:02:22 +09:00

228 lines
7.5 KiB
PHP

<?php
namespace Tests\Feature\Template;
use App\Contracts\Extension\CacheInterface;
use App\Enums\ExtensionStatus;
use App\Models\Template;
use App\Services\TemplateService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;
class TemplateLanguageServingTest extends TestCase
{
use RefreshDatabase;
private Template $activeTemplate;
private Template $inactiveTemplate;
private TemplateService $templateService;
protected function setUp(): void
{
parent::setUp();
$this->templateService = app(TemplateService::class);
// sirsoft-admin_basic 템플릿 설치 및 활성화
$this->templateService->installTemplate('sirsoft-admin_basic');
$this->activeTemplate = Template::where('identifier', 'sirsoft-admin_basic')->first();
$this->templateService->activateTemplate($this->activeTemplate->id);
// 비활성화된 템플릿 생성 (테스트용)
$this->inactiveTemplate = Template::create([
'identifier' => 'sirsoft-user_basic',
'vendor' => 'sirsoft',
'name' => ['ko' => 'User Basic', 'en' => 'User Basic'],
'version' => '1.0.0',
'description' => ['ko' => '기본 사용자 템플릿', 'en' => 'Basic user template'],
'type' => 'user',
'status' => ExtensionStatus::Inactive->value,
]);
// 캐시 초기화
Cache::flush();
}
/**
* 한국어 언어 파일 조회 성공 테스트
*/
public function test_can_retrieve_korean_language_file(): void
{
$response = $this->getJson('/api/templates/sirsoft-admin_basic/lang/ko.json');
$response->assertStatus(200)
->assertJsonStructure([
'common',
'auth',
]);
}
/**
* 영어 언어 파일 조회 성공 테스트
*/
public function test_can_retrieve_english_language_file(): void
{
$response = $this->getJson('/api/templates/sirsoft-admin_basic/lang/en.json');
$response->assertStatus(200)
->assertJsonStructure([
'common',
'auth',
]);
}
/**
* 지원하지 않는 로케일 요청 시 404 반환
*/
public function test_unsupported_locale_returns_404(): void
{
$response = $this->getJson('/api/templates/sirsoft-admin_basic/lang/fr.json');
$response->assertStatus(404)
->assertJson([
'success' => false,
])
->assertJsonFragment([
'message' => __('templates.errors.locale_not_supported', [
'template' => 'sirsoft-admin_basic',
'locale' => 'fr',
]),
]);
}
/**
* 존재하지 않는 템플릿 요청 시 404 반환
*/
public function test_nonexistent_template_returns_404(): void
{
$response = $this->getJson('/api/templates/nonexistent-template/lang/ko.json');
$response->assertStatus(404)
->assertJson([
'success' => false,
])
->assertJsonFragment([
'message' => __('templates.errors.not_found', ['template' => 'nonexistent-template']),
]);
}
/**
* 비활성화된 템플릿 요청 시 404 반환
*/
public function test_inactive_template_returns_404(): void
{
$response = $this->getJson('/api/templates/sirsoft-user_basic/lang/ko.json');
$response->assertStatus(404)
->assertJson([
'success' => false,
])
->assertJsonFragment([
'message' => __('templates.errors.not_found', ['template' => 'sirsoft-user_basic']),
]);
}
/**
* 언어 파일이 캐싱되는지 확인
*/
public function test_language_file_is_cached(): void
{
// `?v` 생략 시 현재 확장 캐시 버전으로 폴백하므로 (#588 — `.v0` 사각 키 방지)
// 버전을 시드해 결정적 키로 검증한다
Cache::put('g7:core:ext.cache_version', 1234);
$cache = app(CacheInterface::class);
$cacheKey = 'template.language.sirsoft-admin_basic.ko.v1234';
$cache->forget($cacheKey);
// 첫 번째 요청
$this->getJson('/api/templates/sirsoft-admin_basic/lang/ko.json');
// 캐시 확인
$cachedData = $cache->get($cacheKey);
$this->assertNotNull($cachedData);
$this->assertIsArray($cachedData);
$this->assertArrayHasKey('success', $cachedData);
$this->assertTrue($cachedData['success']);
$this->assertArrayHasKey('data', $cachedData);
}
/**
* Cache-Control 헤더가 설정되는지 확인
*/
public function test_cache_control_header_is_set(): void
{
// public max-age 는 프로덕션 정책 (dev 는 no-cache 환경 분기, #122 작업 C)
app()['env'] = 'production';
$response = $this->getJson('/api/templates/sirsoft-admin_basic/lang/ko.json');
$response->assertStatus(200);
// Cache-Control 헤더 존재 여부 및 max-age 값 확인
$this->assertTrue($response->headers->has('Cache-Control'));
$cacheControl = $response->headers->get('Cache-Control');
$this->assertStringContainsString('max-age=3600', $cacheControl);
$this->assertStringContainsString('public', $cacheControl);
}
/**
* lang If-None-Match 일치 시 304 (조건부 캐시 — #122 작업 C)
*/
public function test_language_returns_304_with_matching_etag(): void
{
app()['env'] = 'production';
$first = $this->getJson('/api/templates/sirsoft-admin_basic/lang/ko.json');
$first->assertStatus(200);
$etag = $first->headers->get('ETag');
$this->assertNotNull($etag);
$second = $this->getJson(
'/api/templates/sirsoft-admin_basic/lang/ko.json',
['If-None-Match' => $etag]
);
$second->assertStatus(304);
$this->assertSame('', $second->getContent());
}
/**
* 개발 환경에서 lang 은 no-cache (파일 수정 즉시 반영 — F10)
*
* @scenario publish_state=unpublished, artifact_integrity=intact, filesystem_writable=writable, environment=dev, trigger=manual_command, process_user=web
*/
public function test_language_no_cache_in_development(): void
{
app()['env'] = 'local';
$response = $this->getJson('/api/templates/sirsoft-admin_basic/lang/ko.json');
$response->assertStatus(200);
$cacheControl = (string) $response->headers->get('Cache-Control');
$this->assertStringContainsString('no-cache', $cacheControl);
$this->assertStringNotContainsString('max-age=3600', $cacheControl);
}
/**
* 잘못된 로케일 형식 요청 시 404 반환
*/
public function test_invalid_locale_format_returns_404(): void
{
// 3자리 로케일 - 라우트 매칭 실패로 404
$response = $this->getJson('/api/templates/sirsoft-admin_basic/lang/kor.json');
$response->assertStatus(404);
// 숫자 포함 로케일 - 라우트 매칭 실패로 404
$response = $this->getJson('/api/templates/sirsoft-admin_basic/lang/k1.json');
$response->assertStatus(404);
// 대문자 로케일 - 라우트 매칭 실패로 404
$response = $this->getJson('/api/templates/sirsoft-admin_basic/lang/KO.json');
$response->assertStatus(404);
}
}