diff --git a/middleware/validation.py b/middleware/validation.py index 13af00d..deec6a5 100644 --- a/middleware/validation.py +++ b/middleware/validation.py @@ -287,16 +287,16 @@ class FileValidator: if extension.lower() == ".pdf": if not content.startswith(self.PDF_MAGIC_BYTES): raise ValidationError( - "File content does not match expected PDF format. " - "The file may be corrupted.", + "Le contenu du fichier ne correspond pas au format PDF attendu. " + "Le fichier est peut-être corrompu.", code="invalid_file_content", ) return # Office files are ZIP-based if not content.startswith(self.OFFICE_MAGIC_BYTES): raise ValidationError( - "File content does not match expected Office format. " - "The file may be corrupted or not a valid Office document.", + "Le contenu du fichier ne correspond pas au format Office attendu. " + "Le fichier est peut-être corrompu ou n'est pas un document Office valide.", code="invalid_file_content", ) diff --git a/routes/translate_routes.py b/routes/translate_routes.py index 8a9a7e3..d69c158 100644 --- a/routes/translate_routes.py +++ b/routes/translate_routes.py @@ -100,15 +100,15 @@ class TranslateEndpointError(Exception): PRO_FEATURE_REQUIRED = "PRO_FEATURE_REQUIRED" ERROR_MESSAGES = { - INVALID_FORMAT: "Unsupported file format. Accepted formats: .xlsx, .docx, .pptx", - CORRUPTED_FILE: "The file appears corrupted or is not a valid Office document.", - FILE_TOO_LARGE: f"File is too large (max {MAX_FILE_SIZE_MB} MB).", - QUOTA_EXCEEDED: "Monthly translation limit reached.", - URL_DOWNLOAD_FAILED: "Failed to download file from URL.", - URL_UNREACHABLE: "URL unreachable.", - UNAUTHORIZED: "Authentication required.", - MISSING_FILE: "File or URL required.", - PRO_FEATURE_REQUIRED: "This feature requires a Pro subscription.", + INVALID_FORMAT: "Format de fichier non pris en charge. Formats acceptés : .xlsx, .docx, .pptx", + CORRUPTED_FILE: "Le fichier semble corrompu ou n'est pas un document Office valide.", + FILE_TOO_LARGE: f"Le fichier est trop volumineux (max {MAX_FILE_SIZE_MB} Mo).", + QUOTA_EXCEEDED: "Limite mensuelle de traduction atteinte.", + URL_DOWNLOAD_FAILED: "Échec du téléchargement du fichier depuis l'URL.", + URL_UNREACHABLE: "URL inaccessible.", + UNAUTHORIZED: "Authentification requise.", + MISSING_FILE: "Fichier ou URL requis.", + PRO_FEATURE_REQUIRED: "Cette fonctionnalité nécessite un abonnement Pro.", } def __init__( @@ -169,8 +169,8 @@ async def validate_file_content(content: bytes, extension: str) -> None: if len(content) < 4: raise TranslateEndpointError( code=TranslateEndpointError.CORRUPTED_FILE, - message="File is too small to be a valid document.", - details={"reason": "File is too small"}, + message="Le fichier est trop petit pour être un document valide.", + details={"reason": "Fichier trop petit"}, ) header = content[:5] @@ -179,8 +179,8 @@ async def validate_file_content(content: bytes, extension: str) -> None: if not header[:4] == PDF_MAGIC_BYTES: raise TranslateEndpointError( code=TranslateEndpointError.CORRUPTED_FILE, - message="File is not a valid PDF.", - details={"reason": "Invalid PDF header"}, + message="Le fichier n'est pas un PDF valide.", + details={"reason": "En-tête PDF invalide"}, ) return @@ -188,10 +188,10 @@ async def validate_file_content(content: bytes, extension: str) -> None: if header[:4] != OFFICE_MAGIC_BYTES: raise TranslateEndpointError( code=TranslateEndpointError.CORRUPTED_FILE, - message="File is not a valid Office document.", + message="Le fichier n'est pas un document Office valide ou est corrompu.", details={ "accepted_formats": list(ACCEPTED_EXTENSIONS), - "hint": "Office files (.xlsx, .docx, .pptx) must be valid ZIP archives.", + "hint": "Les fichiers Office (.xlsx, .docx, .pptx) doivent être des archives ZIP valides.", }, ) diff --git a/tests/test_story_2_13_url_validation.py b/tests/test_story_2_13_url_validation.py index 50ac81c..d5a3d92 100644 --- a/tests/test_story_2_13_url_validation.py +++ b/tests/test_story_2_13_url_validation.py @@ -38,13 +38,28 @@ def create_valid_excel() -> bytes: return buf.read() -client = TestClient(app) +@pytest.fixture() +def client(monkeypatch): + """TestClient with rate limiting and quota reservation bypassed for URL validation tests.""" + from middleware.rate_limiting import RateLimitMiddleware + + async def _dispatch(self, request, call_next): + return await call_next(request) + + monkeypatch.setattr(RateLimitMiddleware, "dispatch", _dispatch) + monkeypatch.setattr("routes.translate_routes.reserve_translation_quota", lambda user_id: True) + from main import app + + return TestClient(app) class MockUser: def __init__(self): self.id = "user_abc123" self.plan = "pro" + self.docs_translated_this_month = 0 + self.pages_translated_this_month = 0 + self.extra_credits = 0 async def mock_get_authenticated_user(): @@ -72,7 +87,7 @@ def create_mock_client_with_stream(mock_response): @pytest.mark.asyncio -async def test_validate_file_url_invalid_format(): +async def test_validate_file_url_invalid_format(client): """Test that invalid file format (.txt) returns INVALID_FORMAT error.""" app.dependency_overrides[get_authenticated_user] = mock_get_authenticated_user try: @@ -94,7 +109,7 @@ async def test_validate_file_url_invalid_format(): @pytest.mark.asyncio -async def test_validate_file_url_corrupted_magic_bytes(): +async def test_validate_file_url_corrupted_magic_bytes(client): """Test that corrupted file (invalid magic bytes) returns CORRUPTED_FILE error.""" app.dependency_overrides[get_authenticated_user] = mock_get_authenticated_user try: diff --git a/tests/test_story_2_16_url_ingestion.py b/tests/test_story_2_16_url_ingestion.py index f6f1cca..58b3041 100644 --- a/tests/test_story_2_16_url_ingestion.py +++ b/tests/test_story_2_16_url_ingestion.py @@ -77,12 +77,18 @@ class MockProUser: def __init__(self): self.id = "pro_user_123" self.plan = "pro" + self.docs_translated_this_month = 0 + self.pages_translated_this_month = 0 + self.extra_credits = 0 class MockFreeUser: def __init__(self): self.id = "free_user_123" self.plan = "free" + self.docs_translated_this_month = 0 + self.pages_translated_this_month = 0 + self.extra_credits = 0 async def mock_get_pro_user(): diff --git a/tests/test_translation_metadata_integration.py b/tests/test_translation_metadata_integration.py index fc3bc27..16cc8cb 100644 --- a/tests/test_translation_metadata_integration.py +++ b/tests/test_translation_metadata_integration.py @@ -5,13 +5,28 @@ from unittest.mock import patch, AsyncMock, MagicMock from main import app from routes.translate_routes import get_authenticated_user -client = TestClient(app) +@pytest.fixture() +def client(monkeypatch): + """TestClient with rate limiting and quota reservation bypassed for metadata tests.""" + from middleware.rate_limiting import RateLimitMiddleware + + async def _dispatch(self, request, call_next): + return await call_next(request) + + monkeypatch.setattr(RateLimitMiddleware, "dispatch", _dispatch) + monkeypatch.setattr("routes.translate_routes.reserve_translation_quota", lambda user_id: True) + from main import app + + return TestClient(app) class MockUser: def __init__(self, user_id="user_123"): self.id = user_id self.plan = "free" + self.docs_translated_this_month = 0 + self.pages_translated_this_month = 0 + self.extra_credits = 0 async def mock_auth(): @@ -19,7 +34,7 @@ async def mock_auth(): @pytest.mark.asyncio -async def test_translate_endpoint_triggers_tracking(): +async def test_translate_endpoint_triggers_tracking(client): app.dependency_overrides[get_authenticated_user] = mock_auth with patch( @@ -70,7 +85,7 @@ async def test_translate_endpoint_triggers_tracking(): @pytest.mark.asyncio -async def test_translate_endpoint_handles_hash_failure(): +async def test_translate_endpoint_handles_hash_failure(client): app.dependency_overrides[get_authenticated_user] = mock_auth with patch("routes.translate_routes.file_validator.validate_async") as mock_val: