run($context); } /** * @scenario backfill=legacy_row * * @effects backfill_fills_legacy_rows */ #[Test] public function legacy_rows_are_backfilled_with_first_internal_image(): void { // createTestPost 는 DB 직접 insert — 모델 saving 이벤트를 타지 않아 // 업그레이드 전 작성 글(캐시 null)과 동일한 상태다 $legacy = $this->createTestPost([ 'content_mode' => 'html', 'content' => '

글

', ]); $this->runBackfill(); $this->assertSame( '/storage/uploads/legacy.jpg', DB::table('board_posts')->where('id', $legacy)->value('content_thumbnail_url'), '첫 외부 후보는 건너뛰고 첫 내부 이미지가 백필되어야 합니다.' ); } /** * @scenario backfill=already_filled * * @effects backfill_fills_legacy_rows */ #[Test] public function already_filled_rows_are_untouched(): void { $filled = $this->createTestPost([ 'content' => '', 'content_thumbnail_url' => '/storage/uploads/kept.jpg', ]); $this->runBackfill(); $this->assertSame( '/storage/uploads/kept.jpg', DB::table('board_posts')->where('id', $filled)->value('content_thumbnail_url'), '이미 값이 있는 행은 백필 대상이 아니어야 합니다.' ); } /** * @scenario backfill=external_only * * @effects backfill_keeps_external_only_null */ #[Test] public function external_only_rows_stay_null(): void { $external = $this->createTestPost([ 'content_mode' => 'html', 'content' => '', ]); $noImage = $this->createTestPost(['content_mode' => 'html', 'content' => '

이미지 없음

']); $this->runBackfill(); $this->assertNull(DB::table('board_posts')->where('id', $external)->value('content_thumbnail_url')); $this->assertNull(DB::table('board_posts')->where('id', $noImage)->value('content_thumbnail_url')); } /** * text 모드 레거시 행은 리터럴 img 마크업이 있어도 백필되지 않아야 합니다. * * text 모드 본문은 이스케이프 렌더라 상세에 이미지가 표시되지 않는다 — * 목록 썸네일만 뜨면 상세와 어긋난다 (content_mode 기본값이 text 라 모집단이 크다). * * @scenario backfill=text_mode * * @effects text_mode_content_never_caches */ #[Test] public function text_mode_rows_are_not_backfilled(): void { $textMode = $this->createTestPost([ 'content_mode' => 'text', 'content' => '텍스트 본문의 마크업', ]); $this->runBackfill(); $this->assertNull( DB::table('board_posts')->where('id', $textMode)->value('content_thumbnail_url'), 'text 모드 글은 백필 대상이 아니어야 합니다.' ); } /** * @scenario backfill=idempotent * * @effects backfill_idempotent_second_run */ #[Test] public function second_run_is_idempotent(): void { $this->createTestPost(['content_mode' => 'html', 'content' => '']); $this->createTestPost(['content_mode' => 'html', 'content' => '']); $this->createTestPost(['content_mode' => 'html', 'content' => '

이미지 없음

']); $this->runBackfill(); $first = $this->snapshot(); $this->runBackfill(); $second = $this->snapshot(); $this->assertSame($first, $second, '재실행 결과가 동일해야 합니다.'); } /** * 이 게시판 게시글의 (id, content_thumbnail_url) 스냅샷을 반환합니다. * * @return array 스냅샷 */ private function snapshot(): array { return DB::table('board_posts') ->where('board_id', $this->board->id) ->orderBy('id') ->get(['id', 'content_thumbnail_url']) ->map(fn ($row) => ['id' => (int) $row->id, 'content_thumbnail_url' => $row->content_thumbnail_url]) ->all(); } }