All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 3m16s
654 lines
23 KiB
Python
654 lines
23 KiB
Python
"""
|
|
Tests pour GET /api/v1/download/{job_id}
|
|
Couvre les AC 1-6 de la story 2.12 : Telechargement Fichier Traduit
|
|
"""
|
|
|
|
import io
|
|
from pathlib import Path
|
|
from unittest.mock import patch, MagicMock
|
|
from zipfile import ZipFile
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
TRANSLATE_URL = "/api/v1/translate"
|
|
STATUS_URL = "/api/v1/translations"
|
|
DOWNLOAD_URL = "/api/v1/download"
|
|
REGISTER_URL = "/api/v1/auth/register"
|
|
LOGIN_URL = "/api/v1/auth/login"
|
|
|
|
VALID_USER = {
|
|
"email": "download@example.com",
|
|
"password": "Password123!",
|
|
"name": "Download User",
|
|
}
|
|
|
|
|
|
def create_valid_excel() -> bytes:
|
|
"""Create a minimal valid .xlsx file (ZIP with office content)."""
|
|
buf = io.BytesIO()
|
|
with ZipFile(buf, "w") as zf:
|
|
zf.writestr(
|
|
"[Content_Types].xml",
|
|
'<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"></Types>',
|
|
)
|
|
zf.writestr(
|
|
"_rels/.rels",
|
|
'<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>',
|
|
)
|
|
zf.writestr(
|
|
"xl/workbook.xml",
|
|
'<?xml version="1.0"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"></workbook>',
|
|
)
|
|
buf.seek(0)
|
|
return buf.read()
|
|
|
|
|
|
def create_valid_docx() -> bytes:
|
|
"""Create a minimal valid .docx file."""
|
|
buf = io.BytesIO()
|
|
with ZipFile(buf, "w") as zf:
|
|
zf.writestr(
|
|
"[Content_Types].xml",
|
|
'<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"></Types>',
|
|
)
|
|
zf.writestr(
|
|
"_rels/.rels",
|
|
'<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>',
|
|
)
|
|
zf.writestr(
|
|
"word/document.xml",
|
|
'<?xml version="1.0"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"></w:document>',
|
|
)
|
|
buf.seek(0)
|
|
return buf.read()
|
|
|
|
|
|
def create_valid_pptx() -> bytes:
|
|
"""Create a minimal valid .pptx file."""
|
|
buf = io.BytesIO()
|
|
with ZipFile(buf, "w") as zf:
|
|
zf.writestr(
|
|
"[Content_Types].xml",
|
|
'<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"></Types>',
|
|
)
|
|
zf.writestr(
|
|
"_rels/.rels",
|
|
'<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>',
|
|
)
|
|
zf.writestr(
|
|
"ppt/presentation.xml",
|
|
'<?xml version="1.0"?><p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"></p:presentation>',
|
|
)
|
|
buf.seek(0)
|
|
return buf.read()
|
|
|
|
|
|
@pytest.fixture()
|
|
def users_file(tmp_path: Path) -> Path:
|
|
"""Fichier de stockage JSON isole pour les tests."""
|
|
return tmp_path / "users.json"
|
|
|
|
|
|
@pytest.fixture()
|
|
def client(users_file: Path, monkeypatch):
|
|
"""TestClient avec stockage JSON isole et rate limiting desactive."""
|
|
import services.auth_service as auth_svc
|
|
|
|
monkeypatch.setattr(auth_svc, "USERS_FILE", users_file)
|
|
monkeypatch.setattr(auth_svc, "USE_DATABASE", False)
|
|
monkeypatch.setattr(auth_svc, "DATABASE_AVAILABLE", False)
|
|
|
|
from middleware.rate_limiting import RateLimitManager
|
|
|
|
async def _check_request_allow(self, request):
|
|
return True, "ok", "test"
|
|
|
|
async def _check_translation_allow(self, request, file_size_mb=0):
|
|
return True, "ok"
|
|
|
|
monkeypatch.setattr(RateLimitManager, "check_request", _check_request_allow)
|
|
monkeypatch.setattr(RateLimitManager, "check_translation", _check_translation_allow)
|
|
|
|
def _check_usage_limits_allow(user):
|
|
return {
|
|
"can_translate": True,
|
|
"docs_used": 0,
|
|
"docs_limit": 5,
|
|
"docs_remaining": 5,
|
|
"pages_used": 0,
|
|
"extra_credits": 0,
|
|
"max_pages_per_doc": 50,
|
|
"max_file_size_mb": 10,
|
|
"allowed_providers": ["google", "deepl"],
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
"routes.translate_routes.check_usage_limits", _check_usage_limits_allow
|
|
)
|
|
|
|
from main import app
|
|
|
|
return TestClient(app, raise_server_exceptions=True)
|
|
|
|
|
|
@pytest.fixture()
|
|
def authenticated_client(client):
|
|
"""Client avec un utilisateur enregistre et authentifie."""
|
|
client.post(REGISTER_URL, json=VALID_USER)
|
|
response = client.post(
|
|
LOGIN_URL,
|
|
json={
|
|
"email": VALID_USER["email"],
|
|
"password": VALID_USER["password"],
|
|
},
|
|
)
|
|
token = response.json()["data"]["access_token"]
|
|
client.headers["Authorization"] = f"Bearer {token}"
|
|
return client
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# AC1: Download Endpoint
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestDownloadEndpoint:
|
|
"""AC1: GET /api/v1/download/{id} returns translated file as binary download"""
|
|
|
|
def test_returns_400_for_invalid_job_id_format(self, authenticated_client):
|
|
"""Invalid job_id format returns 400 with INVALID_JOB_ID"""
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/invalid_format")
|
|
assert response.status_code == 400
|
|
body = response.json()
|
|
assert body["error"] == "INVALID_JOB_ID"
|
|
|
|
def test_returns_400_for_job_id_with_special_chars(self, authenticated_client):
|
|
"""Job ID with special chars returns 400"""
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/tr_invalid@#$%")
|
|
assert response.status_code == 400
|
|
body = response.json()
|
|
assert body["error"] == "INVALID_JOB_ID"
|
|
|
|
def test_returns_404_for_non_existent_job(self, authenticated_client):
|
|
"""AC4: Non-existent job returns 404 with FILE_EXPIRED"""
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/tr_nonexistent123")
|
|
assert response.status_code == 404
|
|
body = response.json()
|
|
assert body["error"] == "FILE_EXPIRED"
|
|
|
|
def test_returns_404_for_job_without_output_path(self, authenticated_client):
|
|
"""AC4: Job without output_path returns 404 with FILE_EXPIRED"""
|
|
from routes import translate_routes
|
|
|
|
job_id = "tr_test_no_output"
|
|
translate_routes._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "completed",
|
|
"file_name": "test.xlsx",
|
|
"output_path": None,
|
|
}
|
|
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/{job_id}")
|
|
assert response.status_code == 404
|
|
body = response.json()
|
|
assert body["error"] == "FILE_EXPIRED"
|
|
|
|
def test_returns_404_for_file_deleted_from_disk(
|
|
self, authenticated_client, tmp_path
|
|
):
|
|
"""AC4: Job with output_path but file missing from disk returns 404"""
|
|
from routes import translate_routes
|
|
|
|
nonexistent_file = tmp_path / "deleted_file.xlsx"
|
|
|
|
job_id = "tr_deleted_disk"
|
|
translate_routes._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "completed",
|
|
"file_name": "deleted.xlsx",
|
|
"file_extension": ".xlsx",
|
|
"output_path": str(nonexistent_file),
|
|
}
|
|
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/{job_id}")
|
|
assert response.status_code == 404
|
|
body = response.json()
|
|
assert body["error"] == "FILE_EXPIRED"
|
|
assert body["details"]["status"] == "file_deleted"
|
|
|
|
def test_returns_404_for_non_completed_job(self, authenticated_client):
|
|
"""AC5: Non-completed jobs return 404 with NOT_READY"""
|
|
from routes import translate_routes
|
|
|
|
job_id = "tr_test_processing"
|
|
translate_routes._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "processing",
|
|
"progress_percent": 50,
|
|
"file_name": "test.xlsx",
|
|
}
|
|
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/{job_id}")
|
|
assert response.status_code == 404
|
|
body = response.json()
|
|
assert body["error"] == "NOT_READY"
|
|
|
|
def test_returns_404_for_queued_job(self, authenticated_client):
|
|
"""AC5: Queued jobs return 404 with NOT_READY"""
|
|
from routes import translate_routes
|
|
|
|
job_id = "tr_test_queued"
|
|
translate_routes._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "queued",
|
|
"file_name": "test.xlsx",
|
|
}
|
|
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/{job_id}")
|
|
assert response.status_code == 404
|
|
body = response.json()
|
|
assert body["error"] == "NOT_READY"
|
|
|
|
def test_returns_404_for_failed_job(self, authenticated_client):
|
|
"""AC5: Failed jobs return 404 with NOT_READY"""
|
|
from routes import translate_routes
|
|
|
|
job_id = "tr_test_failed"
|
|
translate_routes._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "failed",
|
|
"error_message": "Something went wrong",
|
|
"file_name": "test.xlsx",
|
|
}
|
|
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/{job_id}")
|
|
assert response.status_code == 404
|
|
body = response.json()
|
|
assert body["error"] == "NOT_READY"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# AC2: Content-Disposition Header
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestContentDisposition:
|
|
"""AC2: Header includes original filename with "_translated" suffix"""
|
|
|
|
def test_content_disposition_has_translated_suffix(
|
|
self, authenticated_client, tmp_path
|
|
):
|
|
"""Content-Disposition includes _translated suffix"""
|
|
from routes import translate_routes
|
|
|
|
output_file = tmp_path / "test_translated.xlsx"
|
|
output_file.write_bytes(create_valid_excel())
|
|
|
|
job_id = "tr_test_disposition"
|
|
translate_routes._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "completed",
|
|
"file_name": "report.xlsx",
|
|
"file_extension": ".xlsx",
|
|
"output_path": str(output_file),
|
|
}
|
|
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/{job_id}")
|
|
assert response.status_code == 200
|
|
content_disp = response.headers.get("content-disposition", "")
|
|
assert "attachment" in content_disp
|
|
assert "report_translated.xlsx" in content_disp
|
|
|
|
def test_content_disposition_for_docx(self, authenticated_client, tmp_path):
|
|
"""Content-Disposition works for .docx files"""
|
|
from routes import translate_routes
|
|
|
|
output_file = tmp_path / "doc_translated.docx"
|
|
output_file.write_bytes(create_valid_docx())
|
|
|
|
job_id = "tr_test_docx"
|
|
translate_routes._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "completed",
|
|
"file_name": "document.docx",
|
|
"file_extension": ".docx",
|
|
"output_path": str(output_file),
|
|
}
|
|
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/{job_id}")
|
|
assert response.status_code == 200
|
|
content_disp = response.headers.get("content-disposition", "")
|
|
assert "document_translated.docx" in content_disp
|
|
|
|
def test_content_disposition_for_pptx(self, authenticated_client, tmp_path):
|
|
"""Content-Disposition works for .pptx files"""
|
|
from routes import translate_routes
|
|
|
|
output_file = tmp_path / "ppt_translated.pptx"
|
|
output_file.write_bytes(create_valid_pptx())
|
|
|
|
job_id = "tr_test_pptx"
|
|
translate_routes._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "completed",
|
|
"file_name": "presentation.pptx",
|
|
"file_extension": ".pptx",
|
|
"output_path": str(output_file),
|
|
}
|
|
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/{job_id}")
|
|
assert response.status_code == 200
|
|
content_disp = response.headers.get("content-disposition", "")
|
|
assert "presentation_translated.pptx" in content_disp
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# AC3: Immediate File Deletion (tested via BackgroundTask)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestFileDeletion:
|
|
"""AC3: File is deleted immediately after successful download"""
|
|
|
|
def test_file_deleted_after_download(self, authenticated_client, tmp_path):
|
|
"""File should be deleted after download completes"""
|
|
from routes import translate_routes
|
|
|
|
output_file = tmp_path / "to_delete.xlsx"
|
|
output_file.write_bytes(create_valid_excel())
|
|
|
|
input_file = tmp_path / "input_file.xlsx"
|
|
input_file.write_bytes(create_valid_excel())
|
|
|
|
job_id = "tr_test_delete"
|
|
translate_routes._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "completed",
|
|
"file_name": "to_delete.xlsx",
|
|
"file_extension": ".xlsx",
|
|
"output_path": str(output_file),
|
|
"input_path": str(input_file),
|
|
}
|
|
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/{job_id}")
|
|
assert response.status_code == 200
|
|
|
|
import time
|
|
|
|
time.sleep(0.5)
|
|
|
|
assert not output_file.exists(), "Output file should be deleted after download"
|
|
assert not input_file.exists(), "Input file should be deleted after download"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# AC6: Correct MIME Types
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestMIMETypes:
|
|
"""AC6: Content-Type set correctly for each format"""
|
|
|
|
def test_mime_type_xlsx(self, authenticated_client, tmp_path):
|
|
"""xlsx returns correct MIME type"""
|
|
from routes import translate_routes
|
|
|
|
output_file = tmp_path / "test.xlsx"
|
|
output_file.write_bytes(create_valid_excel())
|
|
|
|
job_id = "tr_test_mime_xlsx"
|
|
translate_routes._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "completed",
|
|
"file_name": "test.xlsx",
|
|
"file_extension": ".xlsx",
|
|
"output_path": str(output_file),
|
|
}
|
|
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/{job_id}")
|
|
assert response.status_code == 200
|
|
content_type = response.headers.get("content-type", "")
|
|
assert (
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
in content_type
|
|
)
|
|
|
|
def test_mime_type_docx(self, authenticated_client, tmp_path):
|
|
"""docx returns correct MIME type"""
|
|
from routes import translate_routes
|
|
|
|
output_file = tmp_path / "test.docx"
|
|
output_file.write_bytes(create_valid_docx())
|
|
|
|
job_id = "tr_test_mime_docx"
|
|
translate_routes._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "completed",
|
|
"file_name": "test.docx",
|
|
"file_extension": ".docx",
|
|
"output_path": str(output_file),
|
|
}
|
|
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/{job_id}")
|
|
assert response.status_code == 200
|
|
content_type = response.headers.get("content-type", "")
|
|
assert (
|
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
in content_type
|
|
)
|
|
|
|
def test_mime_type_pptx(self, authenticated_client, tmp_path):
|
|
"""pptx returns correct MIME type"""
|
|
from routes import translate_routes
|
|
|
|
output_file = tmp_path / "test.pptx"
|
|
output_file.write_bytes(create_valid_pptx())
|
|
|
|
job_id = "tr_test_mime_pptx"
|
|
translate_routes._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "completed",
|
|
"file_name": "test.pptx",
|
|
"file_extension": ".pptx",
|
|
"output_path": str(output_file),
|
|
}
|
|
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/{job_id}")
|
|
assert response.status_code == 200
|
|
content_type = response.headers.get("content-type", "")
|
|
assert (
|
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
in content_type
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# AC4: File Expired/Not Found
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestFileExpired:
|
|
"""AC4: If translation not found, expired, or output_path missing, returns 404"""
|
|
|
|
def test_file_expired_message_in_french(self, authenticated_client):
|
|
"""Error message should be in French"""
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/tr_nonexistent")
|
|
assert response.status_code == 404
|
|
body = response.json()
|
|
assert body["error"] == "FILE_EXPIRED"
|
|
assert (
|
|
"non disponible" in body["message"].lower()
|
|
or "expire" in body["message"].lower()
|
|
)
|
|
|
|
def test_not_ready_message_in_french(self, authenticated_client):
|
|
"""NOT_READY error message should be in French"""
|
|
from routes import translate_routes
|
|
|
|
job_id = "tr_test_not_ready_msg"
|
|
translate_routes._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "processing",
|
|
"progress_percent": 30,
|
|
"file_name": "test.xlsx",
|
|
}
|
|
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/{job_id}")
|
|
assert response.status_code == 404
|
|
body = response.json()
|
|
assert body["error"] == "NOT_READY"
|
|
assert "cours" in body["message"].lower() or "encore" in body["message"].lower()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Integration: Full flow
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestDownloadIntegration:
|
|
"""Integration tests for download flow"""
|
|
|
|
def test_download_returns_binary_content(self, authenticated_client, tmp_path):
|
|
"""Download returns actual binary content"""
|
|
from routes import translate_routes
|
|
|
|
content = create_valid_excel()
|
|
output_file = tmp_path / "binary_test.xlsx"
|
|
output_file.write_bytes(content)
|
|
|
|
job_id = "tr_test_binary"
|
|
translate_routes._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "completed",
|
|
"file_name": "binary_test.xlsx",
|
|
"file_extension": ".xlsx",
|
|
"output_path": str(output_file),
|
|
"user_id": None,
|
|
}
|
|
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/{job_id}")
|
|
assert response.status_code == 200
|
|
assert len(response.content) > 0
|
|
assert response.content[:2] == b"PK"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Error Details
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestErrorDetails:
|
|
"""Test that error responses include proper details"""
|
|
|
|
def test_file_expired_includes_job_id_in_details(self, authenticated_client):
|
|
"""FILE_EXPIRED includes job_id in details"""
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/tr_nonexistent999")
|
|
assert response.status_code == 404
|
|
body = response.json()
|
|
assert body["error"] == "FILE_EXPIRED"
|
|
assert "details" in body
|
|
assert body["details"]["job_id"] == "tr_nonexistent999"
|
|
|
|
def test_not_ready_includes_status_in_details(self, authenticated_client):
|
|
"""NOT_READY includes status in details"""
|
|
from routes import translate_routes
|
|
|
|
job_id = "tr_test_details"
|
|
translate_routes._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "processing",
|
|
"progress_percent": 45,
|
|
"file_name": "test.xlsx",
|
|
}
|
|
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/{job_id}")
|
|
assert response.status_code == 404
|
|
body = response.json()
|
|
assert body["error"] == "NOT_READY"
|
|
assert "details" in body
|
|
assert body["details"]["status"] == "processing"
|
|
assert body["details"]["progress_percent"] == 45
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Authorization Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestDownloadAuthorization:
|
|
"""Test authorization for download endpoint"""
|
|
|
|
def test_user_cannot_download_other_users_file(self, client, tmp_path):
|
|
"""User cannot download file belonging to another user"""
|
|
from routes import translate_routes
|
|
|
|
output_file = tmp_path / "other_user_file.xlsx"
|
|
output_file.write_bytes(create_valid_excel())
|
|
|
|
job_id = "tr_other_user123"
|
|
translate_routes._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "completed",
|
|
"file_name": "other.xlsx",
|
|
"file_extension": ".xlsx",
|
|
"output_path": str(output_file),
|
|
"user_id": "different_user_id_456",
|
|
}
|
|
|
|
client.post(REGISTER_URL, json=VALID_USER)
|
|
response = client.post(
|
|
LOGIN_URL,
|
|
json={
|
|
"email": VALID_USER["email"],
|
|
"password": VALID_USER["password"],
|
|
},
|
|
)
|
|
token = response.json()["data"]["access_token"]
|
|
client.headers["Authorization"] = f"Bearer {token}"
|
|
|
|
response = client.get(f"{DOWNLOAD_URL}/{job_id}")
|
|
assert response.status_code == 403
|
|
body = response.json()
|
|
assert body["error"] == "ACCESS_DENIED"
|
|
|
|
def test_user_can_download_own_file(self, authenticated_client, tmp_path):
|
|
"""User can download their own file"""
|
|
from routes import translate_routes
|
|
|
|
output_file = tmp_path / "own_file.xlsx"
|
|
output_file.write_bytes(create_valid_excel())
|
|
|
|
job_id = "tr_own_file123"
|
|
translate_routes._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "completed",
|
|
"file_name": "own.xlsx",
|
|
"file_extension": ".xlsx",
|
|
"output_path": str(output_file),
|
|
"user_id": None,
|
|
}
|
|
|
|
response = authenticated_client.get(f"{DOWNLOAD_URL}/{job_id}")
|
|
assert response.status_code == 200
|
|
|
|
def test_anonymous_user_can_download_public_job(self, client, tmp_path):
|
|
"""Anonymous users can download jobs without user_id (public)"""
|
|
from routes import translate_routes
|
|
|
|
output_file = tmp_path / "public_job.xlsx"
|
|
output_file.write_bytes(create_valid_excel())
|
|
|
|
job_id = "tr_public_job99"
|
|
translate_routes._translation_jobs[job_id] = {
|
|
"id": job_id,
|
|
"status": "completed",
|
|
"file_name": "public.xlsx",
|
|
"file_extension": ".xlsx",
|
|
"output_path": str(output_file),
|
|
"user_id": None,
|
|
}
|
|
|
|
response = client.get(f"{DOWNLOAD_URL}/{job_id}")
|
|
assert response.status_code == 200
|