Files
office_translator/tests/test_providers/test_google_integration.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

423 lines
16 KiB
Python

"""
Integration tests for GoogleTranslationProvider.
Tests for error handling, retry logic, and health checks.
Uses mocking to simulate various API error scenarios.
"""
import pytest
from unittest.mock import patch, MagicMock
import time
from services.providers.google_provider import (
GoogleTranslationProvider,
GoogleProviderError,
GOOGLE_QUOTA_EXCEEDED,
GOOGLE_INVALID_KEY,
GOOGLE_NETWORK_ERROR,
GOOGLE_UNSUPPORTED_LANGUAGE,
GOOGLE_TEXT_TOO_LONG,
)
from services.providers.schemas import TranslationRequest
class TestGoogleProviderHealthCheck:
"""Tests for health check functionality."""
@pytest.fixture
def provider(self):
return GoogleTranslationProvider(use_cache=False)
def test_health_check_returns_status(self, provider):
"""Test health check returns ProviderHealthStatus."""
status = provider.health_check()
assert status.name == "google"
assert isinstance(status.available, bool)
assert status.latency_ms is not None
def test_health_check_includes_last_check_timestamp(self, provider):
"""Test health check includes last_check timestamp."""
status = provider.health_check()
assert status.last_check is not None
assert "T" in status.last_check # ISO format
def test_health_check_caches_result(self, provider):
"""Test health check result is cached for 60 seconds."""
status1 = provider.health_check()
status2 = provider.health_check()
assert status1.last_check == status2.last_check
def test_health_check_cache_ttl(self, provider):
"""Test health check cache expires after TTL."""
provider._health_cache_ttl = 0.1 # 100ms TTL for testing
status1 = provider.health_check()
time.sleep(0.15)
status2 = provider.health_check()
assert status1.last_check != status2.last_check
class TestGoogleProviderErrorCodes:
"""Tests for specific Google error codes."""
@pytest.fixture
def provider(self):
return GoogleTranslationProvider(use_cache=False)
def test_quota_exceeded_error(self, provider):
"""Test GOOGLE_QUOTA_EXCEEDED error on 429 response."""
request = TranslationRequest(text="Hello", target_language="fr")
with patch.object(provider, "_make_api_request") as mock_api:
mock_api.side_effect = GoogleProviderError(
code=GOOGLE_QUOTA_EXCEEDED,
message="Quota Google Translate dépassé. Réessayez demain.",
details={"reset_at": "2024-01-16T00:00:00Z"},
)
response = provider.translate_text(request)
assert response.error is not None
assert response.error_code == GOOGLE_QUOTA_EXCEEDED
assert (
"quota" in response.error.lower() or "dépassé" in response.error.lower()
)
def test_invalid_key_error(self, provider):
"""Test GOOGLE_INVALID_KEY error on 401 response."""
request = TranslationRequest(text="Hello", target_language="fr")
with patch.object(provider, "_make_api_request") as mock_api:
mock_api.side_effect = GoogleProviderError(
code=GOOGLE_INVALID_KEY,
message="Clé API Google invalide. Contactez l'administrateur.",
)
response = provider.translate_text(request)
assert response.error is not None
assert response.error_code == GOOGLE_INVALID_KEY
def test_network_error(self, provider):
"""Test GOOGLE_NETWORK_ERROR on timeout/connection error."""
request = TranslationRequest(text="Hello", target_language="fr")
with patch.object(provider, "_make_api_request") as mock_api:
mock_api.side_effect = GoogleProviderError(
code=GOOGLE_NETWORK_ERROR,
message="Service Google Translate indisponible. Réessayez.",
)
response = provider.translate_text(request)
assert response.error is not None
assert response.error_code == GOOGLE_NETWORK_ERROR
def test_unsupported_language_error(self, provider):
"""Test GOOGLE_UNSUPPORTED_LANGUAGE for invalid language."""
request = TranslationRequest(text="Hello", target_language="xx")
with patch.object(provider, "_make_api_request") as mock_api:
mock_api.side_effect = GoogleProviderError(
code=GOOGLE_UNSUPPORTED_LANGUAGE,
message="Langue 'xx' non supportée par Google.",
details={"unsupported_language": "xx"},
)
response = provider.translate_text(request)
assert response.error is not None
assert response.error_code == GOOGLE_UNSUPPORTED_LANGUAGE
def test_text_too_long_error(self, provider):
"""Test GOOGLE_TEXT_TOO_LONG for text exceeding limit."""
long_text = "x" * 5001
request = TranslationRequest(text=long_text, target_language="fr")
with patch.object(provider, "_make_api_request") as mock_api:
mock_api.side_effect = GoogleProviderError(
code=GOOGLE_TEXT_TOO_LONG,
message="Texte trop long (max 5000 caractères par requête).",
details={"text_length": 5001, "max_length": 5000},
)
response = provider.translate_text(request)
assert response.error is not None
assert response.error_code == GOOGLE_TEXT_TOO_LONG
class TestGoogleProviderRetryLogic:
"""Tests for retry logic with exponential backoff."""
@pytest.fixture
def provider(self):
return GoogleTranslationProvider(use_cache=False)
def test_retry_on_transient_error(self, provider):
"""Test that transient errors trigger retry."""
request = TranslationRequest(text="Hello", target_language="fr")
call_count = 0
def mock_api_call(*args, **kwargs):
nonlocal call_count
call_count += 1
if call_count < 3:
raise GoogleProviderError(
code=GOOGLE_NETWORK_ERROR, message="Temporary network error"
)
return "Bonjour"
with patch.object(provider, "_make_api_request", side_effect=mock_api_call):
response = provider.translate_text(request)
assert call_count == 3
assert response.translated_text == "Bonjour"
assert response.error is None
def test_max_retries_exceeded(self, provider):
"""Test that max retries is respected."""
request = TranslationRequest(text="Hello", target_language="fr")
with patch.object(provider, "_make_api_request") as mock_api:
mock_api.side_effect = GoogleProviderError(
code=GOOGLE_NETWORK_ERROR, message="Network error"
)
response = provider.translate_text(request)
assert response.error is not None
assert mock_api.call_count == 4 # Initial + 3 retries
def test_no_retry_on_invalid_key(self, provider):
"""Test that invalid key errors don't retry."""
request = TranslationRequest(text="Hello", target_language="fr")
with patch.object(provider, "_make_api_request") as mock_api:
mock_api.side_effect = GoogleProviderError(
code=GOOGLE_INVALID_KEY, message="Invalid key"
)
response = provider.translate_text(request)
assert response.error is not None
assert mock_api.call_count == 1 # No retries for auth errors
class TestGoogleProviderTimeout:
"""Tests for timeout configuration."""
@pytest.fixture
def provider(self):
return GoogleTranslationProvider(use_cache=False)
def test_default_timeout(self, provider):
"""Test default timeout is 30 seconds."""
assert provider.timeout == 30
def test_custom_timeout(self):
"""Test custom timeout configuration."""
provider = GoogleTranslationProvider(use_cache=False, timeout=60)
assert provider.timeout == 60
def test_timeout_raises_network_error(self, provider):
"""Test that timeout raises GOOGLE_NETWORK_ERROR."""
request = TranslationRequest(text="Hello", target_language="fr")
import socket
with patch.object(provider, "_make_api_request") as mock_api:
mock_api.side_effect = socket.timeout("Request timed out")
response = provider.translate_text(request)
assert response.error is not None
assert response.error_code == GOOGLE_NETWORK_ERROR
class TestGoogleProviderErrorFormat:
"""Tests for JSON error format compliance."""
@pytest.fixture
def provider(self):
return GoogleTranslationProvider(use_cache=False)
def test_error_response_format(self, provider):
"""Test that errors return JSON: {error, message, details?} format."""
request = TranslationRequest(text="Hello", target_language="fr")
with patch.object(provider, "_make_api_request") as mock_api:
mock_api.side_effect = GoogleProviderError(
code=GOOGLE_QUOTA_EXCEEDED,
message="Quota exceeded",
details={"reset_at": "2024-01-16T00:00:00Z"},
)
response = provider.translate_text(request)
assert response.error is not None
assert response.error_code is not None
error_dict = response.to_error_dict()
assert "error" in error_dict
assert "message" in error_dict
def test_error_no_document_content_in_response(self, provider):
"""Test that error response never contains document content."""
sensitive_text = "SENSITIVE_DATA_12345"
request = TranslationRequest(text=sensitive_text, target_language="fr")
with patch.object(provider, "_make_api_request") as mock_api:
mock_api.side_effect = GoogleProviderError(
code=GOOGLE_NETWORK_ERROR, message="Network error"
)
response = provider.translate_text(request)
assert sensitive_text not in str(response.error)
assert sensitive_text not in str(response.to_error_dict())
class TestGoogleProviderLogging:
"""Tests for structlog logging."""
@pytest.fixture
def provider(self):
return GoogleTranslationProvider(use_cache=False)
def test_error_logged_with_structlog(self, provider):
"""Test errors are logged with structlog (no document content)."""
request = TranslationRequest(text="Hello", target_language="fr")
with patch.object(provider, "_make_api_request") as mock_api:
mock_api.side_effect = GoogleProviderError(
code=GOOGLE_QUOTA_EXCEEDED, message="Quota exceeded"
)
with patch("services.providers.google_provider.logger") as mock_logger:
response = provider.translate_text(request)
assert mock_logger.error.called or mock_logger.warning.called
def test_log_contains_metadata_not_content(self, provider):
"""Test logs contain metadata (text_length) not content."""
request = TranslationRequest(text="Secret content", target_language="fr")
with patch.object(provider, "_make_api_request") as mock_api:
mock_api.side_effect = GoogleProviderError(
code=GOOGLE_NETWORK_ERROR, message="Network error"
)
with patch("services.providers.google_provider.logger") as mock_logger:
response = provider.translate_text(request)
if mock_logger.error.called:
call_args = str(mock_logger.error.call_args)
assert "Secret content" not in call_args
@pytest.mark.integration
class TestGoogleProviderRealAPI:
"""Integration tests with real Google Translate API (via deep_translator)."""
@pytest.fixture
def provider(self):
return GoogleTranslationProvider(use_cache=False)
def test_real_translation_en_to_fr(self, provider):
"""Test real translation from English to French."""
request = TranslationRequest(text="Hello", target_language="fr")
response = provider.translate_text(request)
assert response.error is None
assert response.translated_text.lower() in ["bonjour", "salut", "hello"]
assert response.provider_name == "google"
def test_real_translation_with_auto_detect(self, provider):
"""Test translation with automatic language detection."""
request = TranslationRequest(
text="Bonjour le monde", target_language="en", source_language="auto"
)
response = provider.translate_text(request)
assert response.error is None
assert (
"world" in response.translated_text.lower()
or "hello" in response.translated_text.lower()
)
def test_real_health_check(self, provider):
"""Test real health check."""
status = provider.health_check()
assert status.name == "google"
assert status.available is True
assert status.latency_ms is not None
assert status.last_check is not None
def test_real_batch_translation(self, provider):
"""Test real batch translation."""
requests = [
TranslationRequest(text="Hello", target_language="es"),
TranslationRequest(text="World", target_language="es"),
]
responses = provider.translate_batch(requests)
assert len(responses) == 2
assert all(r.error is None for r in responses)
assert "hola" in responses[0].translated_text.lower()
assert "mundo" in responses[1].translated_text.lower()
class TestGoogleProviderOptimization:
"""Tests for API usage optimization."""
@pytest.fixture
def provider(self):
return GoogleTranslationProvider(use_cache=False)
def test_skip_translation_same_language(self, provider):
"""Test translation is skipped when source == target."""
request = TranslationRequest(
text="Hello World", target_language="en", source_language="en"
)
response = provider.translate_text(request)
assert response.translated_text == "Hello World"
assert response.from_cache is False
def test_translation_not_skipped_auto_detect(self, provider):
"""Test translation is not skipped with auto-detect."""
request = TranslationRequest(
text="Bonjour", target_language="en", source_language="auto"
)
with patch.object(provider, "_make_api_request", return_value="Hello"):
response = provider.translate_text(request)
assert response.translated_text == "Hello"
provider._make_api_request.assert_called_once()
def test_usage_metrics_logged(self, provider):
"""Test that usage metrics are logged on success."""
request = TranslationRequest(text="Hello", target_language="fr")
with patch.object(provider, "_make_api_request", return_value="Bonjour"):
with patch("services.providers.google_provider.logger") as mock_logger:
response = provider.translate_text(request)
# Check that success was logged with metrics
success_calls = [
call
for call in mock_logger.info.call_args_list
if "google_translation_success" in str(call)
]
assert len(success_calls) > 0
log_msg = str(success_calls[0])
assert "chars=" in log_msg
assert "source_lang=" in log_msg