All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m29s
Un travail echoue ou expire cote serveur restait dans « Traductions
recentes » : sa page de relecture n'affichait rien (0 segments) avec une
erreur brute non traduite, et aucune suppression n'existait.
- serveur : nouvelle route DELETE /api/v1/translations/{job_id} —
retire le travail de la memoire et de Redis, supprime ses segments de
relecture ; idempotente (200 meme si le serveur avait deja oublie le
travail) ; refuse les travaux en cours (annuler d'abord) ; proprietaire
seul (404 pour le travail d'autrui, sans reveler son existence)
- interface : bouton corbeille sur chaque ligne des traductions recentes
(deux clics pour confirmer) qui nettoie la liste locale ET le serveur ;
l'historique serveur ne liste plus les travaux non termines
- page de relecture : message d'erreur traduit (fini le message brut) et
etat « rien a relire » explicite avec explication
- libelles ajoutes dans les 13 langues de l'interface
- 5 tests backend nouveaux (tests/test_delete_translation_history.py)
145 lines
4.9 KiB
Python
145 lines
4.9 KiB
Python
"""DELETE /api/v1/translations/{job_id} — retrait d'un travail de l'historique.
|
|
|
|
Contexte : un travail échoué ou oublié par le serveur (rétention 24 h en
|
|
mémoire) restait affiché dans « Traductions récentes » sans aucun moyen de
|
|
le retirer, et sa page de relecture était vide. La route supprime le
|
|
travail de la mémoire, sa clé Redis et ses segments de relecture.
|
|
"""
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from unittest.mock import patch
|
|
|
|
from main import app
|
|
from routes.translate_routes import get_authenticated_user, _translation_jobs
|
|
|
|
|
|
@pytest.fixture()
|
|
def client(monkeypatch):
|
|
from middleware.rate_limiting import RateLimitMiddleware
|
|
|
|
async def _dispatch(self, request, call_next):
|
|
return await call_next(request)
|
|
|
|
monkeypatch.setattr(RateLimitMiddleware, "dispatch", _dispatch)
|
|
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
|
|
|
|
|
|
def _make_job(job_id="tr_del1", user_id="user_123", status="completed"):
|
|
return {
|
|
"id": job_id,
|
|
"user_id": user_id,
|
|
"status": status,
|
|
"file_name": "doc.docx",
|
|
"created_at": "2026-09-01T10:00:00",
|
|
"progress_percent": 100,
|
|
}
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_jobs():
|
|
saved = dict(_translation_jobs)
|
|
_translation_jobs.clear()
|
|
yield
|
|
_translation_jobs.clear()
|
|
_translation_jobs.update(saved)
|
|
|
|
|
|
def _seed_segments(job_id, user_id="user_123", count=2):
|
|
from database.models import Base, TranslationSegment
|
|
from database.connection import sync_engine, get_sync_session
|
|
|
|
Base.metadata.create_all(bind=sync_engine)
|
|
with get_sync_session() as session:
|
|
for i in range(count):
|
|
session.add(
|
|
TranslationSegment(
|
|
job_id=job_id,
|
|
user_id=user_id,
|
|
segment_index=i,
|
|
source_text=f"Hello {i}",
|
|
translated_text=f"Bonjour {i}",
|
|
status="pending",
|
|
)
|
|
)
|
|
session.commit()
|
|
|
|
|
|
class TestDeleteTranslationHistory:
|
|
def test_owner_deletes_completed_job(self, client):
|
|
app.dependency_overrides[get_authenticated_user] = _async_user(MockUser())
|
|
_translation_jobs["tr_del1"] = _make_job()
|
|
_seed_segments("tr_del1")
|
|
|
|
res = client.delete("/api/v1/translations/tr_del1")
|
|
assert res.status_code == 200
|
|
body = res.json()["data"]
|
|
assert body["deleted"] is True
|
|
assert body["segments_removed"] == 2
|
|
assert "tr_del1" not in _translation_jobs
|
|
|
|
from database.connection import get_sync_session
|
|
from database.models import TranslationSegment
|
|
|
|
with get_sync_session() as session:
|
|
remaining = (
|
|
session.query(TranslationSegment)
|
|
.filter(TranslationSegment.job_id == "tr_del1")
|
|
.count()
|
|
)
|
|
assert remaining == 0
|
|
app.dependency_overrides.clear()
|
|
|
|
def test_delete_is_idempotent_when_job_already_gone(self, client):
|
|
app.dependency_overrides[get_authenticated_user] = _async_user(MockUser())
|
|
# Le serveur a oublié le travail (rétention expirée / redémarrage) :
|
|
# le client veut quand même nettoyer sa liste locale.
|
|
res = client.delete("/api/v1/translations/tr_unknown")
|
|
assert res.status_code == 200
|
|
assert res.json()["data"]["deleted"] is True
|
|
app.dependency_overrides.clear()
|
|
|
|
def test_other_users_job_is_404(self, client):
|
|
app.dependency_overrides[get_authenticated_user] = _async_user(MockUser())
|
|
_translation_jobs["tr_someone"] = _make_job("tr_someone", user_id="user_999")
|
|
res = client.delete("/api/v1/translations/tr_someone")
|
|
assert res.status_code == 404
|
|
# Le travail reste intact
|
|
assert "tr_someone" in _translation_jobs
|
|
app.dependency_overrides.clear()
|
|
|
|
def test_running_job_requires_cancel_first(self, client):
|
|
app.dependency_overrides[get_authenticated_user] = _async_user(MockUser())
|
|
_translation_jobs["tr_run"] = _make_job("tr_run", status="processing")
|
|
res = client.delete("/api/v1/translations/tr_run")
|
|
assert res.status_code == 409
|
|
assert res.json()["error"] == "JOB_IN_PROGRESS"
|
|
assert "tr_run" in _translation_jobs
|
|
app.dependency_overrides.clear()
|
|
|
|
def test_unauthenticated_is_401(self, client):
|
|
async def _anonymous():
|
|
return None
|
|
|
|
app.dependency_overrides[get_authenticated_user] = _anonymous
|
|
_translation_jobs["tr_anon"] = _make_job()
|
|
res = client.delete("/api/v1/translations/tr_anon")
|
|
assert res.status_code == 401
|
|
app.dependency_overrides.clear()
|
|
|
|
|
|
def _async_user(user):
|
|
async def _auth():
|
|
return user
|
|
|
|
return _auth
|