확장이 훅으로 등록한 카탈로그에서 고른 값은 그 확장이 사라져도 저장값으로 남는다. 그 상태에서 값만 보고 판정하면 이미 제공 불가한 항목이 사용자 화면에 그대로 노출된다. 예외도 경고도 로그도 남지 않아 관리자 화면과 나란히 보지 않으면 드러나지 않는다. - 결제수단에 지정한 PG, 현금영수증 발급사, 검색엔진 드라이버 세 축에 같은 판정을 적용. 공개 응답에서는 제거하고 관리자 응답에는 표시를 남긴다 — 운영자가 고쳐야 할 대상이라 감추면 복구 경로가 사라진다. 서버 검증도 같은 판정을 공유해 화면 우회 제출을 막는다. - 설정 단건 저장이 벌크 저장과 다른 경로를 타던 문제를 코어·이커머스·게시판에서 정리. 저장은 성공하고 파생 값만 갱신되지 않아 어느 화면에서 저장했느냐로 결과가 갈렸다. - 단건 저장의 값 형태 계약 신설. 빈 값으로 되돌릴 수 있게 하고, 타입은 defaults.json 의 기본값을 SSoT 로 삼는다. 해석 불가한 입력은 조용히 캐스팅하지 않고 거절한다. - 드라이버 셀렉트를 서버 카탈로그에 바인딩하고, 저장값이 카탈로그에 없으면 그 값을 드러내는 안내를 8곳 전수에 붙인다. 종전에는 빈 칸으로만 보여 무엇이 저장됐는지 알 수 없었다. 시나리오 매니페스트 8종을 면제 없이 신설했다. `audit:allow` 는 cross product 와 effects 검사를 함께 끄므로, 면제가 걸린 매니페스트에 선언을 추가하면 검증이 회복되지 않는다. 그 아래에서 대응 테스트가 없는 effect 이름 2건이 살아남아 있었고, 함께 정리했다.
126 lines
4.5 KiB
PHP
126 lines
4.5 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Extension;
|
|
|
|
use App\Contracts\Extension\TemplateManagerInterface;
|
|
use App\Extension\TemplateManager;
|
|
use App\Services\TemplateService;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* 템플릿 로드 멱등성 + 서비스 공유 인스턴스화 (C-2)
|
|
*
|
|
* `TemplateService` 생성자가 가드 없이 `TemplateManager::loadTemplates()` 를 호출했다.
|
|
* 그 메서드는 공유 싱글톤의 템플릿 맵을 통째로 리셋한 뒤 디렉토리를 다시 스캔하므로,
|
|
* 서비스가 주입될 때마다 풀스캔이 반복되고 싱글톤 상태가 매번 변형됐다.
|
|
* `TemplateManager` 안에서 `TemplateService` 를 해석하는 재진입 경로까지 있다.
|
|
*
|
|
* 생성자 로드를 단순 제거할 수는 없다 — 웹/serve/test 는 CoreServiceProvider::boot 가
|
|
* 로드를 보장하지만 그 외 콘솔 경로는 로딩을 건너뛰므로, 커맨드가 생성자 로드에 의존한다.
|
|
* 그래서 "아직 로드하지 않았을 때만 로드하는" 멱등 진입점을 둔다.
|
|
*/
|
|
class TemplateManagerEnsureLoadedTest extends TestCase
|
|
{
|
|
/**
|
|
* 매니저의 템플릿 맵을 반환합니다. (protected 프로퍼티 조회)
|
|
*
|
|
* @param TemplateManager $manager 대상 매니저
|
|
* @return array 템플릿 맵
|
|
*/
|
|
private function templatesOf(TemplateManager $manager): array
|
|
{
|
|
$prop = (new \ReflectionClass($manager))->getProperty('templates');
|
|
$prop->setAccessible(true);
|
|
|
|
return $prop->getValue($manager);
|
|
}
|
|
|
|
/**
|
|
* 계약에 멱등 로드 진입점이 선언되어 있다. (실패-먼저)
|
|
*/
|
|
public function test_interface_declares_ensure_loaded(): void
|
|
{
|
|
$this->assertTrue(
|
|
method_exists(TemplateManagerInterface::class, 'ensureLoaded'),
|
|
'멱등 로드 진입점이 계약에 없습니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* ensureLoaded 는 두 번 호출해도 재스캔하지 않는다. (실패-먼저)
|
|
*/
|
|
public function test_ensure_loaded_is_idempotent(): void
|
|
{
|
|
/** @var TemplateManager $manager */
|
|
$manager = $this->app->make(TemplateManager::class);
|
|
$manager->loadTemplates();
|
|
|
|
$before = $this->templatesOf($manager);
|
|
// 로드 완료 표식을 남긴 뒤에는 맵을 리셋하지 않아야 한다
|
|
$manager->ensureLoaded();
|
|
|
|
$this->assertSame(
|
|
array_keys($before),
|
|
array_keys($this->templatesOf($manager)),
|
|
'ensureLoaded 가 이미 로드된 맵을 다시 만들었습니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 명시적 loadTemplates() 는 강제 재스캔 계약을 유지한다. (비회귀 pin)
|
|
*/
|
|
public function test_explicit_load_templates_still_rescans(): void
|
|
{
|
|
/** @var TemplateManager $manager */
|
|
$manager = $this->app->make(TemplateManager::class);
|
|
$manager->ensureLoaded();
|
|
|
|
$prop = (new \ReflectionClass($manager))->getProperty('templates');
|
|
$prop->setAccessible(true);
|
|
$prop->setValue($manager, ['stale-marker' => 'x']);
|
|
|
|
$manager->loadTemplates();
|
|
|
|
$this->assertArrayNotHasKey(
|
|
'stale-marker',
|
|
$this->templatesOf($manager),
|
|
'명시적 재스캔이 기존 맵을 갱신하지 않았습니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* TemplateService 생성자는 공유 싱글톤의 템플릿 맵을 리셋하지 않는다. (실패-먼저)
|
|
*/
|
|
public function test_service_construction_does_not_reset_shared_map(): void
|
|
{
|
|
/** @var TemplateManager $manager */
|
|
$manager = $this->app->make(TemplateManager::class);
|
|
$manager->loadTemplates();
|
|
|
|
$prop = (new \ReflectionClass($manager))->getProperty('templates');
|
|
$prop->setAccessible(true);
|
|
$prop->setValue($manager, ['sentinel-template' => ['identifier' => 'sentinel-template']]);
|
|
|
|
$this->app->forgetInstance(TemplateService::class);
|
|
$this->app->make(TemplateService::class);
|
|
|
|
$this->assertArrayHasKey(
|
|
'sentinel-template',
|
|
$this->templatesOf($manager),
|
|
'TemplateService 주입이 공유 템플릿 맵을 통째로 재스캔했습니다.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* TemplateService 는 공유 인스턴스로 해석된다. (실패-먼저)
|
|
*/
|
|
public function test_template_service_is_shared_instance(): void
|
|
{
|
|
$this->assertSame(
|
|
$this->app->make(TemplateService::class),
|
|
$this->app->make(TemplateService::class),
|
|
'TemplateService 가 해석할 때마다 새로 만들어집니다.'
|
|
);
|
|
}
|
|
}
|