공개 (@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종
226 lines
7.6 KiB
PHP
226 lines
7.6 KiB
PHP
<?php
|
|
|
|
namespace Plugins\Sirsoft\Ckeditor5\Tests\Feature\Console;
|
|
|
|
use App\Extension\Storage\PluginStorageDriver;
|
|
use App\Services\PluginSettingsService;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Mockery;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Plugins\Sirsoft\Ckeditor5\Models\Ckeditor5ImageUpload;
|
|
use Plugins\Sirsoft\Ckeditor5\Tests\PluginTestCase;
|
|
|
|
/**
|
|
* 미참조 이미지 정리 커맨드 테스트
|
|
*
|
|
* 인수 기준은 "설정을 만지지 않은 사이트에서 자동 삭제 0" 입니다. 스케줄 게이트가
|
|
* 설정 조회 실패 시 true 로 폴백하므로, 커맨드가 `--scheduled` 에서 false 폴백으로
|
|
* 재확인하는지를 회귀로 고정합니다.
|
|
*/
|
|
class PruneUnusedImagesCommandTest extends PluginTestCase
|
|
{
|
|
/**
|
|
* 플러그인 설정 조회를 지정 값으로 고정합니다.
|
|
*
|
|
* 테스트 환경에는 플러그인이 설치돼 있지 않아 PluginSettingsService 가 기본값을
|
|
* 그대로 돌려주므로, 설정 분기를 밟으려면 조회 지점을 대체해야 합니다.
|
|
*
|
|
* @param array<string, mixed> $values 설정 키 => 값
|
|
*/
|
|
private function fakePluginSettings(array $values): void
|
|
{
|
|
$service = Mockery::mock(PluginSettingsService::class);
|
|
|
|
$service->shouldReceive('get')
|
|
->andReturnUsing(function (string $identifier, ?string $key = null, mixed $default = null) use ($values) {
|
|
return array_key_exists($key, $values) ? $values[$key] : $default;
|
|
});
|
|
|
|
$this->app->instance(PluginSettingsService::class, $service);
|
|
}
|
|
|
|
/**
|
|
* 미참조 업로드 1건(파일 포함)을 만듭니다.
|
|
*
|
|
* @param int $ageDays 업로드 경과 일수
|
|
* @return Ckeditor5ImageUpload 생성된 기록
|
|
*/
|
|
private function makeStaleUpload(int $ageDays = 40): Ckeditor5ImageUpload
|
|
{
|
|
$filename = uniqid('cmd-').'.png';
|
|
(new PluginStorageDriver('sirsoft-ckeditor5', 'plugins'))->put('images', "2026/08/14/{$filename}", 'bytes');
|
|
|
|
$upload = Ckeditor5ImageUpload::create([
|
|
'original_name' => $filename,
|
|
'file_path' => "images/2026/08/14/{$filename}",
|
|
'storage_disk' => 'plugins',
|
|
'file_size' => 5,
|
|
'mime_type' => 'image/png',
|
|
]);
|
|
|
|
$upload->forceFill(['created_at' => now()->subDays($ageDays)])->saveQuietly();
|
|
|
|
return $upload->refresh();
|
|
}
|
|
|
|
/**
|
|
* 설정을 만지지 않은 사이트(설정 키 부재) 에서 스케줄 호출은 아무것도 지우지 않아야 한다.
|
|
*
|
|
* @effects schedule_gate_off_by_default
|
|
*/
|
|
#[Test]
|
|
public function scheduled_run_deletes_nothing_when_the_setting_was_never_touched(): void
|
|
{
|
|
$upload = $this->makeStaleUpload();
|
|
|
|
$this->artisan('sirsoft-ckeditor5:prune-unused-images --scheduled')
|
|
->assertSuccessful();
|
|
|
|
$this->assertDatabaseHas('ckeditor5_image_uploads', ['id' => $upload->id]);
|
|
}
|
|
|
|
/**
|
|
* @effects schedule_gate_off_explicit
|
|
*/
|
|
#[Test]
|
|
public function scheduled_run_stops_early_when_the_toggle_is_off(): void
|
|
{
|
|
$this->fakePluginSettings(['unusedImageCleanup' => false]);
|
|
$upload = $this->makeStaleUpload();
|
|
|
|
$this->artisan('sirsoft-ckeditor5:prune-unused-images --scheduled')
|
|
->assertSuccessful();
|
|
|
|
$this->assertDatabaseHas('ckeditor5_image_uploads', ['id' => $upload->id]);
|
|
}
|
|
|
|
/**
|
|
* @effects schedule_gate_on
|
|
*/
|
|
#[Test]
|
|
public function scheduled_run_prunes_when_the_toggle_is_on(): void
|
|
{
|
|
$this->fakePluginSettings([
|
|
'unusedImageCleanup' => true,
|
|
'unusedImageRetentionDays' => 30,
|
|
]);
|
|
$upload = $this->makeStaleUpload();
|
|
|
|
$this->artisan('sirsoft-ckeditor5:prune-unused-images --scheduled')
|
|
->assertSuccessful();
|
|
|
|
$this->assertDatabaseMissing('ckeditor5_image_uploads', ['id' => $upload->id]);
|
|
}
|
|
|
|
/**
|
|
* @effects retention_disabled_guard
|
|
*/
|
|
#[Test]
|
|
public function retention_below_one_day_performs_no_cleanup(): void
|
|
{
|
|
$upload = $this->makeStaleUpload();
|
|
|
|
$this->artisan('sirsoft-ckeditor5:prune-unused-images --days=0')
|
|
->assertSuccessful();
|
|
|
|
$this->assertDatabaseHas('ckeditor5_image_uploads', ['id' => $upload->id]);
|
|
}
|
|
|
|
/**
|
|
* @effects days_option_overrides_setting
|
|
*/
|
|
#[Test]
|
|
public function days_option_overrides_the_configured_retention(): void
|
|
{
|
|
$this->fakePluginSettings(['unusedImageRetentionDays' => 365]);
|
|
$upload = $this->makeStaleUpload(ageDays: 40);
|
|
|
|
$this->artisan('sirsoft-ckeditor5:prune-unused-images --days=7')
|
|
->assertSuccessful();
|
|
|
|
$this->assertDatabaseMissing('ckeditor5_image_uploads', ['id' => $upload->id]);
|
|
}
|
|
|
|
/**
|
|
* @effects manual_run_dry_run_no_delete
|
|
*/
|
|
#[Test]
|
|
public function dry_run_lists_targets_without_deleting(): void
|
|
{
|
|
$upload = $this->makeStaleUpload();
|
|
|
|
$this->artisan('sirsoft-ckeditor5:prune-unused-images --dry-run')
|
|
->expectsOutputToContain('[DRY RUN]')
|
|
->assertSuccessful();
|
|
|
|
$this->assertDatabaseHas('ckeditor5_image_uploads', ['id' => $upload->id]);
|
|
}
|
|
|
|
/**
|
|
* 수동 실행(스케줄 아님)은 토글과 무관하게 운영자 의사이므로 그대로 수행한다.
|
|
*
|
|
* @effects manual_run_ignores_toggle
|
|
*/
|
|
#[Test]
|
|
public function manual_run_prunes_regardless_of_the_schedule_toggle(): void
|
|
{
|
|
$this->fakePluginSettings(['unusedImageCleanup' => false, 'unusedImageRetentionDays' => 30]);
|
|
$upload = $this->makeStaleUpload();
|
|
|
|
$this->artisan('sirsoft-ckeditor5:prune-unused-images')
|
|
->assertSuccessful();
|
|
|
|
$this->assertDatabaseMissing('ckeditor5_image_uploads', ['id' => $upload->id]);
|
|
}
|
|
|
|
/**
|
|
* 실행 결과는 로그에 남긴다.
|
|
*
|
|
* 스케줄 실행은 아무도 보고 있지 않으므로, 무엇이 몇 건 지워졌는지가 로그에 남지 않으면
|
|
* 사후에 확인할 방법이 없다.
|
|
*
|
|
* @effects prune_logs_run_summary
|
|
*/
|
|
#[Test]
|
|
public function a_real_run_records_its_summary_in_the_log(): void
|
|
{
|
|
$this->fakePluginSettings(['unusedImageCleanup' => true, 'unusedImageRetentionDays' => 30]);
|
|
$this->makeStaleUpload();
|
|
|
|
Log::shouldReceive('info')
|
|
->once()
|
|
->with('PruneUnusedImagesCommand: 미참조 에디터 이미지 정리 완료', Mockery::on(function (array $context): bool {
|
|
return ($context['deleted'] ?? null) === 1
|
|
&& ($context['failed'] ?? null) === 0
|
|
&& ($context['days'] ?? null) === 30;
|
|
}));
|
|
|
|
Log::shouldReceive('warning')->zeroOrMoreTimes();
|
|
|
|
$this->artisan('sirsoft-ckeditor5:prune-unused-images --scheduled')
|
|
->assertSuccessful();
|
|
}
|
|
|
|
/**
|
|
* dry-run 은 아무것도 지우지 않으므로 완료 로그도 남기지 않는다.
|
|
*
|
|
* @effects prune_dry_run_no_delete
|
|
*/
|
|
#[Test]
|
|
public function a_dry_run_does_not_record_a_completion_log(): void
|
|
{
|
|
$this->fakePluginSettings(['unusedImageRetentionDays' => 30]);
|
|
$this->makeStaleUpload();
|
|
|
|
Log::shouldReceive('info')
|
|
->with('PruneUnusedImagesCommand: 미참조 에디터 이미지 정리 완료', Mockery::any())
|
|
->never();
|
|
|
|
Log::shouldReceive('info')->zeroOrMoreTimes();
|
|
Log::shouldReceive('warning')->zeroOrMoreTimes();
|
|
|
|
$this->artisan('sirsoft-ckeditor5:prune-unused-images --dry-run')
|
|
->assertSuccessful();
|
|
}
|
|
}
|