Files
HeuJung f32729e20e fix(core): SEO 봇 캐시·확장 번들 캐시의 정합 결함 5건 수정
KVE-2026-2191 대응에서 계획 안 세부를 자율 결정한 지점을 회귀 전수조사해 정합 결함
5건을 고쳤다. 넷은 같은 미출시 버전이 만든 자기교정이고, 마지막 하나는 7.0.10 및 그
이전부터 있던 결함이라 공개 CHANGELOG 에 항목을 넣었다.

- 적중 통계의 레이아웃명이 항상 비어 화면별 표가 성립하지 않던 것 → 페이지 캐시 값에
 레이아웃명을 함께 담고 적중 경로가 그것으로 귀속한다. 이전 버전이 문자열로만 저장한
 항목은 그대로 읽어 배포 직후 살아 있는 캐시를 버리지 않는다. 인터페이스는 불변이고
 코어 구현일 때만 항목 전체를 읽는다.
- 통계 url 컬럼(255)이 캐시 키 URL(정규화 쿼리 최대 512바이트)보다 짧아 긴 주소의 기록이
 엄격 모드에서 실패하고 그 예외를 서비스가 삼키던 것 → 768 로 확장(utf8mb4 인덱스 키
 상한 3072바이트). down 은 잘리는 행을 먼저 지운다.
- 경로당 변종 상한이 언어를 합산해 다국어 사이트의 실효 상한이 언어 수만큼 줄던 것 →
 인덱스 항목이 url|locale 별이므로 경로·언어 조합 단위로 판정한다.
- 그릴 게 없는 요청(미라우트 404·SEO 비활성)이 렌더 예산을 소모해, 캐시에 남지 않는 죽은
 주소 재크롤이 정상 페이지의 예산을 태우던 것 → null 렌더는 차감을 환급한다. 렌더 도중
 예외는 비용을 이미 치른 것이라 환급하지 않는다.
- 병합 단계에서 건너뛴 확장이 있는 결과가 캐시로 굳어 버전 bump 전까지 그 확장 자산이
 사라진 채 고정되던 것 → 병합 루프를 mergeJs/mergeCss 로 분리해 건너뛴 확장을 함께
 돌려주고, 하나라도 있으면 캐시하지 않고 Log::error 로 남긴다. 캐시 없이 돌면 호출측이
 매 요청 다시 병합하므로 원인이 사라지는 즉시 회복한다. 기본 로그 수준이 error 라
 종전 per-extension warning 은 기록되지 않아 흔적이 없었다.

회귀 테스트 8건은 red 확인 후 green 으로 전환했고, 변경 심볼의 소비자 모집단(테스트
클래스 9종)을 프로세스 분할로 재실행해 189건 통과를 확인했다.
2026-09-08 08:17:51 +09:00

275 lines
9.1 KiB
PHP

