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', 'addFromString('vendor/laravel/framework/src/file.php', '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); } }