Files
Gnuboard7/tests/Unit/Upgrades/ClearSeoPageCacheAfterRendererParityFixTest.php
HeuJung 0dfb45951b fix(core,basic): 검색 봇 화면 렌더러 패리티 결손 일괄 수정
일반 화면(React)과 봇 화면(PHP)은 같은 레이아웃을 각각 렌더하는데 봇
화면 쪽 지원 범위가 뒤처져, 확장이 교체한 본문이 통째로 빈 채 나갔다
(공개 이슈 ). 같은 결손 계열을 전수 조사해 함께 닫는다.

- 확장 주입 props 를 렌더 진입 시점에 해석해 자손 전체에 상속
 → 게시글·답글 원글·댓글·페이지·상품 설명 5개 지점 복구
- 사용자 작성 HTML 정화기 신설 — isHtml 판정에 따라 이스케이프/정화.
 미정화 상태에서 봇 경로로 저장형 스크립트가 실제 실행됐다
- 노드 키 패리티: conditions / type:iterator / responsive 매칭 규칙 /
 children 문자열 / text 우선순위 / $t:defer / 레이아웃 초기 상태
- 표현식: 단일 바인딩 판정 교체, ||·&& 를 값 반환(JS 시맨틱)으로,
 비교식이 raw 경로에서 null 이 되던 문제
- 데이터소스: 엔드포인트 표현식 전면 해석 + 레이아웃 8곳 선언 보강
- 봇 페이지 캐시 1회 전량 무효화 업그레이드 스텝

지원 범위 SSoT 표를 문서에 고정하고, 한쪽만 고치는 것을 막는 검사 룰과
레이아웃 전수 순회 테스트로 잠갔다.
2026-07-27 10:03:06 +09:00

160 lines
5.8 KiB
PHP

