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>
246 lines
9.0 KiB
Python
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
|