Files
Gnuboard7/modules/_bundled/sirsoft-page/tests/Unit/Models/PageContentThumbnailTest.php
T
HeuJung 1ba67ca7cf feat(content): 본문 이미지 목록 썸네일·og:image 폴백 전 티어 구현
공개 이슈 https://github.com/gnuboard/g7/issues/22 — 본문에 삽입한 이미지가
카드/갤러리 목록 썸네일과 공유 미리보기(og:image)에 반영되지 않던 공백을
코어+3모듈+2템플릿에서 해소.

- 코어 7.0.9: HtmlImageExtractor(내부 이미지 한정, origin 정규화 SSoT 재사용)
 + seo.og_image_default 사이트 기본 공유 이미지 설정 + SeoMetaResolver 폴백 체인
- board 1.1.0 / ecommerce 1.2.0 / page 1.1.0: content_thumbnail_url 캐시 컬럼,
 saving 추출(html 모드 한정·첨부 우선·비밀글 게이트 유지),
 filter_content_thumbnail 훅, 기존 데이터 백필 업그레이드 스텝
- ecommerce 카테고리 og:image 생산자 신설, page og:description 死키 정정
- sirsoft-basic 1.1.2 og 바인딩 정정, admin_basic 1.0.7 SEO 탭 업로더(+ja 팩 2종)
- 검수 중 발견한 text 모드 오캐시 결함은 실패 테스트 선행 후 수정
2026-08-22 22:21:54 +09:00

174 lines
5.5 KiB
PHP

<?php
namespace Modules\Sirsoft\Page\Tests\Unit\Models;
use App\Extension\HookManager;
use Illuminate\Support\Facades\DB;
use Modules\Sirsoft\Page\Models\Page;
use Modules\Sirsoft\Page\Tests\ModuleTestCase;
use PHPUnit\Framework\Attributes\Test;
/**
* Page 모델 content_thumbnail_url 캐시 계산 테스트 (공개 이슈 #22 동종)
*
* saving 이벤트가 다국어 본문(JSON)에서 첫 내부 이미지 URL 을 저장 시점에
* 추출·캐시하는 계약을 고정합니다. 버전 롤백(restoreVersion)이 Repository 를
* 직접 호출해도 모델 이벤트가 커버하는 것이 배치 근거입니다.
*/
class PageContentThumbnailTest extends ModuleTestCase
{
private const FILTER_HOOK = 'sirsoft-page.page.filter_content_thumbnail';
protected function setUp(): void
{
parent::setUp();
config(['app.locale' => 'ko']);
}
protected function tearDown(): void
{
HookManager::clearFilter(self::FILTER_HOOK);
parent::tearDown();
}
/**
* @param array $attributes 덮어쓸 속성
* @return Page 생성된 페이지
*/
private function createPage(array $attributes = []): Page
{
return Page::factory()->create($attributes);
}
/**
* @scenario image_source=content_internal_only, locale_content=default_locale
*
* @effects content_image_fills_page_og
*/
#[Test]
public function saving_extracts_first_internal_image(): void
{
$page = $this->createPage([
'content' => [
'ko' => '<p>본문</p><img src="/storage/pages/ko-first.jpg">',
'en' => '<img src="/storage/pages/en.jpg">',
],
]);
$this->assertSame('/storage/pages/ko-first.jpg', $page->fresh()->content_thumbnail_url);
}
/**
* @scenario image_source=content_internal_only, locale_content=other_locale_only
*
* @effects other_locale_image_used_when_default_has_none
*/
#[Test]
public function falls_back_to_other_locale(): void
{
$page = $this->createPage([
'content' => [
'ko' => '<p>이미지 없음</p>',
'en' => '<img src="/storage/pages/en-only.jpg">',
],
]);
$this->assertSame('/storage/pages/en-only.jpg', $page->fresh()->content_thumbnail_url);
}
/**
* @scenario image_source=content_external_only, locale_content=default_locale
*
* @effects external_only_content_yields_null
*/
#[Test]
public function external_only_content_yields_null(): void
{
$page = $this->createPage([
'content' => ['ko' => '<img src="https://evil.example.org/x.jpg"><img src="//cdn.example.org/y.jpg">'],
]);
$this->assertNull($page->fresh()->content_thumbnail_url);
}
/**
* @scenario image_source=none, locale_content=default_locale
*
* @effects external_only_content_yields_null
*/
#[Test]
public function text_mode_and_no_image_yield_null(): void
{
$noImage = $this->createPage(['content' => ['ko' => '<p>이미지 없음</p>']]);
$textMode = $this->createPage([
'content_mode' => 'text',
'content' => ['ko' => '텍스트 <img src="/storage/pages/literal.jpg"> 마크업'],
]);
$this->assertNull($noImage->fresh()->content_thumbnail_url);
$this->assertNull($textMode->fresh()->content_thumbnail_url, 'text 모드는 캐시하지 않아야 합니다.');
}
/**
* @effects recompute_on_content_change
*/
#[Test]
public function content_change_recomputes_and_unrelated_update_does_not(): void
{
$page = $this->createPage([
'content' => ['ko' => '<img src="/storage/pages/old.jpg">'],
]);
$page->update(['content' => ['ko' => '<img src="/storage/pages/new.jpg">']]);
$this->assertSame('/storage/pages/new.jpg', $page->fresh()->content_thumbnail_url);
DB::table('pages')->where('id', $page->id)
->update(['content_thumbnail_url' => '/storage/pages/manual.jpg']);
$page->fresh()->update(['published' => true, 'published_at' => now()]);
$this->assertSame(
'/storage/pages/manual.jpg',
$page->fresh()->content_thumbnail_url,
'content 가 dirty 가 아니면 재계산하지 않아야 합니다.'
);
}
/**
* @effects filter_hook_can_override_or_block
*/
#[Test]
public function filter_hook_can_override_or_block(): void
{
HookManager::addFilter(self::FILTER_HOOK, fn ($value) => null);
$blocked = $this->createPage([
'content' => ['ko' => '<img src="/storage/pages/internal.jpg">'],
]);
$this->assertNull($blocked->fresh()->content_thumbnail_url);
}
/**
* 버전 롤백 경로(Repository 직접 update)에서도 캐시가 재계산되어야 합니다.
*
* @effects version_restore_recomputes_cache
*/
#[Test]
public function model_level_update_recomputes_cache(): void
{
$page = $this->createPage([
'content' => ['ko' => '<img src="/storage/pages/v1.jpg">'],
]);
// restoreVersion 은 Repository 를 직접 호출한다 — 모델 update 경로와 동일한
// saving 이벤트를 타므로 모델 레벨 update 로 등가 검증한다
Page::findOrFail($page->id)->update([
'content' => ['ko' => '<img src="/storage/pages/v2-restored.jpg">'],
]);
$this->assertSame('/storage/pages/v2-restored.jpg', $page->fresh()->content_thumbnail_url);
}
}