공개 (@Tuwasduliebst 제보) 대응. - ckeditor5: 미참조 이미지 정리 엔진 신설 — 참조 판정(hash + 파일명 2토큰, filter_reference_sources 훅으로 확장 콘텐츠 등록) + 정리 커맨드(기본 꺼짐 옵트인, 3중 게이트) + 업로드 관리 화면(권한 분리·참조 배지·개별/선택 삭제) + 기설치본 설정 백필 upgrade step - board: 임시 첨부 GC(상시) + 소프트삭제 첨부 영구 정리(옵트인) + 설정 UI, 저장 payload 누락(F4)·빈 temp 디렉토리 잔존(F5) 수정 - ecommerce: 임시 상품 이미지 GC + 카테고리 삭제 시 이미지 파일 미삭제(S4) 수정 - page: 임시 첨부 GC - 코어: 고아 첨부 정리 커맨드(옵트인, site_logo 보호) + 사이트 로고 교체 시 구 로고 재편입 차단(S6) + DB 백업 미구현 501 정직화(B3) + 플러그인 관리자 메뉴 설치·업데이트 자동 동기화(F1·F7) - 동반: 플러그인 설정 검증 오류 필드명 라벨화(F3), Toggle 노드 type 정정 수평 전개(marketing 1.0.2 bump·gdpr·ecommerce), ja 언어팩 4종, API 문서, hooks.md, 시나리오 매니페스트 8종, Playwright E2E 4종
215 lines
7.8 KiB
PHP
215 lines
7.8 KiB
PHP
<?php
|
|
|
|
namespace Plugins\Sirsoft\Ckeditor5\Tests\Unit\Services;
|
|
|
|
use App\Extension\HookManager;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Plugins\Sirsoft\Ckeditor5\Models\Ckeditor5ImageUpload;
|
|
use Plugins\Sirsoft\Ckeditor5\Services\ImageReferenceScanService;
|
|
use Plugins\Sirsoft\Ckeditor5\Tests\PluginTestCase;
|
|
|
|
/**
|
|
* 업로드 이미지 참조 판정 테스트
|
|
*
|
|
* 본문에 박히는 URL 이 두 형태(해시 API 폴백형 / 디스크 직접 URL)라 판정 토큰도 둘입니다.
|
|
* 한 형태만 검사하면 다른 형태로 저장된 이미지를 "미참조" 로 오판해 삭제하게 되므로,
|
|
* 두 변종 모두를 회귀로 고정합니다.
|
|
*/
|
|
class ImageReferenceScanServiceTest extends PluginTestCase
|
|
{
|
|
private ImageReferenceScanService $service;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->service = app(ImageReferenceScanService::class);
|
|
}
|
|
|
|
/**
|
|
* 업로드 기록을 생성합니다.
|
|
*
|
|
* @param string $hash 이미지 해시
|
|
* @param string $filename 저장 파일명
|
|
* @return Ckeditor5ImageUpload 생성된 기록
|
|
*/
|
|
private function makeUpload(string $hash = 'abcdef012345', string $filename = 'aaaaaaaa-bbbb.png'): Ckeditor5ImageUpload
|
|
{
|
|
return Ckeditor5ImageUpload::create([
|
|
'hash' => $hash,
|
|
'original_name' => 'sample.png',
|
|
'file_path' => "images/2026/08/14/{$filename}",
|
|
'storage_disk' => 'plugins',
|
|
'file_size' => 100,
|
|
'mime_type' => 'image/png',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 코어 소유 참조 소스(메일 템플릿 본문)에 임의 내용을 담은 행을 만듭니다.
|
|
*
|
|
* @param string $content 본문
|
|
*/
|
|
private function seedCoreSourceContent(string $content): void
|
|
{
|
|
DB::table('mail_templates')->insert([
|
|
'type' => 'reference-scan-test-'.uniqid(),
|
|
'subject' => json_encode(['ko' => '참조 스캔 테스트']),
|
|
'body' => $content,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @effects reference_sources_default_core_only
|
|
*/
|
|
#[Test]
|
|
public function default_sources_are_core_only_when_no_extension_registers(): void
|
|
{
|
|
$tables = array_column($this->service->getReferenceSources(), 'table');
|
|
|
|
$this->assertContains('template_layouts', $tables);
|
|
$this->assertContains('notification_templates', $tables);
|
|
$this->assertNotContains('board_posts', $tables, '훅 미등록 상태에서는 모듈 소스가 포함되지 않아야 한다.');
|
|
}
|
|
|
|
/**
|
|
* @effects reference_sources_hook_append
|
|
*/
|
|
#[Test]
|
|
public function extensions_can_append_sources_through_the_filter_hook(): void
|
|
{
|
|
HookManager::addFilter(
|
|
ImageReferenceScanService::FILTER_REFERENCE_SOURCES,
|
|
fn (array $sources) => [...$sources, ['table' => 'pages', 'columns' => ['content']]],
|
|
);
|
|
|
|
$tables = array_column(app(ImageReferenceScanService::class)->getReferenceSources(), 'table');
|
|
|
|
$this->assertContains('pages', $tables);
|
|
}
|
|
|
|
/**
|
|
* @effects reference_match_hash_api_url
|
|
*/
|
|
#[Test]
|
|
public function hash_form_url_in_content_counts_as_referenced(): void
|
|
{
|
|
$upload = $this->makeUpload(hash: 'a1b2c3d4e5f6');
|
|
$this->seedCoreSourceContent('<img src="/api/plugins/sirsoft-ckeditor5/images/a1b2c3d4e5f6">');
|
|
|
|
$this->assertTrue($this->service->isReferenced($upload));
|
|
}
|
|
|
|
/**
|
|
* @effects reference_match_direct_url
|
|
*/
|
|
#[Test]
|
|
public function direct_disk_url_in_content_counts_as_referenced(): void
|
|
{
|
|
$upload = $this->makeUpload(hash: 'ffffffffffff', filename: '9f8e7d6c-1234.webp');
|
|
$this->seedCoreSourceContent('<img src="https://cdn.test/assets/ckeditor5/images/2026/08/14/9f8e7d6c-1234.webp">');
|
|
|
|
$this->assertTrue(
|
|
$this->service->isReferenced($upload),
|
|
'직접 URL 은 해시를 포함하지 않으므로 저장 파일명으로 매칭되어야 한다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @effects reference_unreferenced
|
|
*/
|
|
#[Test]
|
|
public function content_without_the_image_leaves_it_unreferenced(): void
|
|
{
|
|
$upload = $this->makeUpload(hash: '0123456789ab', filename: 'unused-file.png');
|
|
$this->seedCoreSourceContent('<p>이미지가 없는 본문</p>');
|
|
|
|
$this->assertFalse($this->service->isReferenced($upload));
|
|
}
|
|
|
|
/**
|
|
* LIKE 와일드카드가 들어간 파일명이 임의 문자로 해석되면 무관한 본문이 매칭된다.
|
|
*
|
|
* @effects reference_like_escape
|
|
*/
|
|
#[Test]
|
|
public function like_wildcards_in_the_filename_do_not_match_arbitrary_content(): void
|
|
{
|
|
$upload = $this->makeUpload(hash: 'cafebabe1234', filename: 'a%b_c.png');
|
|
$this->seedCoreSourceContent('<img src="/files/aXXXbYc.png">');
|
|
|
|
$this->assertFalse(
|
|
$this->service->isReferenced($upload),
|
|
'% 와 _ 는 이스케이프되어 리터럴로 비교되어야 한다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 불량 선언은 건너뛰되 **반드시 흔적을 남긴다**.
|
|
*
|
|
* 조용히 건너뛰면 확장이 등록한 소스가 오타 하나로 빠져도 아무도 모른 채 그 확장의
|
|
* 콘텐츠에 박힌 이미지가 "미참조" 로 분류된다 — 삭제 방향의 오판이라 경고가 유일한
|
|
* 발견 통로다. skip 동작과 경고를 함께 고정한다.
|
|
*
|
|
* @effects reference_invalid_source_skipped, reference_invalid_source_warns
|
|
*/
|
|
#[Test]
|
|
public function invalid_source_declarations_are_skipped_without_breaking_the_scan(): void
|
|
{
|
|
Log::spy();
|
|
|
|
HookManager::addFilter(
|
|
ImageReferenceScanService::FILTER_REFERENCE_SOURCES,
|
|
fn (array $sources) => [
|
|
...$sources,
|
|
['table' => 'no_such_table_here', 'columns' => ['content']],
|
|
['table' => 'template_layouts', 'columns' => ['no_such_column']],
|
|
['columns' => ['content']],
|
|
],
|
|
);
|
|
|
|
$service = app(ImageReferenceScanService::class);
|
|
$tables = array_column($service->getReferenceSources(), 'table');
|
|
|
|
$this->assertNotContains('no_such_table_here', $tables);
|
|
$this->assertContains('template_layouts', $tables);
|
|
|
|
// 형식 자체가 깨진 선언(table 키 없음) 1건
|
|
Log::shouldHaveReceived('warning')
|
|
->withArgs(fn ($message) => is_string($message) && str_contains($message, '형식이 올바르지 않아'))
|
|
->once();
|
|
|
|
// 실재하지 않는 테이블 1건 + 실재하지 않는 컬럼 1건
|
|
Log::shouldHaveReceived('warning')
|
|
->withArgs(fn ($message) => is_string($message) && str_contains($message, '존재하는 테이블/컬럼이 없어'))
|
|
->twice();
|
|
|
|
// 불량 선언이 섞여도 판정 자체는 정상 동작해야 한다.
|
|
$upload = $this->makeUpload(hash: 'deadbeef0000', filename: 'skip-test.png');
|
|
$this->seedCoreSourceContent('<img src="/api/plugins/sirsoft-ckeditor5/images/deadbeef0000">');
|
|
|
|
$this->assertTrue($service->isReferenced($upload));
|
|
}
|
|
|
|
/**
|
|
* @effects reference_map_batch
|
|
*/
|
|
#[Test]
|
|
public function map_referenced_reports_each_upload_separately(): void
|
|
{
|
|
$referenced = $this->makeUpload(hash: '111111111111', filename: 'used.png');
|
|
$unreferenced = $this->makeUpload(hash: '222222222222', filename: 'unused.png');
|
|
|
|
$this->seedCoreSourceContent('<img src="/api/plugins/sirsoft-ckeditor5/images/111111111111">');
|
|
|
|
$map = $this->service->mapReferenced(collect([$referenced, $unreferenced]));
|
|
|
|
$this->assertTrue($map[$referenced->id]);
|
|
$this->assertFalse($map[$unreferenced->id]);
|
|
}
|
|
}
|