pluginManager = app(PluginManager::class); $this->templateManager = app(TemplateManager::class); $this->layoutService = app(LayoutService::class); $this->service = new PluginSettingsService( $this->pluginManager, $this->templateManager, $this->layoutService ); // 테스트용 설정 디렉토리 경로 $this->testSettingsDir = dirname(ExtensionStoragePath::plugin('placeholder')); } // ======================================================================== // get() 메서드 테스트 // ======================================================================== /** * 존재하지 않는 플러그인 조회 시 기본값 반환 */ public function test_get_returns_default_when_plugin_not_found(): void { $result = $this->service->get('nonexistent-plugin'); $this->assertNull($result); } /** * 존재하지 않는 플러그인에 기본값 지정 시 해당 기본값 반환 */ public function test_get_returns_custom_default_when_plugin_not_found(): void { $defaultValue = ['key' => 'default']; $result = $this->service->get('nonexistent-plugin', null, $defaultValue); $this->assertEquals($defaultValue, $result); } /** * 플러그인 전체 설정 조회 성공 */ public function test_get_returns_all_settings_when_key_is_null(): void { // Mock plugin with config values $mockPluginManager = Mockery::mock(PluginManager::class); $mockPlugin = $this->createMockPlugin([ 'identifier' => 'test-settings-plugin', 'configValues' => [ 'api_key' => 'default-api-key', 'enabled' => true, 'timeout' => 30, ], 'settingsSchema' => [], ]); $mockPluginManager->shouldReceive('getPlugin') ->with('test-settings-plugin') ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $result = $service->get('test-settings-plugin'); $this->assertIsArray($result); $this->assertArrayHasKey('api_key', $result); $this->assertArrayHasKey('enabled', $result); $this->assertArrayHasKey('timeout', $result); } /** * 특정 키의 설정값 조회 성공 */ public function test_get_returns_specific_key_value(): void { $mockPluginManager = Mockery::mock(PluginManager::class); $mockPlugin = $this->createMockPlugin([ 'identifier' => 'test-key-plugin', 'configValues' => [ 'api_key' => 'my-secret-key', 'timeout' => 60, ], 'settingsSchema' => [], ]); $mockPluginManager->shouldReceive('getPlugin') ->with('test-key-plugin') ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $result = $service->get('test-key-plugin', 'timeout'); $this->assertEquals(60, $result); } /** * 존재하지 않는 키 조회 시 기본값 반환 */ public function test_get_returns_default_for_missing_key(): void { $mockPluginManager = Mockery::mock(PluginManager::class); $mockPlugin = $this->createMockPlugin([ 'identifier' => 'test-missing-key-plugin', 'configValues' => ['existing_key' => 'value'], 'settingsSchema' => [], ]); $mockPluginManager->shouldReceive('getPlugin') ->with('test-missing-key-plugin') ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $result = $service->get('test-missing-key-plugin', 'nonexistent_key', 'default_value'); $this->assertEquals('default_value', $result); } /** * 중첩된 키 값 조회 (dot notation) */ public function test_get_returns_nested_key_value(): void { $mockPluginManager = Mockery::mock(PluginManager::class); $mockPlugin = $this->createMockPlugin([ 'identifier' => 'test-nested-plugin', 'configValues' => [ 'database' => [ 'host' => 'localhost', 'port' => 3306, ], ], 'settingsSchema' => [], ]); $mockPluginManager->shouldReceive('getPlugin') ->with('test-nested-plugin') ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $host = $service->get('test-nested-plugin', 'database.host'); $port = $service->get('test-nested-plugin', 'database.port'); $this->assertEquals('localhost', $host); $this->assertEquals(3306, $port); } // ======================================================================== // save() 메서드 테스트 // ======================================================================== /** * 존재하지 않는 플러그인 저장 시 false 반환 */ public function test_save_returns_false_when_plugin_not_found(): void { $result = $this->service->save('nonexistent-plugin', ['key' => 'value']); $this->assertFalse($result); } /** * 플러그인 설정 저장 성공 */ public function test_save_stores_settings_successfully(): void { $identifier = 'test-save-plugin-'.uniqid(); $mockPluginManager = Mockery::mock(PluginManager::class); $mockPlugin = $this->createMockPlugin([ 'identifier' => $identifier, 'configValues' => [], 'settingsSchema' => [], 'storage' => [ 'exists' => false, 'put' => true, ], ]); $mockPluginManager->shouldReceive('getPlugin') ->with($identifier) ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $result = $service->save($identifier, [ 'api_key' => 'new-api-key', 'enabled' => true, ]); $this->assertTrue($result); } /** * 기존 설정과 병합되는지 확인 */ public function test_save_merges_with_existing_settings(): void { $identifier = 'test-merge-plugin-'.uniqid(); // 기존 설정 파일 내용 $existingContent = json_encode([ 'existing_key' => 'existing_value', 'to_update' => 'old_value', ]); $mockPluginManager = Mockery::mock(PluginManager::class); $mockPlugin = $this->createMockPlugin([ 'identifier' => $identifier, 'configValues' => [], 'settingsSchema' => [], 'storage' => [ 'exists' => true, 'get' => $existingContent, 'put' => true, ], ]); $mockPluginManager->shouldReceive('getPlugin') ->with($identifier) ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $result = $service->save($identifier, [ 'to_update' => 'new_value', 'new_key' => 'new_value', ]); $this->assertTrue($result); } // ======================================================================== // getLayout() 메서드 테스트 // ======================================================================== /** * 플러그인 인스턴스가 없으면 null 반환 */ public function test_get_layout_returns_null_when_plugin_instance_not_found(): void { $result = $this->service->getLayout('nonexistent-plugin'); $this->assertNull($result); } /** * 레이아웃 파일이 없으면 null 반환 */ public function test_get_layout_returns_null_when_layout_file_not_exists(): void { // Mock PluginManager to return a plugin without layout file $mockPluginManager = Mockery::mock(PluginManager::class); $mockPlugin = $this->createMockPlugin([ 'identifier' => 'test-no-layout', 'settingsLayout' => '/nonexistent/path/settings.json', ]); $mockPluginManager->shouldReceive('getPlugin') ->with('test-no-layout') ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $result = $service->getLayout('test-no-layout'); $this->assertNull($result); } /** * 실제 Daum 우편번호 플러그인 레이아웃 조회 테스트 */ public function test_get_layout_returns_layout_for_daum_postcode_plugin(): void { // Daum 우편번호 플러그인이 설치되어 있는지 확인 $pluginPath = base_path('plugins/sirsoft-daum_postcode/resources/layouts/settings.json'); if (! file_exists($pluginPath)) { $this->markTestSkipped('Daum postcode plugin layout file not found'); } // Mock PluginManager $mockPluginManager = Mockery::mock(PluginManager::class); $mockPlugin = $this->createMockPlugin([ 'identifier' => 'sirsoft-daum_postcode', 'settingsLayout' => $pluginPath, ]); $mockPluginManager->shouldReceive('getPlugin') ->with('sirsoft-daum_postcode') ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $result = $service->getLayout('sirsoft-daum_postcode'); $this->assertIsArray($result); $this->assertArrayHasKey('version', $result); $this->assertArrayHasKey('meta', $result); $this->assertArrayHasKey('schema', $result); } /** * 레이아웃 파일 파싱 실패 시 null 반환 */ public function test_get_layout_returns_null_on_invalid_json(): void { // 임시 잘못된 JSON 파일 생성 $tempPath = sys_get_temp_dir().'/invalid_layout_'.uniqid().'.json'; file_put_contents($tempPath, 'invalid json content {{{'); $mockPluginManager = Mockery::mock(PluginManager::class); $mockPlugin = $this->createMockPlugin([ 'identifier' => 'test-invalid-json', 'settingsLayout' => $tempPath, ]); $mockPluginManager->shouldReceive('getPlugin') ->with('test-invalid-json') ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $result = $service->getLayout('test-invalid-json'); $this->assertNull($result); // 임시 파일 삭제 @unlink($tempPath); } /** * 레이아웃이 sanitize 되는지 확인 (components, data_sources 대상) */ public function test_get_layout_sanitizes_layout_content(): void { // 임시 레이아웃 파일 생성 (components에 XSS 포함) $tempPath = sys_get_temp_dir().'/xss_layout_'.uniqid().'.json'; $layoutContent = json_encode([ 'version' => '1.0.0', 'meta' => [ 'title' => 'Test Plugin Settings', ], 'schema' => [ 'field1' => [ 'type' => 'string', 'label' => 'API Key', ], ], 'components' => [ [ 'type' => 'basic', 'name' => 'Div', 'props' => [ 'onClick' => 'javascript:alert("XSS")', ], ], ], ]); file_put_contents($tempPath, $layoutContent); $mockPluginManager = Mockery::mock(PluginManager::class); $mockPlugin = $this->createMockPlugin([ 'identifier' => 'test-xss-plugin', 'settingsLayout' => $tempPath, ]); $mockPluginManager->shouldReceive('getPlugin') ->with('test-xss-plugin') ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $result = $service->getLayout('test-xss-plugin'); $this->assertIsArray($result); // 레이아웃이 정상적으로 반환되는지 확인 $this->assertArrayHasKey('version', $result); $this->assertArrayHasKey('meta', $result); $this->assertArrayHasKey('schema', $result); // components의 props에서 javascript: 프로토콜이 제거되었는지 확인 $this->assertArrayHasKey('components', $result); $this->assertStringNotContainsString('javascript:', $result['components'][0]['props']['onClick'] ?? ''); // 임시 파일 삭제 @unlink($tempPath); } // ======================================================================== // 암호화/복호화 테스트 // ======================================================================== /** * 민감한 필드가 암호화되어 저장되는지 확인 */ public function test_save_encrypts_sensitive_fields(): void { $identifier = 'test-encrypt-plugin-'.uniqid(); // Mock plugin with sensitive field schema $mockPluginManager = Mockery::mock(PluginManager::class); $mockPlugin = $this->createMockPlugin([ 'identifier' => $identifier, 'configValues' => [], 'settingsSchema' => [ 'api_key' => [ 'type' => 'string', 'sensitive' => true, ], 'public_key' => [ 'type' => 'string', 'sensitive' => false, ], ], 'storage' => [ 'exists' => false, 'put' => true, ], ]); $mockPluginManager->shouldReceive('getPlugin') ->with($identifier) ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $result = $service->save($identifier, [ 'api_key' => 'secret-api-key', 'public_key' => 'public-value', ]); $this->assertTrue($result); // 암호화 로직은 내부적으로 동작하며, put이 호출되었으므로 성공으로 간주 } /** * 민감한 필드가 복호화되어 조회되는지 확인 */ public function test_get_decrypts_sensitive_fields(): void { $identifier = 'test-decrypt-plugin-'.uniqid(); // 암호화된 값으로 설정 파일 내용 생성 $encryptedValue = Crypt::encryptString('decrypted-secret'); $fileContent = json_encode([ 'secret_key' => $encryptedValue, 'normal_key' => 'normal-value', ]); // Mock plugin with sensitive field schema and Storage $mockPluginManager = Mockery::mock(PluginManager::class); $mockPlugin = $this->createMockPlugin([ 'identifier' => $identifier, 'configValues' => [], 'settingsSchema' => [ 'secret_key' => [ 'type' => 'string', 'sensitive' => true, ], 'normal_key' => [ 'type' => 'string', 'sensitive' => false, ], ], 'storage' => [ 'exists' => true, 'get' => $fileContent, ], ]); $mockPluginManager->shouldReceive('getPlugin') ->with($identifier) ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $result = $service->get($identifier); // 민감한 필드는 복호화되어 반환됨 $this->assertEquals('decrypted-secret', $result['secret_key']); // 비민감 필드는 그대로 반환됨 $this->assertEquals('normal-value', $result['normal_key']); } /** * 복호화 실패 시 원래 값 유지 (레거시 데이터 호환) */ public function test_get_preserves_value_on_decrypt_failure(): void { $identifier = 'test-legacy-plugin-'.uniqid(); // 암호화되지 않은 레거시 값으로 설정 파일 내용 생성 $fileContent = json_encode([ 'old_secret' => 'plain-text-secret', // 암호화되지 않은 레거시 값 ]); // Mock plugin with sensitive field schema $mockPluginManager = Mockery::mock(PluginManager::class); $mockPlugin = $this->createMockPlugin([ 'identifier' => $identifier, 'configValues' => [], 'settingsSchema' => [ 'old_secret' => [ 'type' => 'string', 'sensitive' => true, ], ], 'storage' => [ 'exists' => true, 'get' => $fileContent, ], ]); $mockPluginManager->shouldReceive('getPlugin') ->with($identifier) ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $result = $service->get($identifier); // 복호화 실패 시 원래 값 유지 $this->assertEquals('plain-text-secret', $result['old_secret']); } /** * 이미 암호화된 값은 다시 암호화하지 않음 */ public function test_save_does_not_double_encrypt(): void { $identifier = 'test-double-encrypt-plugin-'.uniqid(); $originalValue = 'original-secret'; $encryptedOnce = Crypt::encryptString($originalValue); // Mock plugin with sensitive field schema $mockPluginManager = Mockery::mock(PluginManager::class); $mockPlugin = $this->createMockPlugin([ 'identifier' => $identifier, 'configValues' => [], 'settingsSchema' => [ 'api_key' => [ 'type' => 'string', 'sensitive' => true, ], ], 'storage' => [ 'exists' => false, 'put' => true, ], ]); $mockPluginManager->shouldReceive('getPlugin') ->with($identifier) ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); // 이미 암호화된 값을 저장 $result = $service->save($identifier, [ 'api_key' => $encryptedOnce, ]); $this->assertTrue($result); // 암호화 로직은 내부적으로 동작하며, put이 호출되었으므로 성공으로 간주 } /** * reset() 메서드가 설정 파일을 삭제하는지 확인 */ public function test_reset_deletes_settings_file(): void { $identifier = 'test-reset-plugin-'.uniqid(); $mockPluginManager = Mockery::mock(PluginManager::class); $mockPlugin = $this->createMockPlugin([ 'identifier' => $identifier, 'storage' => [ 'exists' => true, 'delete' => true, ], ]); $mockPluginManager->shouldReceive('getPlugin') ->with($identifier) ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $result = $service->reset($identifier); $this->assertTrue($result); } /** * clearCache() 메서드가 캐시를 초기화하는지 확인 */ public function test_clear_cache_clears_settings_cache(): void { $identifier = 'test-cache-plugin'; $mockPluginManager = Mockery::mock(PluginManager::class); $mockPlugin = $this->createMockPlugin([ 'identifier' => $identifier, 'configValues' => ['initial' => 'value'], 'settingsSchema' => [], ]); $mockPluginManager->shouldReceive('getPlugin') ->with($identifier) ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); // 첫 번째 조회 (캐시에 저장됨) $service->get($identifier); // 캐시 초기화 $service->clearCache($identifier); // 캐시가 초기화되었으므로 다시 getPlugin이 호출되어야 함 // (이 테스트는 내부 동작을 확인하는 것이 어려우므로 예외 없이 실행되는지만 확인) $this->assertTrue(true); } // ======================================================================== // getAllActiveSettings() 메서드 테스트 // ======================================================================== /** * frontend_schema가 있는 defaults.json이 있을 때 스키마 기반 필터링 */ public function test_get_all_active_settings_uses_frontend_schema_when_defaults_exists(): void { $identifier = 'test-frontend-schema-plugin'; // defaults.json 임시 파일 생성 $tempDefaultsPath = sys_get_temp_dir().'/defaults_'.uniqid().'.json'; file_put_contents($tempDefaultsPath, json_encode([ 'defaults' => [ 'display_mode' => 'layer', 'popup_width' => 500, 'theme_color' => '#1D4ED8', ], 'frontend_schema' => [ 'display_mode' => ['expose' => true], 'popup_width' => ['expose' => true], 'theme_color' => ['expose' => false], ], ])); $mockPlugin = $this->createMockPlugin([ 'identifier' => $identifier, 'hasSettings' => true, 'configValues' => [ 'display_mode' => 'layer', 'popup_width' => 500, 'theme_color' => '#1D4ED8', ], 'settingsSchema' => [], 'settingsDefaultsPath' => $tempDefaultsPath, ]); $mockPluginManager = Mockery::mock(PluginManager::class); $mockPluginManager->shouldReceive('getActivePlugins') ->andReturn([$mockPlugin]); $mockPluginManager->shouldReceive('getPlugin') ->with($identifier) ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $result = $service->getAllActiveSettings(); $this->assertArrayHasKey($identifier, $result); $this->assertArrayHasKey('display_mode', $result[$identifier]); $this->assertArrayHasKey('popup_width', $result[$identifier]); $this->assertArrayNotHasKey('theme_color', $result[$identifier]); @unlink($tempDefaultsPath); } /** * defaults.json이 없을 때 기존 동작 유지 (sensitive만 제외) */ public function test_get_all_active_settings_falls_back_to_exclude_sensitive_when_no_defaults(): void { $identifier = 'test-no-defaults-plugin'; $mockPlugin = $this->createMockPlugin([ 'identifier' => $identifier, 'hasSettings' => true, 'configValues' => [ 'display_mode' => 'popup', 'api_key' => 'secret-key', ], 'settingsSchema' => [ 'display_mode' => ['type' => 'string'], 'api_key' => ['type' => 'string', 'sensitive' => true], ], 'settingsDefaultsPath' => null, ]); $mockPluginManager = Mockery::mock(PluginManager::class); $mockPluginManager->shouldReceive('getActivePlugins') ->andReturn([$mockPlugin]); $mockPluginManager->shouldReceive('getPlugin') ->with($identifier) ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $result = $service->getAllActiveSettings(); $this->assertArrayHasKey($identifier, $result); $this->assertArrayHasKey('display_mode', $result[$identifier]); $this->assertArrayNotHasKey('api_key', $result[$identifier]); } /** * hasSettings()=false인 플러그인은 결과에 미포함 */ public function test_get_all_active_settings_skips_plugins_without_settings(): void { $mockPlugin = $this->createMockPlugin([ 'identifier' => 'no-settings-plugin', 'hasSettings' => false, 'configValues' => [], 'settingsSchema' => [], ]); $mockPluginManager = Mockery::mock(PluginManager::class); $mockPluginManager->shouldReceive('getActivePlugins') ->andReturn([$mockPlugin]); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $result = $service->getAllActiveSettings(); $this->assertEmpty($result); } /** * frontend_schema로 카테고리 구조 필터링 (카테고리 expose:false → 전체 제외) */ public function test_get_all_active_settings_filters_by_category_expose(): void { $identifier = 'test-category-expose-plugin'; // 카테고리 구조 설정을 가진 defaults.json $tempDefaultsPath = sys_get_temp_dir().'/defaults_cat_'.uniqid().'.json'; file_put_contents($tempDefaultsPath, json_encode([ 'defaults' => [ 'general' => ['site_name' => 'Test'], 'secrets' => ['api_key' => 'hidden'], ], 'frontend_schema' => [ 'general' => ['expose' => true], 'secrets' => ['expose' => false], ], ])); $mockPlugin = $this->createMockPlugin([ 'identifier' => $identifier, 'hasSettings' => true, 'configValues' => [ 'general' => ['site_name' => 'Test'], 'secrets' => ['api_key' => 'hidden'], ], 'settingsSchema' => [], 'settingsDefaultsPath' => $tempDefaultsPath, ]); $mockPluginManager = Mockery::mock(PluginManager::class); $mockPluginManager->shouldReceive('getActivePlugins') ->andReturn([$mockPlugin]); $mockPluginManager->shouldReceive('getPlugin') ->with($identifier) ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $result = $service->getAllActiveSettings(); $this->assertArrayHasKey($identifier, $result); $this->assertArrayHasKey('general', $result[$identifier]); $this->assertArrayNotHasKey('secrets', $result[$identifier]); @unlink($tempDefaultsPath); } /** * frontend_schema로 필드 레벨 필터링 (필드 expose:false → 해당 필드만 제외) */ public function test_get_all_active_settings_filters_by_field_expose(): void { $identifier = 'test-field-expose-plugin'; $tempDefaultsPath = sys_get_temp_dir().'/defaults_field_'.uniqid().'.json'; file_put_contents($tempDefaultsPath, json_encode([ 'defaults' => [ 'payment' => [ 'currency' => 'KRW', 'secret_key' => 'sk_xxx', 'public_key' => 'pk_xxx', ], ], 'frontend_schema' => [ 'payment' => [ 'expose' => true, 'fields' => [ 'currency' => ['expose' => true], 'secret_key' => ['expose' => false], 'public_key' => ['expose' => true], ], ], ], ])); $mockPlugin = $this->createMockPlugin([ 'identifier' => $identifier, 'hasSettings' => true, 'configValues' => [ 'payment' => [ 'currency' => 'KRW', 'secret_key' => 'sk_xxx', 'public_key' => 'pk_xxx', ], ], 'settingsSchema' => [], 'settingsDefaultsPath' => $tempDefaultsPath, ]); $mockPluginManager = Mockery::mock(PluginManager::class); $mockPluginManager->shouldReceive('getActivePlugins') ->andReturn([$mockPlugin]); $mockPluginManager->shouldReceive('getPlugin') ->with($identifier) ->andReturn($mockPlugin); $service = new PluginSettingsService( $mockPluginManager, $this->templateManager, $this->layoutService ); $result = $service->getAllActiveSettings(); $this->assertArrayHasKey($identifier, $result); $this->assertArrayHasKey('payment', $result[$identifier]); $this->assertSame('KRW', $result[$identifier]['payment']['currency']); $this->assertSame('pk_xxx', $result[$identifier]['payment']['public_key']); $this->assertArrayNotHasKey('secret_key', $result[$identifier]['payment']); @unlink($tempDefaultsPath); } protected function tearDown(): void { Mockery::close(); parent::tearDown(); } }