이슈 후속(S7) 마감 + 릴리스 버전 확정. 기능(S7): - 사이트맵 전체 재생성 완료/실패 시 실행 관리자에게 알림(수동 실행 한정). 스케줄러·증분·봇 재생성은 알림 없음(triggered_by 부재 시 skip). - 최종 실패 전용 알림 + 재생성 전용 큐 + WS 진행상황 payload 봉투 정정. - Reverb OFF 폴링 폴백 경로 수정(실브라우저 폴링에서만 드러난 결함). 버전(D28 — 정정, D20 번복): - 릴리스를 7.0.5→7.0.6(patch)로 확정. 7.1.0(minor)은 다른 브랜치에서 별도 진행. - config/app.php + .env + README + INSTALL + CHANGELOG + 모듈 3 requires 를 7.0.6 로. - D19 SEO 캐시 이행 업그레이드 스텝을 Upgrade_7_0_6 로 리네임. 이유: 스텝 버전 게이팅이 version<=toVersion 이라, 7.1.0 이름이면 7.0.5→7.0.6 업그레이드에서 이행이 실행되지 않는다. 검증: composer test-smoke OK(2·29) · 재생성 알림/업그레이드 이행/진행상황 테스트 green · version-sync/version-bump(new-bump)/extension-require green · audit 0 error.
242 lines
7.5 KiB
PHP
242 lines
7.5 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Upgrades;
|
|
|
|
require_once __DIR__.'/../../../upgrades/data/7.0.6/migrations/01_NullifyUntouchedSeoCacheOverrides.php';
|
|
|
|
use App\Extension\UpgradeContext;
|
|
use App\Upgrades\Data\V7_0_6\Migrations\NullifyUntouchedSeoCacheOverrides;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* 7.0.6 업그레이드 스텝 — SEO 캐시 오버라이드 이행 (결정 D19)
|
|
*
|
|
* 판정 규칙:
|
|
* 옛 기본값과 같으면 "안 건드린 것" → null(미설정)로 비움
|
|
* 옛 기본값과 다르면 "의도적으로 바꾼 것" → 보존 (이제부터 오버라이드로 발동)
|
|
*
|
|
* 이 판정이 틀리면 기존 사이트의 캐시 동작이 의도치 않게 바뀐다.
|
|
*/
|
|
class NullifyUntouchedSeoCacheOverridesTest extends TestCase
|
|
{
|
|
private NullifyUntouchedSeoCacheOverrides $migration;
|
|
|
|
/**
|
|
* 테스트 초기화 - settings 디스크를 가짜로 대체합니다.
|
|
*/
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
Storage::fake('settings');
|
|
$this->migration = new NullifyUntouchedSeoCacheOverrides;
|
|
}
|
|
|
|
/**
|
|
* 업그레이드 컨텍스트를 만듭니다.
|
|
*
|
|
* @return UpgradeContext 컨텍스트
|
|
*/
|
|
private function context(): UpgradeContext
|
|
{
|
|
return new UpgradeContext('7.0.5', '7.0.6', '7.0.6');
|
|
}
|
|
|
|
/**
|
|
* seo.json 을 기록합니다.
|
|
*
|
|
* @param array<string, mixed> $settings 설정 배열
|
|
*/
|
|
private function writeSeoJson(array $settings): void
|
|
{
|
|
Storage::disk('settings')->put('seo.json', json_encode($settings));
|
|
}
|
|
|
|
/**
|
|
* seo.json 을 읽습니다.
|
|
*
|
|
* @return array<string, mixed> 설정 배열
|
|
*/
|
|
private function readSeoJson(): array
|
|
{
|
|
return json_decode(Storage::disk('settings')->get('seo.json'), true);
|
|
}
|
|
|
|
/**
|
|
* name: 로그용 식별자를 반환한다
|
|
*/
|
|
public function test_name_returns_identifier(): void
|
|
{
|
|
$this->assertSame('NullifyUntouchedSeoCacheOverrides', $this->migration->name());
|
|
}
|
|
|
|
/**
|
|
* 옛 기본값 그대로인 키는 null 로 비운다 (안 건드린 것으로 추정)
|
|
*/
|
|
public function test_clears_keys_left_at_legacy_defaults(): void
|
|
{
|
|
$this->writeSeoJson([
|
|
'cache_enabled' => true,
|
|
'cache_ttl' => 7200,
|
|
'sitemap_cache_ttl' => 86400,
|
|
'sitemap_enabled' => true,
|
|
]);
|
|
|
|
$this->migration->run($this->context());
|
|
$result = $this->readSeoJson();
|
|
|
|
$this->assertNull($result['cache_enabled']);
|
|
$this->assertNull($result['cache_ttl']);
|
|
$this->assertNull($result['sitemap_cache_ttl']);
|
|
}
|
|
|
|
/**
|
|
* 옛 기본값과 다른 값은 보존한다 (의도적으로 바꾼 것 → 오버라이드로 발동)
|
|
*
|
|
* 실제 사례: seo.cache_ttl=3600 을 설정해 뒀으나 그동안 무시되던 사이트.
|
|
*/
|
|
public function test_preserves_values_that_differ_from_legacy_defaults(): void
|
|
{
|
|
$this->writeSeoJson([
|
|
'cache_enabled' => false,
|
|
'cache_ttl' => 3600,
|
|
'sitemap_cache_ttl' => 604800,
|
|
]);
|
|
|
|
$this->migration->run($this->context());
|
|
$result = $this->readSeoJson();
|
|
|
|
$this->assertFalse($result['cache_enabled'], 'false 는 기본값(true)과 다르므로 보존돼야 합니다.');
|
|
$this->assertSame(3600, $result['cache_ttl']);
|
|
$this->assertSame(604800, $result['sitemap_cache_ttl']);
|
|
}
|
|
|
|
/**
|
|
* 일부만 바뀐 경우 — 바뀐 것만 보존하고 나머지는 비운다
|
|
*/
|
|
public function test_clears_untouched_and_preserves_changed_in_same_file(): void
|
|
{
|
|
$this->writeSeoJson([
|
|
'cache_enabled' => true, // 기본값 → 비움
|
|
'cache_ttl' => 3600, // 변경됨 → 보존
|
|
'sitemap_cache_ttl' => 86400, // 기본값 → 비움
|
|
]);
|
|
|
|
$this->migration->run($this->context());
|
|
$result = $this->readSeoJson();
|
|
|
|
$this->assertNull($result['cache_enabled']);
|
|
$this->assertSame(3600, $result['cache_ttl']);
|
|
$this->assertNull($result['sitemap_cache_ttl']);
|
|
}
|
|
|
|
/**
|
|
* seo 카테고리의 다른 키는 건드리지 않는다
|
|
*/
|
|
public function test_does_not_touch_unrelated_keys(): void
|
|
{
|
|
$this->writeSeoJson([
|
|
'cache_ttl' => 7200,
|
|
'sitemap_enabled' => true,
|
|
'sitemap_schedule' => 'hourly',
|
|
'sitemap_schedule_time' => '03:00',
|
|
'meta_title_suffix' => ' | G7',
|
|
]);
|
|
|
|
$this->migration->run($this->context());
|
|
$result = $this->readSeoJson();
|
|
|
|
$this->assertTrue($result['sitemap_enabled']);
|
|
$this->assertSame('hourly', $result['sitemap_schedule'], '재생성 주기는 이 이행과 무관합니다.');
|
|
$this->assertSame('03:00', $result['sitemap_schedule_time']);
|
|
$this->assertSame(' | G7', $result['meta_title_suffix']);
|
|
}
|
|
|
|
/**
|
|
* 멱등: 두 번 실행해도 결과가 같다
|
|
*/
|
|
public function test_is_idempotent(): void
|
|
{
|
|
$this->writeSeoJson([
|
|
'cache_ttl' => 7200,
|
|
'sitemap_cache_ttl' => 3600,
|
|
]);
|
|
|
|
$this->migration->run($this->context());
|
|
$first = $this->readSeoJson();
|
|
|
|
$this->migration->run($this->context());
|
|
$second = $this->readSeoJson();
|
|
|
|
$this->assertSame($first, $second);
|
|
$this->assertNull($second['cache_ttl']);
|
|
$this->assertSame(3600, $second['sitemap_cache_ttl']);
|
|
}
|
|
|
|
/**
|
|
* 이미 null 인 키는 그대로 둔다 (재실행 안전)
|
|
*/
|
|
public function test_leaves_already_null_keys_untouched(): void
|
|
{
|
|
$this->writeSeoJson(['cache_ttl' => null, 'sitemap_cache_ttl' => null]);
|
|
|
|
$this->migration->run($this->context());
|
|
$result = $this->readSeoJson();
|
|
|
|
$this->assertNull($result['cache_ttl']);
|
|
$this->assertNull($result['sitemap_cache_ttl']);
|
|
}
|
|
|
|
/**
|
|
* 키가 아예 없으면 새로 만들지 않는다
|
|
*/
|
|
public function test_does_not_add_absent_keys(): void
|
|
{
|
|
$this->writeSeoJson(['meta_title_suffix' => ' | G7']);
|
|
|
|
$this->migration->run($this->context());
|
|
$result = $this->readSeoJson();
|
|
|
|
$this->assertArrayNotHasKey('cache_ttl', $result);
|
|
$this->assertArrayNotHasKey('sitemap_cache_ttl', $result);
|
|
}
|
|
|
|
/**
|
|
* seo.json 이 없으면 no-op (예외 없음)
|
|
*/
|
|
public function test_no_op_when_settings_file_is_absent(): void
|
|
{
|
|
$this->migration->run($this->context());
|
|
|
|
$this->assertFalse(Storage::disk('settings')->exists('seo.json'));
|
|
}
|
|
|
|
/**
|
|
* 손상된 JSON 은 덮어쓰지 않고 보존한다 (파괴 금지)
|
|
*/
|
|
public function test_preserves_corrupted_json_without_overwriting(): void
|
|
{
|
|
Storage::disk('settings')->put('seo.json', '{ this is not json');
|
|
|
|
$this->migration->run($this->context());
|
|
|
|
$this->assertSame('{ this is not json', Storage::disk('settings')->get('seo.json'));
|
|
}
|
|
|
|
/**
|
|
* 타입이 다른 동일 값은 오버라이드로 본다 (느슨한 비교 금지)
|
|
*
|
|
* 문자열 "7200" 은 기본값 7200(int)과 == 로는 같지만, 운영자가 폼으로 입력한
|
|
* 흔적이므로 === 비교로 보존해야 한다.
|
|
*/
|
|
public function test_treats_type_mismatched_value_as_override(): void
|
|
{
|
|
$this->writeSeoJson(['cache_ttl' => '7200']);
|
|
|
|
$this->migration->run($this->context());
|
|
|
|
$this->assertSame('7200', $this->readSeoJson()['cache_ttl']);
|
|
}
|
|
}
|