Files
office_translator/tests/test_delete_translation_history.py
sepehr 2abd0c0b26
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m29s
fix(relecture): un travail vide ou oublie restait affiche et impossible a retirer
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)
2026-09-01 21:22:05 +02:00

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