Files
HeuJung 057e002ad1 feat(core,ecommerce,ckeditor5,board,admin_basic): 공개 자산 직접 URL(CDN) 서빙 옵트인
StorageInterface::url 의 public 하드코딩을 완화하고 core.storage.filter_url 훅을
신설해, 완전 공개 자산(상품·카테고리·리뷰·에디터 이미지)의 직접 URL 서빙을 옵트인으로
열었다. 미설정 시 기존 스트리밍 100% 보존.

디스크 결정은 확장 개별 설정 > 코어 전역 > 미설정 순이며, 서빙·삭제·이동은 행에 기록된
disk 를 따라 전환 전후 자산이 혼재해도 각자 실제 저장 위치를 향한다. 고아 disk(공급
플러그인 비활성화)는 스트리밍으로 폴백해 500 이 되지 않는다.

공개 이슈 (@lyg-kaban 건의) 대응.
2026-08-14 08:05:54 +09:00

127 lines
4.7 KiB
PHP

<?php
namespace Modules\Sirsoft\Ecommerce\Tests\Unit;
use App\Extension\ModuleManager;
use App\Services\ModuleSettingsService;
use Illuminate\Support\Facades\Config;
use Modules\Sirsoft\Ecommerce\Module;
use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase;
use PHPUnit\Framework\Attributes\Test;
/**
* getStorageDiskFor 설정 로드 순환 참조 회귀 테스트 (실제 설정 로드 경로)
*
* 실제 모듈 인스턴스의 getStorageDiskFor 가 module_setting() 을 경유할 때,
* 설정 로드 자신이 스토리지('settings' 카테고리)를 다시 요구하는 상황을
* 컨테이너 스파이로 재현합니다. PUBLIC_ASSET_CATEGORIES 게이트가 제거되면
* 'settings' 해석이 설정 서비스를 재호출해 호출 횟수 단언이 깨집니다.
*/
class StorageDiskRecursionTest extends ModuleTestCase
{
private Module $module;
/**
* 설정 로드가 스토리지를 경유하는 경로를 재현하는 스파이
*/
private object $settingsSpy;
protected function setUp(): void
{
parent::setUp();
Config::set('filesystems.disks.fake_cdn', [
'driver' => 'local',
'root' => storage_path('framework/testing/disks/fake_cdn'),
'url' => 'https://cdn.test/assets',
]);
Config::set('core.storage.public_asset_disk', '');
$this->module = app(ModuleManager::class)->getModule('sirsoft-ecommerce')
?? new Module(base_path('modules/sirsoft-ecommerce'));
// 실제 설정 로드처럼 get() 내부에서 모듈 스토리지('settings')를 재요구하는 스파이.
// 재진입(= getStorageDiskFor('settings') 가 다시 설정을 조회하는 회귀) 발생 시
// reentered 플래그 + 호출 횟수 증가로 검출됩니다.
$this->settingsSpy = new class extends ModuleSettingsService
{
public ?Module $module = null;
public int $calls = 0;
public bool $reentered = false;
public ?string $nestedDisk = null;
private bool $inGet = false;
public function __construct() {}
public function get(string $identifier, ?string $key = null, mixed $default = null): mixed
{
$this->calls++;
if ($this->inGet) {
// 설정 로드 중 설정 재조회 = 순환 고리. 무한 재귀 대신 폭로.
$this->reentered = true;
return $default;
}
$this->inGet = true;
try {
// 설정 로드가 스토리지를 경유하는 실제 경로 재현 —
// 이 호출이 getStorageDiskFor('settings') 를 타고 되돌아온다.
$this->nestedDisk = $this->module->getStorageFor('settings')->getDisk();
if ($key === 'basic_info.public_asset_disk') {
return 'fake_cdn';
}
return $default;
} finally {
$this->inGet = false;
}
}
};
$this->settingsSpy->module = $this->module;
$this->app->instance(ModuleSettingsService::class, $this->settingsSpy);
}
protected function tearDown(): void
{
$this->app->forgetInstance(ModuleSettingsService::class);
parent::tearDown();
}
/**
* @effects settings_load_recursion_free
*/
#[Test]
public function settings_category_never_consults_module_settings(): void
{
// 'settings' 카테고리 해석은 설정 서비스를 한 번도 거치지 않아야 한다 —
// 거치는 순간 설정 로드 ↔ 디스크 해석의 순환 고리가 열린다.
$this->assertSame($this->module->getStorageDisk(), $this->module->getStorageDiskFor('settings'));
$this->assertSame(0, $this->settingsSpy->calls);
}
/**
* @effects settings_load_recursion_free
*/
#[Test]
public function images_resolution_survives_settings_load_that_uses_storage(): void
{
// 공개 자산 카테고리 해석 → 설정 조회 → (설정 로드가 'settings' 스토리지 재요구)
// 전체 사슬이 유한 종료하고, 설정 조회는 정확히 1회만 발생해야 한다.
$this->assertSame('fake_cdn', $this->module->getStorageDiskFor('images'));
$this->assertSame(1, $this->settingsSpy->calls);
$this->assertFalse($this->settingsSpy->reentered, '설정 로드 중 설정 재조회(순환) 발생');
// 설정 로드가 요구한 'settings' 스토리지는 공개 자산 디스크가 아닌 기본 디스크
$this->assertSame($this->module->getStorageDisk(), $this->settingsSpy->nestedDisk);
}
}