""" Tests for the DeepLTranslationProvider. """ import socket import pytest from concurrent.futures import TimeoutError as FuturesTimeoutError from unittest.mock import patch, MagicMock from services.providers.deepl_provider import ( DeepLTranslationProvider, DeepLProviderError, get_deepl_provider, register_deepl_provider, DEEPL_QUOTA_EXCEEDED, DEEPL_INVALID_KEY, DEEPL_NETWORK_ERROR, DEEPL_UNSUPPORTED_LANGUAGE, DEEPL_TEXT_TOO_LONG, ) from services.providers.schemas import TranslationRequest, TranslationResponse class TestDeepLProviderError: """Tests for DeepLProviderError exception.""" def test_error_creation(self): """Test error creation with all fields.""" error = DeepLProviderError( code=DEEPL_INVALID_KEY, message="Invalid API key", details={"provider": "deepl"}, ) assert error.code == DEEPL_INVALID_KEY assert error.message == "Invalid API key" assert error.details == {"provider": "deepl"} def test_error_to_dict(self): """Test error serialization.""" error = DeepLProviderError( code=DEEPL_QUOTA_EXCEEDED, message="Quota exceeded", details={"reset_at": "2024-01-16T00:00:00Z"}, ) result = error.to_dict() assert result["error"] == DEEPL_QUOTA_EXCEEDED assert result["message"] == "Quota exceeded" assert result["details"]["reset_at"] == "2024-01-16T00:00:00Z" def test_error_to_dict_no_details(self): """Test error serialization without details.""" error = DeepLProviderError( code=DEEPL_NETWORK_ERROR, message="Network error", ) result = error.to_dict() assert result["error"] == DEEPL_NETWORK_ERROR assert result["message"] == "Network error" assert "details" not in result class TestDeepLTranslationProvider: """Tests for DeepLTranslationProvider.""" @pytest.fixture def provider(self): """Create a DeepL provider instance with Pro key.""" return DeepLTranslationProvider( api_key="test-pro-key-12345", use_cache=False, ) @pytest.fixture def provider_free(self): """Create a DeepL provider instance with Free tier key.""" return DeepLTranslationProvider( api_key="test-free-key:fx", use_cache=False, ) def test_init_requires_api_key(self): """Test that initialization requires API key.""" with pytest.raises(ValueError, match="API key is required"): DeepLTranslationProvider(api_key="") def test_get_name(self, provider): """Test provider name.""" assert provider.get_name() == "deepl" def test_detect_api_type_pro(self, provider): """Test Pro API key detection.""" assert provider._api_type == "pro" def test_detect_api_type_free(self, provider_free): """Test Free API key detection.""" assert provider_free._api_type == "free" def test_get_api_url_pro(self, provider): """Test Pro API URL.""" url = provider._get_api_url() assert url == "https://api.deepl.com/v2/translate" def test_get_api_url_free(self, provider_free): """Test Free API URL.""" url = provider_free._get_api_url() assert url == "https://api-free.deepl.com/v2/translate" def test_normalize_language_code_uppercase(self, provider): """Test language code normalization to uppercase.""" assert provider._normalize_language_code("en") == "EN-US" assert provider._normalize_language_code("fr") == "FR" assert provider._normalize_language_code("pt") == "PT-BR" def test_normalize_language_code_preserves_variant(self, provider): """Test that language variants are preserved.""" assert provider._normalize_language_code("en-gb") == "EN-GB" assert provider._normalize_language_code("en-us") == "EN-US" assert provider._normalize_language_code("pt-pt") == "PT-PT" def test_normalize_language_code_auto(self, provider): """Test auto language code handling.""" assert provider._normalize_language_code("auto") == "" assert provider._normalize_language_code("") == "" def test_is_language_supported(self, provider): """Test language support checking.""" assert provider._is_language_supported("en") is True assert provider._is_language_supported("fr") is True assert provider._is_language_supported("EN-US") is True assert provider._is_language_supported("XX") is False def test_translate_text_empty(self, provider): """Test translating empty text.""" request = TranslationRequest(text="", target_language="fr") response = provider.translate_text(request) assert response.translated_text == "" assert response.provider_name == "deepl" assert response.from_cache is False def test_translate_text_whitespace(self, provider): """Test translating whitespace-only text.""" request = TranslationRequest(text=" ", target_language="fr") response = provider.translate_text(request) assert response.translated_text == " " @patch("services.providers.deepl_provider.DeepLTranslationProvider._get_translator") def test_translate_text_success(self, mock_get_translator, provider): """Test successful translation.""" mock_translator = MagicMock() mock_translator.translate.return_value = "Bonjour" mock_get_translator.return_value = mock_translator request = TranslationRequest(text="Hello", target_language="fr") response = provider.translate_text(request) assert response.translated_text == "Bonjour" assert response.provider_name == "deepl" assert response.from_cache is False @patch("services.providers.deepl_provider.DeepLTranslationProvider._get_translator") def test_translate_text_with_source_language(self, mock_get_translator, provider): """Test translation with explicit source language.""" mock_translator = MagicMock() mock_translator.translate.return_value = "Bonjour" mock_get_translator.return_value = mock_translator request = TranslationRequest( text="Hello", target_language="fr", source_language="en" ) response = provider.translate_text(request) assert response.translated_text == "Bonjour" def test_translate_text_same_language_skip(self, provider): """Test that translation is skipped when source == target.""" request = TranslationRequest( text="Hello", target_language="en", source_language="en", ) response = provider.translate_text(request) assert response.translated_text == "Hello" assert response.from_cache is False @patch("services.providers.deepl_provider.DeepLTranslationProvider._get_translator") def test_translate_text_error_fallback(self, mock_get_translator, provider): """Test that translation errors return original text and structured error.""" mock_get_translator.side_effect = Exception("API Error") request = TranslationRequest(text="Hello", target_language="fr") response = provider.translate_text(request) assert response.translated_text == "Hello" assert response.provider_name == "deepl" assert response.error is not None assert response.error_code is not None def test_translate_batch_empty(self, provider): """Test batch translation with empty list.""" responses = provider.translate_batch([]) assert responses == [] @patch.object(DeepLTranslationProvider, "translate_text") def test_translate_batch(self, mock_translate, provider): """Test batch translation.""" mock_translate.side_effect = [ TranslationResponse(translated_text="Bonjour", provider_name="deepl"), TranslationResponse(translated_text="Monde", provider_name="deepl"), ] requests = [ TranslationRequest(text="Hello", target_language="fr"), TranslationRequest(text="World", target_language="fr"), ] responses = provider.translate_batch(requests) assert len(responses) == 2 assert responses[0].translated_text == "Bonjour" assert responses[1].translated_text == "Monde" def test_health_check(self, provider): """Test health check.""" status = provider.health_check() assert status.name == "deepl" assert isinstance(status.available, bool) assert status.latency_ms is not None class TestDeepLErrorCodes: """Tests for DeepL error code handling.""" @pytest.fixture def provider(self): """Create a DeepL provider instance.""" return DeepLTranslationProvider( api_key="test-key-12345", use_cache=False, max_retries=0, ) @patch("services.providers.deepl_provider.DeepLTranslationProvider._get_translator") def test_quota_exceeded_error(self, mock_get_translator, provider): """Test quota exceeded error handling.""" mock_get_translator.side_effect = Exception("quota exceeded") request = TranslationRequest(text="Hello", target_language="fr") response = provider.translate_text(request) assert response.error_code == DEEPL_QUOTA_EXCEEDED assert "quota" in response.error.lower() or "dépassé" in response.error.lower() @patch("services.providers.deepl_provider.DeepLTranslationProvider._get_translator") def test_invalid_key_error(self, mock_get_translator, provider): """Test invalid API key error handling.""" mock_get_translator.side_effect = Exception("403 Forbidden - invalid auth") request = TranslationRequest(text="Hello", target_language="fr") response = provider.translate_text(request) assert response.error_code == DEEPL_INVALID_KEY @patch("services.providers.deepl_provider.DeepLTranslationProvider._get_translator") def test_unsupported_language_error(self, mock_get_translator, provider): """Test unsupported language error handling.""" mock_get_translator.side_effect = Exception("language not supported") request = TranslationRequest(text="Hello", target_language="fr") response = provider.translate_text(request) assert response.error_code == DEEPL_UNSUPPORTED_LANGUAGE def test_text_too_long_error(self, provider): """Test text too long error handling.""" long_text = "x" * (200 * 1024) request = TranslationRequest(text=long_text, target_language="fr") response = provider.translate_text(request) assert response.error_code == DEEPL_TEXT_TOO_LONG assert response.error_details is not None assert "text_length" in response.error_details or "max_length" in response.error_details @patch("services.providers.deepl_provider.DeepLTranslationProvider._get_translator") def test_timeout_exception_maps_to_network_error(self, mock_get_translator, provider): """Test that socket.timeout and FuturesTimeoutError map to DEEPL_NETWORK_ERROR.""" mock_get_translator.side_effect = FuturesTimeoutError() request = TranslationRequest(text="Hello", target_language="fr") response = provider.translate_text(request) assert response.error_code == DEEPL_NETWORK_ERROR assert response.error is not None @patch("services.providers.deepl_provider.DeepLTranslationProvider._get_translator") def test_socket_timeout_maps_to_network_error(self, mock_get_translator, provider): """Test that socket.timeout maps to DEEPL_NETWORK_ERROR.""" mock_get_translator.side_effect = socket.timeout("timed out") request = TranslationRequest(text="Hello", target_language="fr") response = provider.translate_text(request) assert response.error_code == DEEPL_NETWORK_ERROR class TestDeepLProviderCaching: """Tests for DeepL provider caching functionality.""" @pytest.fixture def mock_cache(self): """Create a mock cache.""" cache = MagicMock() cache.get.return_value = None return cache def test_cache_hit(self, mock_cache): """Test that cache hits return cached result.""" mock_cache.get.return_value = "Cached Translation" provider = DeepLTranslationProvider( api_key="test-key-12345", use_cache=True, ) provider._cache = mock_cache request = TranslationRequest(text="Hello", target_language="fr") response = provider.translate_text(request) assert response.translated_text == "Cached Translation" assert response.from_cache is True @patch("services.providers.deepl_provider.DeepLTranslationProvider._get_translator") def test_cache_set_on_miss(self, mock_get_translator, mock_cache): """Test that translations are cached on miss.""" mock_translator = MagicMock() mock_translator.translate.return_value = "Bonjour" mock_get_translator.return_value = mock_translator provider = DeepLTranslationProvider( api_key="test-key-12345", use_cache=True, ) provider._cache = mock_cache request = TranslationRequest(text="Hello", target_language="fr") provider.translate_text(request) mock_cache.set.assert_called_once() class TestDeepLProviderRetry: """Tests for DeepL provider retry logic.""" @pytest.fixture def provider(self): """Create a DeepL provider with retry enabled.""" return DeepLTranslationProvider( api_key="test-key-12345", use_cache=False, max_retries=2, retry_delay=0.01, ) @patch("services.providers.deepl_provider.DeepLTranslationProvider._get_translator") def test_retry_on_network_error(self, mock_get_translator, provider): """Test that network errors trigger retry.""" mock_translator = MagicMock() mock_translator.translate.side_effect = [ Exception("timeout"), "Bonjour", ] mock_get_translator.return_value = mock_translator request = TranslationRequest(text="Hello", target_language="fr") response = provider.translate_text(request) assert response.translated_text == "Bonjour" assert mock_translator.translate.call_count == 2 @patch("services.providers.deepl_provider.DeepLTranslationProvider._get_translator") def test_no_retry_on_invalid_key(self, mock_get_translator, provider): """Test that invalid key errors do not trigger retry.""" mock_translator = MagicMock() mock_translator.translate.side_effect = Exception("401 invalid auth") mock_get_translator.return_value = mock_translator request = TranslationRequest(text="Hello", target_language="fr") provider.translate_text(request) assert mock_translator.translate.call_count == 1 class TestDeepLProviderSingleton: """Tests for DeepL provider singleton functions.""" def test_get_deepl_provider_no_config(self): """Test get_deepl_provider returns None without config.""" import services.providers.deepl_provider as deepl_module deepl_module._provider_instance = None with patch("services.providers.config.ProvidersConfig") as mock_config: mock_config.DEEPL_API_KEY = "" result = deepl_module.get_deepl_provider() assert result is None def test_get_deepl_provider_with_config(self): """Test get_deepl_provider creates instance with config.""" import services.providers.deepl_provider as deepl_module deepl_module._provider_instance = None with patch("services.providers.config.ProvidersConfig") as mock_config: mock_config.DEEPL_API_KEY = "test-key:fx" mock_config.DEEPL_TIMEOUT = 30 mock_config.DEEPL_MAX_RETRIES = 3 mock_config.DEEPL_RETRY_DELAY = 1.0 provider = deepl_module.get_deepl_provider() assert provider is not None assert provider._api_type == "free" deepl_module._provider_instance = None class TestDeepLRegistryIntegration: """Tests for DeepL provider registry integration.""" def test_register_deepl_provider(self): """Test provider registration.""" from services.providers.registry import registry registry.unregister("deepl") with patch("services.providers.deepl_provider.get_deepl_provider") as mock_get: mock_provider = MagicMock() mock_get.return_value = mock_provider from services.providers.deepl_provider import register_deepl_provider result = register_deepl_provider() assert result == mock_provider assert "deepl" in registry registry.unregister("deepl") def test_register_deepl_provider_no_config(self): """Test provider registration when not configured.""" from services.providers.registry import registry registry.unregister("deepl") with patch("services.providers.deepl_provider.get_deepl_provider") as mock_get: mock_get.return_value = None from services.providers.deepl_provider import register_deepl_provider result = register_deepl_provider() assert result is None assert "deepl" not in registry class TestLegacyDeepLAdapter: """Tests for LegacyDeepLAdapter.""" def test_adapter_not_configured(self): """Test adapter when DeepL is not configured.""" with patch("services.providers.deepl_provider.get_deepl_provider") as mock_get: mock_get.return_value = None from services.providers.deepl_provider import LegacyDeepLAdapter adapter = LegacyDeepLAdapter() with pytest.raises(Exception) as exc_info: adapter.translate("Hello", "fr") assert "not configured" in str(exc_info.value).lower()