fix(translate): French error messages and update mock users for quota checks
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m52s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m52s
This commit is contained in:
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user