Files
Gnuboard7/tests/Feature/Services/SettingsServiceSiteLogoTest.php
HeuJung cfc2caba1b feat(ckeditor5,board,ecommerce,page,core,admin_basic): 미참조 에디터 업로드 이미지 정리 신설 + 업로드 고아 동종 결함 7건 전수 해소
공개 (@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종
2026-08-17 02:44:54 +09:00

190 lines
7.2 KiB
PHP

<?php
namespace Tests\Feature\Services;
use App\Contracts\Repositories\ConfigRepositoryInterface;
use App\Enums\AttachmentSourceType;
use App\Extension\Storage\CoreStorageDriver;
use App\Models\Attachment;
use App\Services\SettingsService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
/**
* 사이트 로고 교체 시 구 로고가 재편입되지 않는지 검증 (회귀)
*
* 저장할 목록은 **이번 요청이 제출한 것**이 정한다. 컬렉션 전체를 다시 수집하면 저장에
* 실패했거나 작성 중 이탈해 남은 미참조 첨부까지 설정에 되살아나, 운영자가 올린 적 없는
* 로고가 화면에 다시 나타난다(브라우저 실측 — 신규 1건만 제출했는데 저장 결과가 2건).
*/
class SettingsServiceSiteLogoTest extends TestCase
{
use RefreshDatabase;
/**
* site_logo 컬렉션 첨부를 파일과 함께 만듭니다.
*
* @return Attachment 생성된 첨부
*/
private function makeLogo(): Attachment
{
$filename = uniqid('logo-').'.png';
$path = "2026/08/14/{$filename}";
$disk = config('attachment.disk');
(new CoreStorageDriver($disk))->put('', $path, 'bytes');
return Attachment::create([
'attachmentable_type' => null,
'attachmentable_id' => null,
'source_type' => AttachmentSourceType::Core,
'source_identifier' => null,
'original_filename' => $filename,
'stored_filename' => $filename,
'disk' => $disk,
'path' => $path,
'mime_type' => 'image/png',
'size' => 5,
'collection' => 'site_logo',
'order' => 0,
]);
}
/**
* @effects site_logo_replacement_drops_previous
*/
#[Test]
public function replacing_the_logo_removes_the_previous_one_instead_of_re_collecting_it(): void
{
$service = app(SettingsService::class);
$config = app(ConfigRepositoryInterface::class);
// 1차 저장 — 첫 로고가 설정에 실린다.
$first = $this->makeLogo();
$service->saveSettings(['_tab' => 'general', 'general' => ['site_logo' => [$first->id]]]);
$this->assertSame([$first->id], $config->getCategory('general')['site_logo']);
// 2차 저장 — 운영자가 첫 로고를 빼고 새 로고를 올린 상태.
// 화면은 업로드를 끝낸 뒤 저장을 보내므로 새 로고 id 가 제출값에 실린다.
$second = $this->makeLogo();
$service->saveSettings(['_tab' => 'general', 'general' => ['site_logo' => [$second->id]]]);
$saved = $config->getCategory('general')['site_logo'];
$this->assertSame([$second->id], $saved, '설정에는 최신 로고만 남아야 한다.');
$this->assertDatabaseMissing('attachments', ['id' => $first->id]);
}
/**
* 저장에 실패했거나 이탈로 남은 미참조 첨부가 설정에 되살아나면 안 된다.
*
* @effects site_logo_unreferenced_collection_member_not_re_absorbed
*/
#[Test]
public function an_unreferenced_collection_member_is_not_re_absorbed(): void
{
$service = app(SettingsService::class);
$config = app(ConfigRepositoryInterface::class);
// 설정에 실린 적 없는 첨부 (직전 저장 실패로 남은 상태)
$orphan = $this->makeLogo();
// 운영자는 새 로고 1건만 올려 저장한다.
$fresh = $this->makeLogo();
$service->saveSettings(['_tab' => 'general', 'general' => ['site_logo' => [$fresh->id]]]);
$this->assertSame(
[$fresh->id],
$config->getCategory('general')['site_logo'],
'제출하지 않은 미참조 첨부가 설정에 편입되면 안 된다.'
);
$this->assertDatabaseHas('attachments', ['id' => $orphan->id]);
}
/**
* 제출값에 이미 삭제된 첨부 id 가 섞여 있어도 그 id 는 저장되지 않아야 한다.
*
* @effects site_logo_stale_submitted_id_filtered
*/
#[Test]
public function a_submitted_id_that_no_longer_exists_is_filtered_out(): void
{
$service = app(SettingsService::class);
$config = app(ConfigRepositoryInterface::class);
$logo = $this->makeLogo();
$service->saveSettings(['_tab' => 'general', 'general' => ['site_logo' => [$logo->id, 999999]]]);
$this->assertSame([$logo->id], $config->getCategory('general')['site_logo']);
}
/**
* site_logo 를 제출하지 않은 저장(다른 필드만 변경)은 로고를 건드리지 않아야 한다.
*
* @effects site_logo_untouched_when_not_submitted
*/
#[Test]
public function saving_other_fields_does_not_remove_the_logo(): void
{
$service = app(SettingsService::class);
$config = app(ConfigRepositoryInterface::class);
$logo = $this->makeLogo();
$service->saveSettings(['_tab' => 'general', 'general' => ['site_logo' => [$logo->id]]]);
$service->saveSettings(['_tab' => 'general', 'general' => ['site_name' => '테스트 사이트']]);
$this->assertSame([$logo->id], $config->getCategory('general')['site_logo']);
$this->assertDatabaseHas('attachments', ['id' => $logo->id]);
}
/**
* 저장이 실패하면 뺀 로고의 파일도 남아 있어야 한다 (회귀).
*
* 파기를 저장보다 먼저 하면, 저장이 실패했을 때 설정에는 구 로고 id 가 그대로 남는데
* 파일은 이미 사라져 로고가 깨진다. 파기는 저장이 확정된 뒤에만 수행해야 한다.
*
* @effects site_logo_failed_save_keeps_removed_attachment
*/
#[Test]
public function a_failed_save_does_not_destroy_the_removed_logo(): void
{
$config = app(ConfigRepositoryInterface::class);
// 로고가 저장된 상태를 만든다.
$logo = $this->makeLogo();
app(SettingsService::class)->saveSettings([
'_tab' => 'general',
'general' => ['site_logo' => [$logo->id]],
]);
$this->assertSame([$logo->id], $config->getCategory('general')['site_logo']);
// 저장 단계만 실패하도록 만든다 (조회는 실제 동작 유지).
$failing = \Mockery::mock($config);
$failing->shouldReceive('saveCategory')->andReturnFalse();
$this->app->instance(ConfigRepositoryInterface::class, $failing);
// 운영자가 로고를 빼고 저장 → 저장 실패
app(SettingsService::class)->saveSettings([
'_tab' => 'general',
'general' => ['site_logo' => []],
]);
$this->app->instance(ConfigRepositoryInterface::class, $config);
$this->assertDatabaseHas('attachments', ['id' => $logo->id]);
$this->assertTrue(
(new CoreStorageDriver($logo->disk))->exists('', $logo->path),
'저장이 실패했으면 파일도 남아 있어야 한다.'
);
$this->assertSame(
[$logo->id],
$config->getCategory('general')['site_logo'],
'저장이 실패했으므로 설정도 그대로여야 한다.'
);
}
}