<?php
namespace Tests\Unit\Upgrades;
require_once __DIR__.'/../../../upgrades/data/7.0.6/migrations/02_ClearSeoPageCacheAfterRendererParityFix.php';
use App\Extension\Cache\CoreCacheDriver;
use App\Extension\UpgradeContext;
use App\Upgrades\Data\V7_0_6\Migrations\ClearSeoPageCacheAfterRendererParityFix;
use Tests\TestCase;
/**
* 7.0.6 업그레이드 스텝 — 봇 페이지 캐시 1회 무효화
*
* 7.0.6 이전 렌더러가 빈 본문으로 저장한 캐시가 남아 있으면, 렌더러를 고쳐도
* 캐시 수명 동안 검색 봇이 계속 빈 페이지를 받는다. 업그레이드 시 한 번 비운다.
*/
class ClearSeoPageCacheAfterRendererParityFixTest extends TestCase
{
private ClearSeoPageCacheAfterRendererParityFix $migration;
private CoreCacheDriver $cache;
/**
* 테스트 초기화 - 배열 캐시 스토어와 마이그레이션을 준비합니다.
*/
protected function setUp(): void
{
parent::setUp();
config()->set('cache.default', 'array');
$this->cache = new CoreCacheDriver('array');
$this->cache->flush();
$this->migration = new ClearSeoPageCacheAfterRendererParityFix;
}
/**
* 업그레이드 컨텍스트를 만듭니다.
*
* @return UpgradeContext 컨텍스트
*/
private function context(): UpgradeContext
{
return new UpgradeContext('7.0.5', '7.0.6', '7.0.6');
}
/**
* 인덱스에 등록된 캐시 항목과 인덱스 자체가 모두 삭제됩니다.
*
* @effects stale_page_cache_is_cleared_on_upgrade, cache_index_is_cleared_on_upgrade
*/
public function test_clears_indexed_page_cache_entries_and_index(): void
{
$this->cache->put('seo.page.abc', '<html>빈 본문</html>', 3600);
$this->cache->put('seo.page.def', '<html>빈 본문2</html>', 3600);
$this->cache->put('seo.cached_urls', [
['key' => 'seo.page.abc', 'url' => '/board/free/1'],
['key' => 'seo.page.def', 'url' => '/page/about'],
], 3600);
$this->migration->run($this->context());
$this->assertNull($this->cache->get('seo.page.abc'));
$this->assertNull($this->cache->get('seo.page.def'));
$this->assertNull($this->cache->get('seo.cached_urls'));
}
/**
* 인덱스가 없으면 아무 것도 하지 않습니다 (no-op).
*
* @effects invalidation_is_idempotent, unrelated_cache_namespaces_are_preserved
*/
public function test_no_index_is_noop(): void
{
$this->cache->put('unrelated.key', 'keep', 3600);
$this->migration->run($this->context());
$this->assertSame('keep', $this->cache->get('unrelated.key'));
}
/**
* 재실행해도 부작용이 없습니다 (멱등).
*
* @effects invalidation_is_idempotent
*/
public function test_is_idempotent(): void
{
$this->cache->put('seo.page.abc', '<html></html>', 3600);
$this->cache->put('seo.cached_urls', [['key' => 'seo.page.abc', 'url' => '/']], 3600);
$this->migration->run($this->context());
$this->migration->run($this->context());
$this->assertNull($this->cache->get('seo.page.abc'));
$this->assertNull($this->cache->get('seo.cached_urls'));
}
/**
* 인덱스 항목이 손상되어 있어도 나머지를 삭제하고 인덱스를 비웁니다.
*
* @effects stale_page_cache_is_cleared_on_upgrade, cache_index_is_cleared_on_upgrade
*/
public function test_malformed_index_entries_are_skipped(): void
{
$this->cache->put('seo.page.ok', '<html></html>', 3600);
$this->cache->put('seo.cached_urls', [
'not-an-array',
['url' => '/키없음'],
['key' => '', 'url' => '/빈키'],
['key' => 'seo.page.ok', 'url' => '/정상'],
], 3600);
$this->migration->run($this->context());
$this->assertNull($this->cache->get('seo.page.ok'));
$this->assertNull($this->cache->get('seo.cached_urls'));
}
/**
* 봇 페이지 캐시 외의 캐시는 건드리지 않습니다.
*
* @effects unrelated_cache_namespaces_are_preserved
*/
public function test_unrelated_cache_keys_are_preserved(): void
{
$this->cache->put('extension.middleware_index', ['a'], 3600);
$this->cache->put('seo.page.abc', '<html></html>', 3600);
$this->cache->put('seo.cached_urls', [['key' => 'seo.page.abc', 'url' => '/']], 3600);
$this->migration->run($this->context());
$this->assertSame(['a'], $this->cache->get('extension.middleware_index'));
}
/**
* 인덱스가 손상되어 다른 네임스페이스 키를 가리켜도 그 키는 지우지 않습니다.
*
* 삭제 대상을 봇 페이지 캐시 프리픽스(`seo.page.`)로 한정해, 오염된 인덱스가
* 무관한 캐시를 함께 날리는 사고를 막습니다.
*
* @effects unrelated_cache_namespaces_are_preserved, stale_page_cache_is_cleared_on_upgrade
*/
public function test_index_entries_outside_page_cache_namespace_are_not_forgotten(): void
{
$this->cache->put('extension.middleware_index', ['a'], 3600);
$this->cache->put('seo.page.abc', '<html></html>', 3600);
$this->cache->put('seo.cached_urls', [
['key' => 'seo.page.abc', 'url' => '/'],
// 오염된 엔트리 — 봇 페이지 캐시가 아닌 키를 가리킨다
['key' => 'extension.middleware_index', 'url' => '/'],
], 3600);
$this->migration->run($this->context());
$this->assertSame(['a'], $this->cache->get('extension.middleware_index'), '무관한 캐시가 삭제되었습니다.');
$this->assertNull($this->cache->get('seo.page.abc'), '봇 페이지 캐시는 삭제되어야 합니다.');
}
}