테스트 종료 시 정리할 임시 디렉토리 목록 */ private array $tempDirs = []; protected function setUp(): void { parent::setUp(); $this->extensionManager = app(ExtensionManager::class); } protected function tearDown(): void { foreach ($this->tempDirs as $dir) { if (File::isDirectory($dir)) { File::deleteDirectory($dir); } } parent::tearDown(); } /** * 루트 레벨에 manifest 가 있는 평탄 ZIP 을 추출하고 그 디렉토리를 반환하는지 검증합니다. */ public function test_extract_from_zip_returns_extract_dir_for_flat_layout(): void { if (! class_exists(\ZipArchive::class)) { $this->markTestSkipped('ZipArchive not available'); } $tempDir = $this->makeTempDir('flat'); $zipPath = $tempDir.DIRECTORY_SEPARATOR.'flat.zip'; $zip = new \ZipArchive; $zip->open($zipPath, \ZipArchive::CREATE); $zip->addFromString('module.json', '{"identifier":"sirsoft-example","version":"1.0.0"}'); $zip->addFromString('composer.json', '{}'); $zip->close(); $destDir = $this->makeTempDir('flat_dest'); $extractedDir = $this->extensionManager->extractFromZip($zipPath, $destDir); $this->assertSame($destDir.DIRECTORY_SEPARATOR.'extracted', $extractedDir); $this->assertTrue(File::exists($extractedDir.DIRECTORY_SEPARATOR.'module.json')); } /** * owner-repo-hash/ 래퍼 디렉토리가 1개만 있는 ZIP 은 그 내부를 source 로 반환하는지 검증합니다. */ public function test_extract_from_zip_unwraps_single_wrapper_directory(): void { if (! class_exists(\ZipArchive::class)) { $this->markTestSkipped('ZipArchive not available'); } $tempDir = $this->makeTempDir('wrap'); $zipPath = $tempDir.DIRECTORY_SEPARATOR.'wrap.zip'; $zip = new \ZipArchive; $zip->open($zipPath, \ZipArchive::CREATE); $zip->addFromString('sirsoft-example-abc123/module.json', '{"identifier":"sirsoft-example","version":"1.0.0"}'); $zip->addFromString('sirsoft-example-abc123/README.md', '# example'); $zip->close(); $destDir = $this->makeTempDir('wrap_dest'); $extractedDir = $this->extensionManager->extractFromZip($zipPath, $destDir); $expected = $destDir.DIRECTORY_SEPARATOR.'extracted'.DIRECTORY_SEPARATOR.'sirsoft-example-abc123'; $this->assertSame(realpath($expected), realpath($extractedDir)); $this->assertTrue(File::exists($extractedDir.DIRECTORY_SEPARATOR.'module.json')); } /** * ZIP 파일이 존재하지 않으면 RuntimeException 이 발생하는지 검증합니다. */ public function test_extract_from_zip_throws_when_zip_missing(): void { $destDir = $this->makeTempDir('missing_dest'); $this->expectException(\RuntimeException::class); $this->extensionManager->extractFromZip($destDir.DIRECTORY_SEPARATOR.'nonexistent.zip', $destDir); } /** * prepareZipSource 가 유효한 ZIP + manifest 조합에서 version/manifest 를 반환하는지 검증합니다. */ public function test_prepare_zip_source_returns_version_and_manifest(): void { if (! class_exists(\ZipArchive::class)) { $this->markTestSkipped('ZipArchive not available'); } $tempDir = $this->makeTempDir('prep'); $zipPath = $tempDir.DIRECTORY_SEPARATOR.'ext.zip'; $zip = new \ZipArchive; $zip->open($zipPath, \ZipArchive::CREATE); $zip->addFromString('module.json', json_encode([ 'identifier' => 'sirsoft-example', 'version' => '2.3.4', 'name' => 'Example', ])); $zip->close(); $result = $this->extensionManager->prepareZipSource($zipPath, 'sirsoft-example', 'module.json'); $this->tempDirs[] = $result['temp_dir']; // 함수 내부에서 생성한 temp_dir 정리 등록 $this->assertSame('2.3.4', $result['to_version']); $this->assertSame('sirsoft-example', $result['manifest']['identifier']); $this->assertTrue(File::isDirectory($result['temp_dir'])); $this->assertTrue(File::exists($result['extracted_dir'].DIRECTORY_SEPARATOR.'module.json')); } /** * manifest 파일이 없는 경우 RuntimeException + 임시 디렉토리 자동 정리가 발생하는지 검증합니다. */ public function test_prepare_zip_source_throws_when_manifest_missing(): void { if (! class_exists(\ZipArchive::class)) { $this->markTestSkipped('ZipArchive not available'); } $tempDir = $this->makeTempDir('no_manifest'); $zipPath = $tempDir.DIRECTORY_SEPARATOR.'no_manifest.zip'; $zip = new \ZipArchive; $zip->open($zipPath, \ZipArchive::CREATE); $zip->addFromString('README.md', '# no manifest'); $zip->close(); $this->expectException(\RuntimeException::class); $this->extensionManager->prepareZipSource($zipPath, 'sirsoft-example', 'module.json'); } /** * identifier 가 일치하지 않으면 RuntimeException 이 발생하는지 검증합니다. */ public function test_prepare_zip_source_throws_on_identifier_mismatch(): void { if (! class_exists(\ZipArchive::class)) { $this->markTestSkipped('ZipArchive not available'); } $tempDir = $this->makeTempDir('id_mismatch'); $zipPath = $tempDir.DIRECTORY_SEPARATOR.'id_mismatch.zip'; $zip = new \ZipArchive; $zip->open($zipPath, \ZipArchive::CREATE); $zip->addFromString('module.json', json_encode([ 'identifier' => 'sirsoft-other', 'version' => '1.0.0', ])); $zip->close(); $this->expectException(\RuntimeException::class); $this->extensionManager->prepareZipSource($zipPath, 'sirsoft-example', 'module.json'); } /** * manifest 에 version 필드가 없으면 RuntimeException 이 발생하는지 검증합니다. */ public function test_prepare_zip_source_throws_when_version_missing(): void { if (! class_exists(\ZipArchive::class)) { $this->markTestSkipped('ZipArchive not available'); } $tempDir = $this->makeTempDir('no_version'); $zipPath = $tempDir.DIRECTORY_SEPARATOR.'no_version.zip'; $zip = new \ZipArchive; $zip->open($zipPath, \ZipArchive::CREATE); $zip->addFromString('module.json', json_encode([ 'identifier' => 'sirsoft-example', ])); $zip->close(); $this->expectException(\RuntimeException::class); $this->extensionManager->prepareZipSource($zipPath, 'sirsoft-example', 'module.json'); } /** * 임시 디렉토리를 생성하고 tearDown 시 정리되도록 등록합니다. */ private function makeTempDir(string $suffix): string { $dir = storage_path('test_ext_zip_'.$suffix.'_'.uniqid()); File::ensureDirectoryExists($dir); $this->tempDirs[] = $dir; return $dir; } }