"""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