core:update Step 7 이 targets 전체를 무조건 재복사하고 orphan 을 삭제하여 사용자 .htaccess 커스텀 블록·_bundled 커스텀 확장이 소실되던 사고(공개 )를 차단한다. 백업(base)↔_pending(theirs) 3-way md5 비교로 코어가 실제 변경/추가한 파일만 산출해 적용하고, 코어 미변경 파일은 복사·권한·mtime 을 전부 스킵해 현재 디스크 상태(사용자 수정 포함)를 보존한다. 전체 덮어쓰기 + orphan 정리가 필요하면 --prune 으로 opt-in. 백업 부재(--no-backup) 시 base 가 없어 전체 덮어쓰기로 안전 회귀 + 안내를 출력한다. 기존 stale 정리 테스트는 새 기본(증분)에서 orphan 을 삭제하지 않으므로 prune 컨텍스트로 전환했고, 증분 3-way 판정·목록 적용·fallback 단위 테스트와 --prune 커맨드 표면 테스트를 추가했다. 번들 ja 언어팩은 코어 apply_mode 안내 키(g7-core-ja)와 함께, 같은 빌드에서 동기화된 관리자↔유저 크로스링크 라벨(board/ecommerce/admin_basic/basic)의 일본어 번역을 반영하고 각 패키지 버전·CHANGELOG 를 갱신했다.
137 lines
5.5 KiB
PHP
137 lines
5.5 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Console;
|
|
|
|
use App\Extension\Vendor\VendorBundler;
|
|
use App\Extension\Vendor\VendorMode;
|
|
use App\Services\CoreUpdateService;
|
|
use Illuminate\Support\Facades\File;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* core:update --vendor-mode 3 모드 Feature 테스트.
|
|
*
|
|
* 실제 core:update 커맨드를 풀 체인으로 검증하는 것은 마이그레이션/GitHub 연동 등
|
|
* 사이드 이펙트가 크므로, CoreUpdateService::runVendorInstallInPending() 경로를
|
|
* 중심으로 VendorResolver 분기 + 해시 검증 + zip 추출 동작을 격리 검증한다.
|
|
*/
|
|
class CoreUpdateWithBundleTest extends TestCase
|
|
{
|
|
private CoreUpdateService $service;
|
|
|
|
private string $pendingDir;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->service = app(CoreUpdateService::class);
|
|
$this->pendingDir = storage_path('app/test-core-vendor-bundle-'.uniqid());
|
|
File::ensureDirectoryExists($this->pendingDir);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
if (File::isDirectory($this->pendingDir)) {
|
|
File::deleteDirectory($this->pendingDir);
|
|
}
|
|
parent::tearDown();
|
|
}
|
|
|
|
/**
|
|
* 테스트용 _pending 디렉토리에 vendor-bundle.zip 을 생성합니다.
|
|
*/
|
|
private function createPendingBundle(): void
|
|
{
|
|
File::put($this->pendingDir.'/composer.json', json_encode([
|
|
'name' => 'test/core',
|
|
'require' => ['php' => '^8.2'],
|
|
]));
|
|
|
|
$zipPath = $this->pendingDir.'/vendor-bundle.zip';
|
|
$zip = new \ZipArchive;
|
|
$zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
|
|
$zip->addFromString('vendor/autoload.php', '<?php // test autoload');
|
|
$zip->addFromString('vendor/laravel/framework/src/file.php', '<?php // framework');
|
|
$zip->close();
|
|
|
|
File::put($this->pendingDir.'/vendor-bundle.json', json_encode([
|
|
'schema_version' => VendorBundler::SCHEMA_VERSION,
|
|
'zip_sha256' => hash_file('sha256', $zipPath),
|
|
'package_count' => 1,
|
|
]));
|
|
}
|
|
|
|
public function test_vendor_mode_bundled_extracts_bundle_zip(): void
|
|
{
|
|
$this->createPendingBundle();
|
|
|
|
$result = $this->service->runVendorInstallInPending($this->pendingDir, VendorMode::Bundled);
|
|
|
|
$this->assertSame(VendorMode::Bundled, $result->mode);
|
|
$this->assertSame('bundled', $result->strategy);
|
|
$this->assertFileExists($this->pendingDir.'/vendor/autoload.php');
|
|
$this->assertFileExists($this->pendingDir.'/vendor/laravel/framework/src/file.php');
|
|
}
|
|
|
|
public function test_vendor_mode_bundled_fails_when_bundle_missing(): void
|
|
{
|
|
// composer.json 은 있지만 vendor-bundle.zip 없음
|
|
File::put($this->pendingDir.'/composer.json', json_encode(['name' => 'test/core']));
|
|
|
|
$this->expectException(\App\Extension\Vendor\Exceptions\VendorInstallException::class);
|
|
$this->service->runVendorInstallInPending($this->pendingDir, VendorMode::Bundled);
|
|
}
|
|
|
|
public function test_vendor_mode_bundled_fails_on_tampered_zip(): void
|
|
{
|
|
$this->createPendingBundle();
|
|
|
|
// zip 해시를 조작하여 무결성 검증 실패 유도
|
|
$manifestPath = $this->pendingDir.'/vendor-bundle.json';
|
|
$manifest = json_decode(File::get($manifestPath), true);
|
|
$manifest['zip_sha256'] = hash('sha256', 'tampered');
|
|
File::put($manifestPath, json_encode($manifest));
|
|
|
|
$this->expectException(\App\Extension\Vendor\Exceptions\VendorInstallException::class);
|
|
$this->service->runVendorInstallInPending($this->pendingDir, VendorMode::Bundled);
|
|
}
|
|
|
|
public function test_vendor_mode_composer_requires_composer_executor_fallback(): void
|
|
{
|
|
// composer 모드 요청이지만 실제 composer 실행은 스킵될 수 있는 환경
|
|
// runVendorInstallInPending 은 내부적으로 CoreUpdateService::runComposerInstallInPending() 을
|
|
// composerExecutor 로 전달하므로, composer 바이너리 존재 여부에 따라 동작이 달라짐.
|
|
File::put($this->pendingDir.'/composer.json', json_encode(['name' => 'test/core']));
|
|
|
|
// composer 모드에서 번들 zip 없어도 composer 실행을 시도하지 — zip 무결성 검증 불필요
|
|
// 실제 실행은 환경에 따라 실패할 수 있으므로 예외 타입만 확인
|
|
try {
|
|
$this->service->runVendorInstallInPending($this->pendingDir, VendorMode::Composer);
|
|
$this->assertTrue(true); // composer 실행 성공 (거의 없음)
|
|
} catch (\Throwable $e) {
|
|
$this->assertNotEmpty($e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function test_core_update_command_accepts_vendor_mode_option(): void
|
|
{
|
|
// 실제 core:update 는 풀 체인 실행이 복잡하므로 --help 출력에서 옵션 존재만 검증
|
|
$this->artisan('core:update', ['--help' => true])
|
|
->expectsOutputToContain('--vendor-mode')
|
|
->assertExitCode(0);
|
|
}
|
|
|
|
/**
|
|
* `--prune` 옵션이 등록되어 있는지 검증합니다 (공개 #64).
|
|
*
|
|
* 실제 파일 적용 로직(증분/prune)은 CoreUpdateServiceTest 의 applyUpdate 단위
|
|
* 테스트가 커버하며, 여기서는 커맨드 표면에 옵션이 노출되는지만 확인한다.
|
|
*/
|
|
public function test_core_update_command_accepts_prune_option(): void
|
|
{
|
|
$this->artisan('core:update', ['--help' => true])
|
|
->expectsOutputToContain('--prune')
|
|
->assertExitCode(0);
|
|
}
|
|
}
|