합의되지 않은 구조 변경을 되돌리고, 계획서 전수조사에서 드러난 결함을 처리했다. 쇼핑 화면 호출 통합(F4)은 API 쿼리 튜닝이 아니라 화면 구조 변경이라 원복했다. 레이아웃 9개를 develop 상태로 복원하고 전용 엔드포인트·컨트롤러·문서·테스트를 걷어냈다. 되돌리면 함께 사라지는 비-F4 수정 둘(페이저 has_more_pages 판정 3곳, Icon 크기 클래스 23줄)은 재적용했다. 성능 개선분은 전부 잔존한다. 통합검색 화면 상태를 전역 상태에서 URL 쿼리로 옮겼다. 종전에는 탭·필터·정렬·페이지가 주소에 없어 새로고침·뒤로가기에서 초기화되고 결과를 공유할 수도 없었다. 화면은 정상으로 보이고 콘솔 에러도 없어 코드만으로는 드러나지 않는 형태였다. 확장이 라우트를 추가해도 라우트 캐시가 걸린 사이트에서는 그 주소가 404 가 되던 구멍을 막았다. 훅 캐시와 달리 라우트 캐시에는 스캔 폴백이 없어 예외도 경고도 남지 않는다. 같은 문제를 이미 푼 ConfigCacheHelper 를 미러링해 갱신 지점을 한 곳으로 모았고, 코어 업데이트가 캐시를 비우기만 하고 되살리지 않던 문제도 함께 고쳤다. 상품·페이지 검색이 커서 판정에 페이지 번호를 넘기지 않아, 커서 없이 깊은 페이지를 지목한 딥링크가 조용히 1 페이지를 돌려주던 결함을 고쳤다. 호출 지점을 손으로 열거하는 대신 저장소를 훑는 패리티 테스트로 다음 도메인의 재발을 막는다. 환경설정 고급 탭의 목록 상한값이 저장되지 않던 결함을 고쳤다. 화면·검증·읽기는 모두 있었는데 저장 시 카테고리 분류표가 손으로 열거돼 있어 그 값만 버려지고 있었다. 분류표를 설정 정의에서 도출하도록 바꿔 다음 카테고리가 합류해도 같은 일이 생기지 않게 했다. 범위를 벗어난 값의 안내에 내부 식별자가 노출되던 것도 함께 정리했다. 언어팩 재설치가 활성 팩을 자기 자신과의 슬롯 충돌로 오인해 강등하던 회귀와, CLI 설치가 HTTP 권한 컨텍스트 없이 자동 활성화되는지를 고정하는 테스트를 함께 담았다.
128 lines
5.1 KiB
PHP
128 lines
5.1 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Extension;
|
|
|
|
use App\Console\Commands\Core\CoreUpdateCommand;
|
|
use App\Console\Commands\Core\ExecuteUpgradeStepsCommand;
|
|
use App\Extension\ModuleManager;
|
|
use App\Extension\PluginManager;
|
|
use App\Support\RouteCacheHelper;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use ReflectionClass;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* 라우트 소스가 바뀌는 지점에서 라우트 캐시를 갱신하는지에 대한 회귀 테스트.
|
|
*
|
|
* 확장은 자기 라우트를 부팅 시 등록하지만, `route:cache` 가 걸린 사이트는 캐시에
|
|
* 직렬화된 라우트만 서빙한다. 확장을 설치·업데이트해 라우트가 늘어도 캐시를 갱신하지
|
|
* 않으면 그 라우트는 존재하지 않는 것과 같고, 오류 없이 404 만 난다.
|
|
*
|
|
* 훅 캐시와 달리 라우트 캐시에는 스캔 폴백이 없다는 점이 이 회귀의 핵심이다.
|
|
*/
|
|
class ExtensionRouteCacheInvalidationTest extends TestCase
|
|
{
|
|
#[Test]
|
|
public function 헬퍼는_언제나_stale_캐시를_먼저_제거한다(): void
|
|
{
|
|
Artisan::spy();
|
|
|
|
RouteCacheHelper::rebuild();
|
|
|
|
// 재생성 여부와 무관하게 낡은 캐시는 남기지 않는다 — 낡은 캐시는
|
|
// "방금 설치한 확장이 통째로 없는" 상태를 만든다.
|
|
Artisan::shouldHaveReceived('call')->with('route:clear')->once();
|
|
}
|
|
|
|
#[Test]
|
|
public function 테스트_환경에서는_캐시를_생성하지_않는다(): void
|
|
{
|
|
Artisan::spy();
|
|
|
|
RouteCacheHelper::rebuild();
|
|
|
|
// 캐시 생성은 같은 프로세스의 뒤따르는 테스트로 라우트가 새어 격리를 깬다.
|
|
Artisan::shouldNotHaveReceived('call', ['route:cache']);
|
|
}
|
|
|
|
#[Test]
|
|
public function clear_는_재생성_없이_비우기만_한다(): void
|
|
{
|
|
Artisan::spy();
|
|
|
|
RouteCacheHelper::clear();
|
|
|
|
Artisan::shouldHaveReceived('call')->with('route:clear')->once();
|
|
Artisan::shouldNotHaveReceived('call', ['route:cache']);
|
|
}
|
|
|
|
#[Test]
|
|
public function 라우트가_바뀌는_모든_지점이_갱신을_호출한다(): void
|
|
{
|
|
// 개별 지점을 손으로 열거하면 새 수명주기 메서드가 생겼을 때 그 지점만 조용히
|
|
// 빠진다. 소스를 훑어 "라우트가 바뀌는 지점" 전부가 갱신을 부르는지 본다.
|
|
$sites = [
|
|
ModuleManager::class => ['installModule', 'activateModule', 'deactivateModule', 'uninstallModule', 'updateModule'],
|
|
PluginManager::class => ['installPlugin', 'activatePlugin', 'deactivatePlugin', 'uninstallPlugin', 'updatePlugin'],
|
|
];
|
|
|
|
foreach ($sites as $class => $methods) {
|
|
foreach ($methods as $method) {
|
|
$this->assertStringContainsString(
|
|
'RouteCacheHelper::rebuild()',
|
|
$this->methodSource($class, $method),
|
|
$class.'::'.$method.' 이 라우트 캐시를 갱신하지 않는다 — '
|
|
.'캐시가 걸린 사이트에서 이 조작 뒤 확장 라우트가 404 가 된다'
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[Test]
|
|
public function 코어_업데이트_흐름이_라우트_캐시를_되살린다(): void
|
|
{
|
|
// 코어 업데이트는 흐름 중간에 라우트 캐시를 비운다(vendor 교체 중 상태를 구울 수
|
|
// 없으므로 옳다). 그러나 비운 채로 끝내면 그 사이트는 운영자가 수동으로
|
|
// route:cache 를 돌릴 때까지 라우트 캐시 없이 동작한다 — config 가 같은 이유로
|
|
// 흐름 끝에서 재생성되는 것과 대칭이어야 한다.
|
|
foreach ([CoreUpdateCommand::class, ExecuteUpgradeStepsCommand::class] as $class) {
|
|
$source = file_get_contents((new ReflectionClass($class))->getFileName());
|
|
|
|
$this->assertStringContainsString(
|
|
'RouteCacheHelper::rebuild()',
|
|
$source,
|
|
$class.' 가 흐름 끝에서 라우트 캐시를 되살리지 않는다'
|
|
);
|
|
|
|
// config 와 같은 자리에서 함께 되살아나야 한다 — 한쪽만 되살리면
|
|
// 그 사이트는 절반만 최적화된 상태로 남는다.
|
|
$this->assertStringContainsString(
|
|
'ConfigCacheHelper::rebuild()',
|
|
$source,
|
|
$class.' 의 config 재생성 지점이 사라졌다 (라우트 재생성의 기준점)'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 지정한 메서드의 소스 본문을 반환합니다.
|
|
*
|
|
* @param string $class 대상 클래스
|
|
* @param string $method 대상 메서드
|
|
* @return string 메서드 본문 소스
|
|
*/
|
|
private function methodSource(string $class, string $method): string
|
|
{
|
|
$reflection = new ReflectionClass($class);
|
|
$target = $reflection->getMethod($method);
|
|
$lines = file($reflection->getFileName());
|
|
|
|
return implode('', array_slice(
|
|
$lines,
|
|
$target->getStartLine() - 1,
|
|
$target->getEndLine() - $target->getStartLine() + 1
|
|
));
|
|
}
|
|
}
|