공개 제보 https://github.com/gnuboard/g7/issues/122 대응 — 초기 부트스트랩 리소스(다국어 병합·컴포넌트 정의·라우트·확장 번들·템플릿 dist)를 캐시 버전 디렉토리(public/build/ext/{v}/)에 실파일로 게시해 웹서버가 rewrite 전에 직접 서빙한다. 부트 임계 경로의 PHP 왕복을 제거하고(실측 TTFB 131~144ms → 1~7ms), 미게시·부분게시·GC 직후에는 fetch·태그·번들 3계층이 종전 API 로 즉시 폴백한다. - 게시: 원자적 tmp→rename→manifest(존재=완료), 캐시 락 단일 실행, 인라인 GC (현재+직전 1개), incrementExtensionCacheVersion 단일 지점 terminating 트리거 + blade 자가 치유 + 설치기 태스크(best_effort) + 일일 cleanup 스케줄, sudo 업데이트 대비 소유권 정상화(normalizeOwnership)·prune/백업 제외 - 프론트(engine-v1.61.0): blade 주입 cache_version 1급 시드(이중 부트 로드 제거), fetchStaticFirst 즉시 폴백, ComponentRegistry 버전 키드 매니페스트, ModuleAssetLoader 번들 정적→레거시 폴백, asset-url-recovery staticToLegacy 역변환 - 폴백 API 품질: lang/components/routes ETag+304 + 환경 분기 Cache-Control, 열화 라우트 스냅샷 공개 캐시 금지(서버측 캐시 회피와 대칭), 게시 .htaccess mod_deflate + nginx gzip 스니펫(압축 전송량 회귀 방지) - SEO 정합: 봇 HTML 은 GC 대상 정적 URL 미사용(allowStatic:false), props $switch 봇측 해석 구현(engine-v1.56.0 패리티), 패리티 룰 expression-dialect 그룹 신설, 상주 allow 헤더 제거로 잠금 복원, _comment* 접두 주석 키 분류 - 검증: 전 수정 red→green 4단계, Playwright 라이브 21건, Chrome MCP 24축+M1~M3, 봇 curl 3축, 캐시 저장소(file/redis/database) 축 판정, 히스토리·공개이슈·커밋 이력 전수 재조사 반영 - 코어 7.0.10, engine-v1.61.0. kill-switch: G7_STATIC_CACHE=false
228 lines
7.4 KiB
PHP
228 lines
7.4 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, environment=dev, trigger=manual_command
|
|
*/
|
|
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);
|
|
}
|
|
}
|