Files
Gnuboard7/tests/Feature/Upgrades/RunUpgradeStepsAbstractGuardTest.php
T
HeuJung 67cae00dd8 fix(core): 코어 업데이트 spawn 자식의 config 캐시 오판 3건 수정 + 컨테이너 보존 래퍼 전수
7.0.9 클린 설치본에서 7.0.10 으로 올리자 업그레이드 스텝 단계가 "부모 메모리 stale" 로 중단됐다.
스텝 자식은 부모가 비우지 않은 이전 버전 config 캐시로 부팅해 env APP_VERSION 오버라이드와
신버전 config/app.php 의 update 목록을 모두 보지 못한다. 이전 릴리즈에서는 자식 진입부의
config:cache 가 전역 Container 를 일회용 앱으로 바꿔 놓는 부수효과로 가드가 우연히 침묵했고,
그 부수효과를 걷어낸 withPreservedContainer 가 잠복 결함을 드러냈다.

- stale 가드는 CoreVersionChecker::getCoreVersion(env 우선) 으로 판독
- 부모는 spawn 직전 config 캐시를 비우고, 자식은 캐시 파일이 있으면 디스크 config 를 직접 읽는다
 (7.0.10 이 추가한 public/build/ext 쓰기 권한 정상화 누락 차단)
- route:cache 도 같은 부수효과를 남기므로 RouteCacheHelper·optimizeSystem 을 보존 래퍼로 감쌌다
 (단독 스텝 재실행·시스템 최적화 뒤 정적 재게시 예약 소실)
- 과거 업그레이드 사고 14부류를 7.0.10 변경과 대조 — 재발 조건은 위 3건뿐
- audit 룰 fresh-app-artisan-call-outside-cache-helper + 픽스처, 회귀 테스트 6건(수정 전 red)
2026-09-06 18:41:53 +09:00

169 lines
5.1 KiB
PHP

<?php
namespace Tests\Feature\Upgrades;
use App\Exceptions\CoreUpdateOperationException;
use App\Services\CoreUpdateService;
use Illuminate\Support\Facades\File;
use Tests\TestCase;
/**
* runUpgradeSteps 의 신규 fatal 가드 검증.
*
* 7.0.0-beta.5 부터 step 인스턴스가 AbstractUpgradeStep 미상속이면 즉시 throw.
* legacy step (beta.4 이하) 은 가드 우회.
*
* 회귀 시나리오:
* 1. AbstractUpgradeStep 미상속 step 을 beta.5 버전으로 머지 → runUpgradeSteps 가 즉시 차단
* 2. legacy 버전 (alpha/beta.2~4) 은 미상속이어도 통과 (호환 보장)
* 3. beta.5+ step 이 AbstractUpgradeStep 상속하면 정상 실행
*/
class RunUpgradeStepsAbstractGuardTest extends TestCase
{
private array $stubStepFiles = [];
private ?string $originalEnvVersion = null;
protected function setUp(): void
{
parent::setUp();
// stale 메모리 가드는 env APP_VERSION 을 먼저 읽는다 (spawn 자식 계약). 테스트 프로세스의
// .env.testing 값이 target 판정에 끼어들지 않도록 비우고, 각 테스트가 직접 세운다.
$this->originalEnvVersion = $_ENV['APP_VERSION'] ?? null;
$this->setMemoryVersion(null);
}
protected function tearDown(): void
{
foreach ($this->stubStepFiles as $file) {
if (File::exists($file)) {
File::delete($file);
}
}
$this->setMemoryVersion($this->originalEnvVersion);
parent::tearDown();
}
/**
* 가드가 읽는 "메모리 버전" 을 env 와 config 양쪽에 세운다 (null 이면 env 만 비운다).
*
* @param string|null $version 세울 버전
*/
private function setMemoryVersion(?string $version): void
{
if ($version === null) {
unset($_ENV['APP_VERSION'], $_SERVER['APP_VERSION']);
putenv('APP_VERSION');
return;
}
$_ENV['APP_VERSION'] = $version;
$_SERVER['APP_VERSION'] = $version;
putenv('APP_VERSION='.$version);
config(['app.version' => $version]);
}
public function test_throws_when_beta5_step_does_not_extend_abstract(): void
{
// stale 메모리 가드 우회를 위해 메모리 버전을 target 이상으로
$this->setMemoryVersion('7.9.9');
// 7.9.9-test.guard.a 는 7.0.0-beta.5 보다 명확히 큼 (version_compare 의 pre-release
// 해석에 영향 받지 않음). stub 은 AbstractUpgradeStep 미상속 → fatal 가드 발동.
$version = '7_9_9_test_guard_a';
$className = 'Upgrade_'.$version;
$path = base_path('upgrades/'.$className.'.php');
$this->stubStepFiles[] = $path;
File::put($path, <<<PHP
<?php
namespace App\Upgrades;
use App\Contracts\Extension\UpgradeStepInterface;
use App\Extension\UpgradeContext;
class {$className} implements UpgradeStepInterface
{
public function run(UpgradeContext \$context): void {}
}
PHP);
$this->expectException(CoreUpdateOperationException::class);
$this->expectExceptionMessageMatches('/must extend App\\\\Extension\\\\AbstractUpgradeStep/');
$service = app(CoreUpdateService::class);
$service->runUpgradeSteps('7.0.0-beta.4', '7.9.9-test.guard.a');
}
public function test_passes_when_beta5_step_extends_abstract(): void
{
$this->setMemoryVersion('7.9.9');
$version = '7_9_9_test_guard_b';
$className = 'Upgrade_'.$version;
$path = base_path('upgrades/'.$className.'.php');
$this->stubStepFiles[] = $path;
File::put($path, <<<PHP
<?php
namespace App\Upgrades;
use App\Extension\AbstractUpgradeStep;
class {$className} extends AbstractUpgradeStep
{
}
PHP);
$executed = [];
$service = app(CoreUpdateService::class);
$service->runUpgradeSteps(
'7.0.0-beta.4',
'7.9.9-test.guard.b',
function (string $v) use (&$executed): void {
$executed[] = $v;
},
);
$this->assertContains('7.9.9-test.guard.b', $executed);
}
public function test_legacy_pre_beta5_step_bypasses_guard(): void
{
// legacy beta.4 이하 step 은 AbstractUpgradeStep 미상속이어도 통과 (호환)
$this->setMemoryVersion('7.0.0-beta.5');
$version = '7_0_0_beta_4_test_guard_legacy';
$className = 'Upgrade_'.$version;
$path = base_path('upgrades/'.$className.'.php');
$this->stubStepFiles[] = $path;
File::put($path, <<<PHP
<?php
namespace App\Upgrades;
use App\Contracts\Extension\UpgradeStepInterface;
use App\Extension\UpgradeContext;
class {$className} implements UpgradeStepInterface
{
public function run(UpgradeContext \$context): void {}
}
PHP);
$executed = [];
$service = app(CoreUpdateService::class);
$service->runUpgradeSteps(
'7.0.0-beta.3',
'7.0.0-beta.4.test.guard.legacy',
function (string $v) use (&$executed): void {
$executed[] = $v;
},
);
$this->assertContains('7.0.0-beta.4.test.guard.legacy', $executed);
}
}