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); } }