Files
office_translator/tests/test_providers/test_deepl_provider.py
Sepehr Ramezani 26bd096a06 feat: production deployment - full update with providers, admin, glossaries, pricing, tests
Major changes across backend, frontend, infrastructure:
- Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud)
- Admin panel: user management, pricing, settings
- Glossary system with CSV import/export
- Subscription and tier quota management
- Security hardening (rate limiting, API key auth, path traversal fixes)
- Docker compose for dev, prod, and IONOS deployment
- Alembic migrations for new tables
- Frontend: dashboard, pricing page, landing page, i18n (en/fr)
- Test suite and verification scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-25 15:01:47 +02:00

489 lines
18 KiB
Python

"""
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()