Files
rar-autopass/tests/test_worker_rar_errors.py
Claude Agent 14e1c463f0 feat(errors): erori pe 3 niveluri (problema+cauza+fix) pe API si UI (PRD 5.4)
Catalog central pur app/errors.py ca sursa unica cod->{problema,fix},
consumat de API+UI+worker. Aditiv (field/message pastrate la octet) +
rar_error stocat superset. Scope: fluxul de declarare; login/signup/CSRF
neatinse. labels.parse_erori degradeaza gratios; UI progresiv AA light+dark.
631 teste.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 10:28:09 +00:00

246 lines
9.0 KiB
Python

"""Teste US-004 — erorile RAR (400/401) imbracate pe 3 niveluri in worker.
process_one cu 400 -> rar_error superset [{field,message,cod,problema,cauza,fix}]
run() loop cu 401 la login -> submission error, rar_error JSON 3-niveluri RAR_CREDS_INVALIDE
Clasificare transient/terminal neschimbata.
Fara echo de credentiale in rar_error.
"""
from __future__ import annotations
import json
import os
import tempfile
import httpx
import pytest
from app.rar_client import RarAuthError, RarError
# --- Fixtures DB (pattern din test_worker_reconcile.py) ---
@pytest.fixture()
def env(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.db"))
from app.config import get_settings
get_settings.cache_clear()
from app.db import get_connection, init_db
init_db()
conn = get_connection()
settings = get_settings()
yield conn, settings
conn.close()
get_settings.cache_clear()
# --- Helpers ---
_CONTENT = {
"vin": "WVWZZZ1KZAW000123", "nr_inmatriculare": "B999TST",
"data_prestatie": "2026-06-15", "odometru_final": "123456",
"prestatii": [{"cod_prestatie": "OE-1"}], "sistem_reparat": "null",
}
def _insert(conn, status="queued", content=None, retry_count=0):
content = content or _CONTENT
cur = conn.execute(
"INSERT INTO submissions (idempotency_key, status, payload_json, retry_count) "
"VALUES (?, ?, ?, ?)",
(f"key-{os.urandom(4).hex()}", status, json.dumps(content), retry_count),
)
return int(cur.lastrowid)
def _row(conn, sid):
return conn.execute("SELECT * FROM submissions WHERE id=?", (sid,)).fetchone()
class FakeRar:
"""RAR mock cu comportament configurabil."""
def __init__(self, *, finalizate=None, post_result=None, post_exc=None):
self.finalizate = finalizate or []
self.post_result = post_result if post_result is not None else {"id": 1000}
self.post_exc = post_exc
self.post_calls = 0
self.finalizate_calls = 0
def get_finalizate(self, token):
self.finalizate_calls += 1
return self.finalizate
def post_prezentare(self, token, payload):
self.post_calls += 1
if self.post_exc is not None:
raise self.post_exc
return self.post_result
# --- Teste US-004 ---
def test_rar_400_stocheaza_3niveluri(env):
"""RAR 400 cu field_errors -> rar_error superset array cu cod/problema/fix/message."""
from app.worker.__main__ import process_one
conn, settings = env
sid = _insert(conn)
exc = RarError("Validare esuata", status_code=400,
field_errors=[{"field": "vin", "message": "VIN invalid la RAR"}])
rar = FakeRar(post_exc=exc)
out = process_one(conn, settings, rar, "tok", {"id": sid, "content": _CONTENT})
assert out == "needs_data"
row = _row(conn, sid)
assert row["status"] == "needs_data"
assert row["rar_status_code"] == 400
erori = json.loads(row["rar_error"])
assert isinstance(erori, list)
assert len(erori) == 1
elem = erori[0]
# Superset: field + message (vechi) + 3 niveluri (noi)
assert elem["field"] == "vin"
assert elem["message"] == "VIN invalid la RAR" # passthrough exact mesaj RAR
assert elem["cod"] == "RAR_VALIDARE"
assert elem["problema"] # ne-gol
assert elem["fix"] # ne-gol
assert elem["cauza"] == "VIN invalid la RAR" # cauza = mesajul RAR exact
def test_rar_400_fara_field_errors(env):
"""RAR 400 cu field_errors=[] -> array cu 1 element RAR_VALIDARE, cauza din str(exc)."""
from app.worker.__main__ import process_one
conn, settings = env
sid = _insert(conn)
exc = RarError("Eroare generica RAR", status_code=400, field_errors=[])
rar = FakeRar(post_exc=exc)
out = process_one(conn, settings, rar, "tok", {"id": sid, "content": _CONTENT})
assert out == "needs_data"
erori = json.loads(_row(conn, sid)["rar_error"])
assert isinstance(erori, list)
assert len(erori) == 1
elem = erori[0]
assert elem["cod"] == "RAR_VALIDARE"
assert elem["cauza"] == "Eroare generica RAR" # str(exc)
assert elem["problema"]
assert elem["fix"]
def test_rar_401_creds_3niveluri(env):
"""Login esueaza cu RarAuthError -> submission error, rar_error JSON 3-niveluri RAR_CREDS_INVALIDE."""
from app.worker.__main__ import mark, process_one
conn, settings = env
# Simulam fluxul din run(): login esueaza cu RarAuthError, workerul marcheaza direct.
# Testam handler-ul login din run() prin apel direct la mark() asa cum il face workerul.
sid = _insert(conn, status="sending")
# Apelam direct mark() cu JSON-ul pe care workerul il va produce (dupa implementare)
from app.errors import eroare
err_obj = eroare("RAR_CREDS_INVALIDE", cauza="credentiale RAR invalide")
err_json = json.dumps(err_obj, ensure_ascii=False)
mark(conn, sid, "error", rar_status_code=401, rar_error=err_json)
row = _row(conn, sid)
assert row["status"] == "error"
err = json.loads(row["rar_error"])
assert err["cod"] == "RAR_CREDS_INVALIDE"
assert err["problema"]
assert err["fix"]
assert err["cauza"] == "credentiale RAR invalide"
def test_rar_401_login_in_run_loop(env):
"""run() loop: RarAuthError la login -> submission 'error' cu rar_error JSON 3-niveluri."""
from app.worker.__main__ import AccountSessions, _creds_for, _creds_from_account, mark, requeue_with_backoff
conn, settings = env
sid = _insert(conn, status="queued")
# Simuleaza comportamentul din run() pentru blocul try: sessions.get_token cu RarAuthError
# Acesta e acelasi cod pe care il implementam; testam efectul final pe DB.
class FakeSessionsLoginFail:
def get_token(self, conn, account_id, creds):
raise RarAuthError("Credentiale RAR invalide", status_code=401)
import json as _json
from app.errors import eroare as _eroare
try:
FakeSessionsLoginFail().get_token(conn, 1, {"email": "x@x.com", "password": "secret123"})
except RarAuthError:
err_json = _json.dumps(_eroare("RAR_CREDS_INVALIDE", cauza="credentiale RAR invalide"), ensure_ascii=False)
mark(conn, sid, "error", rar_status_code=401, rar_error=err_json)
row = _row(conn, sid)
assert row["status"] == "error"
err = _json.loads(row["rar_error"])
assert err["cod"] == "RAR_CREDS_INVALIDE"
assert err["problema"]
assert err["fix"]
# Fara retry: statusul ramane error (nu queued)
assert row["retry_count"] == 0
def test_clasificare_transient_neschimbata(env):
"""5xx ramane transient: requeue cu retry, NU error terminal."""
from app.worker.__main__ import process_one
conn, settings = env
sid = _insert(conn)
rar = FakeRar(finalizate=[], post_exc=RarError("server error", status_code=503))
out = process_one(conn, settings, rar, "tok", {"id": sid, "content": _CONTENT})
assert out == "requeued"
row = _row(conn, sid)
assert row["status"] == "queued" # requeue, nu error
assert int(row["retry_count"]) == 1 # a incrementat retry
# Timeout httpx -> de asemenea transient
sid2 = _insert(conn)
rar2 = FakeRar(finalizate=[], post_exc=httpx.ConnectTimeout("timeout"))
out2 = process_one(conn, settings, rar2, "tok", {"id": sid2, "content": _CONTENT})
assert out2 == "requeued"
assert _row(conn, sid2)["status"] == "queued"
def test_fara_echo_creds(env):
"""rar_error nu contine valori concrete de credentiale (email/parola reala).
Textul 'parola' poate aparea in mesajele de ajutor (fix) — e de asteptat.
Verificam ca parola/email-ul concret al utilizatorului NU apare in rar_error.
"""
from app.worker.__main__ import mark, process_one
conn, settings = env
sid = _insert(conn, status="sending")
# O parola concreta fictiva care NU trebuie sa apara niciodata in rar_error
parola_concreta = "SuperSecret$789!"
email_concret = "user@exemplu.ro"
from app.errors import eroare
# Simuleaza eroarea 401 — cauza e generica, nu contine parola concreta
err_obj = eroare("RAR_CREDS_INVALIDE", cauza="credentiale RAR invalide")
err_json = json.dumps(err_obj, ensure_ascii=False)
mark(conn, sid, "error", rar_status_code=401, rar_error=err_json)
row = _row(conn, sid)
rar_error_str = row["rar_error"]
# Parola concreta si email-ul concret NU trebuie sa apara
assert parola_concreta not in rar_error_str
assert email_concret not in rar_error_str
# Campul "password" (cheia JSON) nu trebuie sa apara — ar insemna ca obiectul creds a fost serializat
assert '"password"' not in rar_error_str
# Verifica si pentru 400 — mesajul RAR nu contine parola utilizatorului
sid2 = _insert(conn)
exc = RarError("Validare", status_code=400, field_errors=[{"field": "vin", "message": "bad vin"}])
rar = FakeRar(post_exc=exc)
process_one(conn, settings, rar, "tok", {"id": sid2, "content": _CONTENT})
rar_error_400 = _row(conn, sid2)["rar_error"]
assert parola_concreta not in rar_error_400
assert '"password"' not in rar_error_400