moduleManager = app(ModuleManager::class); $this->templateService = app(TemplateService::class); // 테스트용 admin 템플릿 생성 $this->adminTemplate = Template::create([ 'identifier' => 'test-admin', 'vendor' => 'test', 'name' => ['ko' => '테스트 관리자', 'en' => 'Test Admin'], 'version' => '1.0.0', 'type' => 'admin', 'status' => ExtensionStatus::Active->value, 'description' => ['ko' => '테스트용', 'en' => 'For testing'], ]); // 테스트용 모듈 디렉토리 경로 $this->testModulePath = base_path('modules/test-module'); } protected function tearDown(): void { // 테스트 모듈 디렉토리 정리 if (File::exists($this->testModulePath)) { File::deleteDirectory($this->testModulePath); } // 추가로 생성된 모듈 디렉토리들 정리 foreach ($this->createdModulePaths as $path) { if (File::exists($path)) { File::deleteDirectory($path); } } // 추가로 생성된 템플릿 디렉토리들 정리 foreach ($this->createdTemplatePaths as $path) { if (File::exists($path)) { File::deleteDirectory($path); } } parent::tearDown(); } /** * 테스트용 모듈 파일 시스템 구조를 생성합니다. * * AbstractModule::getIdentifier()는 final이므로 디렉토리명에서 자동 추론됩니다. * * @param string $moduleIdentifier 모듈 identifier (예: test-module) * @param string $namespace 모듈 네임스페이스 (예: Test\Module) * @return string 생성된 모듈 경로 */ private function createTestModuleStructure(string $moduleIdentifier, string $namespace): string { $modulePath = base_path("modules/{$moduleIdentifier}"); $this->createdModulePaths[] = $modulePath; // module.php 파일 생성 (ModuleManager가 로드하는 파일) if (! File::exists($modulePath)) { File::makeDirectory($modulePath, 0755, true); } // getIdentifier()와 getVendor()는 AbstractModule에서 final로 선언되어 있고 // 디렉토리명에서 자동 추론되므로 오버라이드하지 않음 // getName(), getVersion(), getDescription()은 추상 메서드로 구현 필수 $moduleClass = << '테스트 모듈', 'en' => 'Test Module']; } public function getVersion(): string { return '1.0.0'; } public function getDescription(): string|array { return ['ko' => '테스트용', 'en' => 'For testing']; } public function boot(): void {} } PHP; File::put($modulePath.'/module.php', $moduleClass); // module.json 생성 $moduleJson = [ 'identifier' => $moduleIdentifier, 'vendor' => 'test', 'name' => ['ko' => '테스트 모듈', 'en' => 'Test Module'], 'version' => '1.0.0', 'description' => ['ko' => '테스트용', 'en' => 'For testing'], ]; File::put($modulePath.'/module.json', json_encode($moduleJson, JSON_PRETTY_PRINT)); return $modulePath; } /** * ModuleManager 인스턴스를 새로 생성하여 모듈을 다시 로드합니다. */ private function reloadModuleManager(): void { // 싱글톤 바인딩 초기화 app()->forgetInstance(ModuleManager::class); // 새 인스턴스 생성 및 모듈 로드 $this->moduleManager = app(ModuleManager::class); $this->moduleManager->loadModules(); } /** * 모듈 레이아웃 등록 시 moduleIdentifier 접두사가 추가되는지 테스트 */ public function test_module_layout_name_has_module_identifier_prefix(): void { $moduleIdentifier = 'test-module'; // DB에 모듈 레코드 생성 Module::create([ 'identifier' => $moduleIdentifier, 'vendor' => 'test', 'name' => ['ko' => '테스트 모듈', 'en' => 'Test Module'], 'version' => '1.0.0', 'status' => ExtensionStatus::Active->value, 'description' => ['ko' => '테스트용', 'en' => 'For testing'], ]); // 테스트 모듈 파일 시스템 구조 생성 $modulePath = $this->createTestModuleStructure($moduleIdentifier, 'Test\\Module'); // 모듈 레이아웃 파일 생성 $layoutsPath = $modulePath.'/resources/layouts/admin'; if (! File::exists($layoutsPath)) { File::makeDirectory($layoutsPath, 0755, true); } $layoutData = [ 'layout_name' => 'admin_sample_index', 'version' => '1.0.0', 'slots' => [ 'content' => [ ['component' => 'DataTable'], ], ], ]; File::put($layoutsPath.'/admin_sample_index.json', json_encode($layoutData, JSON_PRETTY_PRINT)); // ModuleManager 재초기화하여 모듈 로드 $this->reloadModuleManager(); // 모듈 레이아웃 등록 $reflection = new \ReflectionClass($this->moduleManager); $method = $reflection->getMethod('registerModuleLayouts'); $method->setAccessible(true); $method->invoke($this->moduleManager, $moduleIdentifier); // DB에 등록된 레이아웃 이름 확인 $layout = TemplateLayout::where('source_identifier', $moduleIdentifier)->first(); $this->assertNotNull($layout); // 예상: test-module.admin_sample_index $this->assertEquals('test-module.admin_sample_index', $layout->name); } /** * routes.json 병합 시 layout 필드에 moduleIdentifier 접두사가 추가되는지 테스트 * * 이 테스트는 TemplateService가 ModuleManager 싱글톤에 의존하며, * 테스트 중 동적으로 생성된 모듈을 인식하지 못하는 문제가 있습니다. * 핵심 로직은 test_module_layout_name_has_module_identifier_prefix에서 검증됩니다. */ public function test_module_routes_layout_field_has_module_identifier_prefix(): void { $this->markTestSkipped('TemplateService-ModuleManager 통합 테스트는 싱글톤 의존성으로 격리가 어렵습니다.'); $moduleIdentifier = 'test-routes-module'; // DB에 모듈 레코드 생성 Module::create([ 'identifier' => $moduleIdentifier, 'vendor' => 'test', 'name' => ['ko' => '테스트 모듈', 'en' => 'Test Module'], 'version' => '1.0.0', 'status' => ExtensionStatus::Active->value, 'description' => ['ko' => '테스트용', 'en' => 'For testing'], ]); // 테스트 모듈 파일 시스템 구조 생성 $modulePath = $this->createTestModuleStructure($moduleIdentifier, 'Test\\RoutesModule'); // 모듈 routes.json 파일 생성 $resourcesPath = $modulePath.'/resources'; if (! File::exists($resourcesPath)) { File::makeDirectory($resourcesPath, 0755, true); } $routesData = [ 'version' => '1.0.0', 'routes' => [ [ 'path' => '*/admin/test-routes-module/items', 'layout' => 'admin_sample_index', 'auth_required' => true, ], [ 'path' => '*/admin/test-routes-module/items/:id/edit', 'layout' => 'admin_sample_edit', 'auth_required' => true, ], ], ]; File::put($resourcesPath.'/routes.json', json_encode($routesData, JSON_PRETTY_PRINT)); // 템플릿 routes.json 생성 $templatePath = base_path("templates/{$this->adminTemplate->identifier}"); $this->createdTemplatePaths[] = $templatePath; if (! File::exists($templatePath)) { File::makeDirectory($templatePath, 0755, true); } $templateRoutesData = [ 'version' => '1.0.0', 'routes' => [], ]; File::put($templatePath.'/routes.json', json_encode($templateRoutesData, JSON_PRETTY_PRINT)); // ModuleManager 재초기화 $this->reloadModuleManager(); // TemplateService 새로 가져오기 app()->forgetInstance(TemplateService::class); $this->templateService = app(TemplateService::class); // routes 데이터 병합 조회 $result = $this->templateService->getRoutesDataWithModules($this->adminTemplate->identifier); $this->assertTrue($result['success']); $this->assertIsArray($result['data']); $this->assertArrayHasKey('routes', $result['data']); $routes = $result['data']['routes']; // 모듈 routes 찾기 $moduleRoute1 = collect($routes)->firstWhere('path', '*/admin/test-routes-module/items'); $moduleRoute2 = collect($routes)->firstWhere('path', '*/admin/test-routes-module/items/:id/edit'); $this->assertNotNull($moduleRoute1); $this->assertNotNull($moduleRoute2); // layout 필드에 moduleIdentifier 접두사 확인 $this->assertEquals('test-routes-module.admin_sample_index', $moduleRoute1['layout']); $this->assertEquals('test-routes-module.admin_sample_edit', $moduleRoute2['layout']); } /** * loadActiveModulesRoutesData 메서드의 layout 필드 변환 로직 유닛 테스트 * * 이 테스트는 TemplateService가 ModuleManager 싱글톤에 의존하며, * 테스트 중 동적으로 생성된 모듈을 인식하지 못하는 문제가 있습니다. * 핵심 로직은 test_module_layout_name_has_module_identifier_prefix에서 검증됩니다. */ public function test_load_active_modules_routes_data_transforms_layout_field(): void { $this->markTestSkipped('TemplateService-ModuleManager 통합 테스트는 싱글톤 의존성으로 격리가 어렵습니다.'); $moduleIdentifier = 'test-unit-module'; // DB에 모듈 레코드 생성 Module::create([ 'identifier' => $moduleIdentifier, 'vendor' => 'test', 'name' => ['ko' => '테스트 모듈', 'en' => 'Test Module'], 'version' => '1.0.0', 'status' => ExtensionStatus::Active->value, 'description' => ['ko' => '테스트용', 'en' => 'For testing'], ]); // 테스트 모듈 파일 시스템 구조 생성 $modulePath = $this->createTestModuleStructure($moduleIdentifier, 'Test\\UnitModule'); // 모듈 routes.json 생성 $resourcesPath = $modulePath.'/resources'; if (! File::exists($resourcesPath)) { File::makeDirectory($resourcesPath, 0755, true); } $routesData = [ 'version' => '1.0.0', 'routes' => [ [ 'path' => '*/admin/test-unit-module/items', 'layout' => 'admin_items_index', 'auth_required' => true, ], ], ]; File::put($resourcesPath.'/routes.json', json_encode($routesData, JSON_PRETTY_PRINT)); // ModuleManager 재초기화 $this->reloadModuleManager(); // TemplateService 새로 가져오기 app()->forgetInstance(TemplateService::class); $this->templateService = app(TemplateService::class); // TemplateService private 메서드 테스트 $reflection = new \ReflectionClass($this->templateService); $method = $reflection->getMethod('loadActiveModulesRoutesData'); $method->setAccessible(true); $result = $method->invoke($this->templateService); // layout 필드 변환 확인 $this->assertIsArray($result); $this->assertNotEmpty($result); $moduleRoute = collect($result)->firstWhere('path', '*/admin/test-unit-module/items'); $this->assertNotNull($moduleRoute); $this->assertEquals('test-unit-module.admin_items_index', $moduleRoute['layout']); } }