<?php
namespace Tests\Unit\Cache\Migration;
use App\Contracts\Extension\CacheInterface;
use App\Extension\Cache\CoreCacheDriver;
use App\Seo\SeoCacheManager;
use Illuminate\Support\Facades\Cache;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
/**
* 그룹 C 이관 검증 테스트 (SEO 캐시)
*
* 계획서 §13 C-1, C-2, C-3 의 SEO 캐시 키 이관을 검증합니다.
* - SeoCacheManager: 페이지/인덱스/clearAll/패턴 무효화
* - SeoConfigMerger: 병합 캐시 키 형식
* - Sitemap: GenerateSitemapJob, SitemapController 키 형식
*
* 모듈/플러그인 의존성이 큰 SeoConfigMerger와 Sitemap 통합 테스트는
* 별도 SeoConfigMergerTest / SitemapControllerTest 에 의존합니다.
*/
class SeoCacheTest extends TestCase
{
private CoreCacheDriver $cache;
private SeoCacheManager $manager;
protected function setUp(): void
{
parent::setUp();
Cache::flush();
$this->cache = new CoreCacheDriver('array');
$this->app->instance(CacheInterface::class, $this->cache);
// SEO 캐시 활성화
$this->app['config']->set('g7_settings.core.seo.cache_enabled', true);
$this->app['config']->set('g7_settings.core.seo.cache_ttl', 7200);
$this->manager = new SeoCacheManager($this->cache);
}
protected function tearDown(): void
{
Cache::flush();
parent::tearDown();
}
// ========================================================================
// C-1. SeoCacheManager
// ========================================================================
/**
* C-1-1: SEO 페이지 캐시 생성 — put() 호출 시 새 키 형식.
*/
#[Test]
public function c_1_1_seo_page_cache_uses_new_prefix(): void
{
$this->manager->put('/board/notice/123', 'ko', '<html>Hello</html>');
$expectedKey = 'seo.page.'.md5('/board/notice/123|ko');
// 페이지는 레이아웃명과 함께 저장된다 — 적중 경로가 통계를 화면별로 귀속할 출처다.
$this->assertSame('<html>Hello</html>', $this->cache->get($expectedKey)['html'] ?? null);
$this->assertSame(
'g7:core:'.$expectedKey,
$this->cache->resolveKey($expectedKey)
);
}
/**
* C-1-2: SEO 페이지 캐시 히트.
*/
#[Test]
public function c_1_2_seo_page_cache_hit(): void
{
$this->manager->put('/page', 'ko', '<html>Cached</html>');
$this->assertSame('<html>Cached</html>', $this->manager->get('/page', 'ko'));
$this->assertSame('<html>Cached</html>', $this->manager->get('/page', 'ko'));
}
/**
* C-1-3: URL 패턴 기반 무효화 (와일드카드).
*/
#[Test]
public function c_1_3_invalidate_by_url_pattern_wildcards(): void
{
$this->manager->put('/board/notice/1', 'ko', '<html>1</html>');
$this->manager->put('/board/notice/2', 'ko', '<html>2</html>');
$this->manager->put('/board/free/1', 'ko', '<html>free</html>');
$count = $this->manager->invalidateByUrl('/board/notice/*');
$this->assertSame(2, $count);
$this->assertNull($this->manager->get('/board/notice/1', 'ko'));
$this->assertNull($this->manager->get('/board/notice/2', 'ko'));
$this->assertSame('<html>free</html>', $this->manager->get('/board/free/1', 'ko'));
}
/**
* C-1-5: clearAll() — 모든 SEO 페이지 캐시 + 인덱스 삭제.
*/
#[Test]
public function c_1_5_clear_all_removes_pages_and_index(): void
{
$this->manager->put('/page1', 'ko', '<a/>');
$this->manager->put('/page2', 'ko', '<b/>');
$this->assertNotEmpty($this->manager->getCachedUrls());
$this->manager->clearAll();
$this->assertEmpty($this->manager->getCachedUrls());
$this->assertNull($this->manager->get('/page1', 'ko'));
$this->assertNull($this->manager->get('/page2', 'ko'));
}
/**
* C-1-7: SEO 캐시 비활성 시 저장 안 함.
*/
#[Test]
public function c_1_7_disabled_cache_does_not_store(): void
{
$this->app['config']->set('g7_settings.core.cache.seo_enabled', false);
$this->app['config']->set('g7_settings.core.seo.cache_enabled', false);
$this->manager->put('/page', 'ko', '<html/>');
$this->assertNull($this->manager->get('/page', 'ko'));
}
/**
* C-1-extra: invalidateByLayout — 레이아웃 정보 매칭 삭제.
*/
#[Test]
public function c_1_extra_invalidate_by_layout(): void
{
$this->manager->putWithLayout('/board/list', 'ko', '<list/>', 'board.list');
$this->manager->putWithLayout('/board/show/1', 'ko', '<show/>', 'board.show');
$count = $this->manager->invalidateByLayout('board.list');
$this->assertSame(1, $count);
$this->assertNull($this->manager->get('/board/list', 'ko'));
$this->assertSame('<show/>', $this->manager->get('/board/show/1', 'ko'));
}
// ========================================================================
// C-3. Sitemap key format (GenerateSitemapJob, SitemapController)
// ========================================================================
/**
* C-3-1: Sitemap 캐시 키는 'seo.sitemap' 으로 통일.
*/
#[Test]
public function c_3_1_sitemap_cache_uses_new_key(): void
{
$this->cache->put('seo.sitemap', '<xml/>', 86400);
$this->assertSame('<xml/>', $this->cache->get('seo.sitemap'));
$this->assertSame(
'g7:core:seo.sitemap',
$this->cache->resolveKey('seo.sitemap')
);
}
/**
* C-1-4: 게시글 삭제 시 URL 패턴 + 목록 페이지 동시 무효화.
*/
#[Test]
public function c_1_4_post_delete_invalidates_detail_and_list(): void
{
$this->manager->put('/board/notice/123', 'ko', '<detail/>');
$this->manager->put('/board/notice', 'ko', '<list/>');
// 패턴 무효화: /board/notice* 모두 삭제
$count = $this->manager->invalidateByUrl('/board/notice*');
$this->assertSame(2, $count);
$this->assertNull($this->manager->get('/board/notice/123', 'ko'));
$this->assertNull($this->manager->get('/board/notice', 'ko'));
}
/**
* C-1-6: 모듈 설치 시 SeoCacheManager.clearAll() 동작 검증.
*/
#[Test]
public function c_1_6_module_install_clears_all(): void
{
$this->manager->put('/page1', 'ko', 'a');
$this->manager->put('/page2', 'ko', 'b');
$this->manager->put('/page3', 'ko', 'c');
// 모듈 설치 → SeoExtensionCacheListener 가 clearAll 호출
$this->manager->clearAll();
$this->assertNull($this->manager->get('/page1', 'ko'));
$this->assertNull($this->manager->get('/page2', 'ko'));
$this->assertNull($this->manager->get('/page3', 'ko'));
$this->assertEmpty($this->manager->getCachedUrls());
}
// ========================================================================
// C-2. SeoConfigMerger 병합 캐시 키 형식
// ========================================================================
/**
* C-2-1: SEO config 병합 캐시 키 형식 — 'seo.config.{templateId}'.
*/
#[Test]
public function c_2_1_seo_config_cache_key_format(): void
{
$this->cache->put('seo.config.sirsoft-admin_basic', ['merged' => true]);
$this->assertSame(
'g7:core:seo.config.sirsoft-admin_basic',
$this->cache->resolveKey('seo.config.sirsoft-admin_basic')
);
$this->assertTrue($this->cache->has('seo.config.sirsoft-admin_basic'));
}
/**
* C-2-2: SEO config 캐시 히트.
*/
#[Test]
public function c_2_2_seo_config_cache_hit(): void
{
$this->cache->put('seo.config.sirsoft-basic', ['v' => 1]);
$this->assertSame(['v' => 1], $this->cache->get('seo.config.sirsoft-basic'));
$this->assertSame(['v' => 1], $this->cache->get('seo.config.sirsoft-basic'));
}
/**
* C-2-3: 확장 변경 시 config 캐시 무효화.
*/
#[Test]
public function c_2_3_extension_change_invalidates_config_cache(): void
{
$this->cache->put('seo.config.sirsoft-basic', ['old' => true]);
$this->cache->forget('seo.config.sirsoft-basic');
$this->assertFalse($this->cache->has('seo.config.sirsoft-basic'));
}
/**
* C-3-3: 확장 변경 시 Sitemap 캐시 무효화 (SeoExtensionCacheListener 동작).
*/
#[Test]
public function c_3_3_extension_change_invalidates_sitemap(): void
{
$this->cache->put('seo.sitemap', '<old/>');
$this->cache->forget('seo.sitemap');
$this->assertFalse($this->cache->has('seo.sitemap'));
}
/**
* C-3-4: SEO 설정 변경 시 Sitemap 캐시 무효화 (SeoSettingsCacheListener 동작).
*/
#[Test]
public function c_3_4_seo_settings_change_invalidates_sitemap(): void
{
$this->cache->put('seo.sitemap', '<old/>');
// SeoSettingsCacheListener 동작 시뮬레이션
$this->cache->forget('seo.sitemap');
$this->assertFalse($this->cache->has('seo.sitemap'));
}
}