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