fix(relecture): un travail vide ou oublie restait affiche et impossible a retirer
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m29s
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)
This commit is contained in:
144
tests/test_delete_translation_history.py
Normal file
144
tests/test_delete_translation_history.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user