Files
Gnuboard7/tests/Unit/Seo/SeoRendererStaticAssetBypassTest.php
T
HeuJung 5ba7a83597 feat(core,engine): 부트스트랩 리소스 정적 게시(bake) 및 폴백 체계 도입
공개 제보 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
2026-08-25 17:02:53 +09:00

79 lines
3.1 KiB
PHP

<?php
namespace Tests\Unit\Seo;
use App\Seo\SeoRenderer;
use App\Services\ExtensionStaticCacheService;
use App\Support\AssetUrl;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Tests\TestCase;
/**
* SEO 봇 HTML 의 템플릿 CSS URL 은 정적 게시(bake) 경로를 우회해야 한다 (#122).
*
* 봇 HTML 은 SeoCacheManager(`seo.page.*` — 키에 cache_version 미포함, TTL 수시간)에
* 캐시되는데, 정적 게시 디렉토리는 GC 가 현재+직전 1개만 보존한다. 캐시된 HTML 이
* GC 된 `/build/ext/{v}/…` 를 참조하면 봇 화면의 CSS 가 404 이고, SEO HTML 에는
* asset-url-recovery 파샬이 포함되지 않아 자가 복구 경로도 없다. 따라서 SEO 렌더는
* 항상 무버전 API URL(정적 게이트 우회 = 종전 동작)을 사용한다.
*/
class SeoRendererStaticAssetBypassTest extends TestCase
{
private const VERSION = 424242;
/** 테스트 전용 public 루트 (실 게시 트리 격리) */
private string $isolatedPublicPath;
protected function setUp(): void
{
parent::setUp();
// 실 게시 트리(public/build/ext) 오염 방지 (ExtensionStaticCacheServiceTest 와 동일 근거)
$this->isolatedPublicPath = storage_path('framework/testing/public-seobypass-'.getmypid());
File::ensureDirectoryExists($this->isolatedPublicPath);
$this->app->usePublicPath($this->isolatedPublicPath);
}
protected function tearDown(): void
{
File::deleteDirectory($this->isolatedPublicPath);
AssetUrl::resetStaticExtBaseMemo();
ExtensionStaticCacheService::resetPublishScheduleForTesting();
parent::tearDown();
}
/**
* 정적 게이트가 열린 상태에서도 SEO CSS URL 은 `/build/ext/` 를 참조하지 않는다.
*
* @effects seo_html_never_references_gc_target_static_path
*/
public function test_seo_css_url_은_정적_게시_경로를_우회한다(): void
{
Cache::put('g7:core:ext.cache_version', self::VERSION);
app()['env'] = 'production';
$dir = public_path('build/ext/'.self::VERSION);
File::ensureDirectoryExists($dir.'/templates/sirsoft-basic/assets/css');
File::put($dir.'/manifest.json', '{}');
File::put($dir.'/templates/sirsoft-basic/assets/css/components.css', 'x');
AssetUrl::resetStaticExtBaseMemo();
// 전제 고정 — 블레이드 경로(기본값)는 정적 게이트를 통과하는 상태다.
// 이 단언이 깨지면 아래 우회 단언은 게이트 미통과로 인한 공허한 통과가 된다.
$this->assertSame(
'/build/ext/'.self::VERSION.'/templates/sirsoft-basic/assets/css/components.css',
AssetUrl::templateAsset('sirsoft-basic', 'css/components.css')
);
$method = new \ReflectionMethod(SeoRenderer::class, 'getTemplateCssUrls');
$urls = $method->invoke(app(SeoRenderer::class), 'sirsoft-basic');
$this->assertNotEmpty($urls);
foreach ($urls as $url) {
$this->assertStringNotContainsString('/build/ext/', $url);
}
}
}