Files
rar-autopass/tests/test_web_erori.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

286 lines
10 KiB
Python

"""Teste US-006 (PRD 5.4): Componenta UI de eroare pe 3 niveluri.
Pasul A (RED): teste scrise inainte de implementare.
"""
from __future__ import annotations
import csv
import io
import json
import os
import re
import tempfile
import pytest
# ---------------------------------------------------------------------------
# Teste pure pentru parse_erori (fara HTTP)
# ---------------------------------------------------------------------------
from app.web.labels import parse_erori # noqa: E402
def test_parse_erori_array_3niveluri():
"""Array imbogatit cu cod/problema/cauza/fix -> lista cu toate 3 nivelurile."""
rar_error = json.dumps([
{
"field": "vin",
"cod": "VIN_FORMAT",
"problema": "VIN invalid",
"cauza": "VIN-ul are 16 caractere; RAR cere exact 17.",
"fix": "Verifica VIN-ul pe talon (pozitia E); 17 caractere majuscule.",
"message": "VIN-ul are 16 caractere; RAR cere exact 17.",
}
])
rezultat = parse_erori(rar_error)
assert len(rezultat) == 1
e = rezultat[0]
assert e["problema"] == "VIN invalid"
assert "17" in e["cauza"]
assert e["fix"] # non-gol
assert e.get("field") == "vin"
def test_parse_erori_unmapped():
"""Dict unmapped 3-niveluri (cod=COD_NEMAPAT) -> 1 element corect."""
rar_error = json.dumps({
"cod": "COD_NEMAPAT",
"problema": "Lipseste codul RAR al operatiei",
"cauza": "Codul OP-99 nu are mapare RAR.",
"fix": "Alege codul RAR pentru aceasta operatie in tab-ul Mapari.",
"unmapped": [{"cod_op_service": "OP-99", "denumire": "Operatie test"}],
})
rezultat = parse_erori(rar_error)
assert len(rezultat) == 1
e = rezultat[0]
assert e["problema"] == "Lipseste codul RAR al operatiei"
assert e["fix"]
def test_parse_erori_creds():
"""Dict cu cod=RAR_CREDS_INVALIDE -> 1 element corect."""
rar_error = json.dumps({
"cod": "RAR_CREDS_INVALIDE",
"problema": "Credentiale RAR invalide",
"cauza": "Autentificarea la RAR a esuat (401).",
"fix": "Verifica email-ul si parola contului RAR in tab-ul Cont.",
})
rezultat = parse_erori(rar_error)
assert len(rezultat) == 1
e = rezultat[0]
assert e["problema"] == "Credentiale RAR invalide"
assert e["fix"]
def test_parse_erori_forma_veche_si_corupt():
"""Forma veche [{ field, message }], string plain, None, invalid -> degradeaza fara exceptie."""
# Forma veche: lista cu field+message dar fara cod
vechi = json.dumps([{"field": "vin", "message": "VIN invalid"}])
r = parse_erori(vechi)
assert isinstance(r, list)
assert len(r) >= 1
# Nu arunca pentru niciun element
for e in r:
assert "problema" in e
# String plain
r2 = parse_erori("Eroare generica de la RAR")
assert isinstance(r2, list)
assert len(r2) >= 1
assert r2[0]["problema"]
# None
r3 = parse_erori(None)
assert isinstance(r3, list)
assert r3 == []
# JSON corupt
r4 = parse_erori("{invalid json[[[")
assert isinstance(r4, list)
assert len(r4) >= 1
# Nu arunca
# Dict fara cod (forma veche dict)
r5 = parse_erori(json.dumps({"auto_send": "cod-abc", "motiv": "auto_send oprit"}))
assert isinstance(r5, list)
assert len(r5) >= 1
# ---------------------------------------------------------------------------
# Fixture HTTP
# ---------------------------------------------------------------------------
@pytest.fixture()
def client(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "erori.db"))
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
from app.config import get_settings
get_settings.cache_clear()
from app.main import app
from fastapi.testclient import TestClient
with TestClient(app) as c:
yield c
get_settings.cache_clear()
def _csv_bytes(rows: list[dict]) -> bytes:
buf = io.StringIO()
writer = csv.DictWriter(buf, fieldnames=list(rows[0].keys()), delimiter=";")
writer.writeheader()
writer.writerows(rows)
return buf.getvalue().encode("utf-8")
def _seed_mapping(conn=None):
"""Mapeaza OP-1 -> R-FRANE (cont dev id=1)."""
from app.db import get_connection
c = conn or get_connection()
try:
c.execute("INSERT OR REPLACE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES ('R-FRANE','Reparatie frane')")
c.execute(
"INSERT OR IGNORE INTO operations_mapping (account_id, cod_op_service, cod_prestatie, auto_send) "
"VALUES (1, 'OP-1', 'R-FRANE', 1)"
)
c.commit()
finally:
if conn is None:
c.close()
def _creeaza_submission_needs_data(client, fix_text=None):
"""Creeaza un submission in starea needs_data si returneaza id-ul."""
import json as _json
from app.db import get_connection
_seed_mapping()
# Insereaza direct un submission needs_data cu rar_error 3-niveluri
fix_folosit = fix_text or "Verifica VIN-ul pe talon (pozitia E); 17 caractere majuscule."
rar_error_3n = _json.dumps([{
"field": "vin",
"cod": "VIN_FORMAT",
"problema": "VIN invalid",
"cauza": "VIN-ul are 16 caractere; RAR cere exact 17.",
"fix": fix_folosit,
"message": "VIN-ul are 16 caractere; RAR cere exact 17.",
}])
payload = _json.dumps({
"vin": "VIN16CARACT000000",
"nr_inmatriculare": "B001TST",
"data_prestatie": "2026-06-15",
"odometru_final": 145000,
"cod_prestatie": "R-FRANE",
"prestatii": [{"cod_prestatie": "R-FRANE"}],
})
idem_key = f"test-erori-{fix_folosit[:20]}"
conn = get_connection()
try:
cur = conn.execute(
"""INSERT INTO submissions
(account_id, idempotency_key, payload_json, status, rar_error, retry_count)
VALUES (1, ?, ?, 'needs_data', ?, 0)""",
(idem_key, payload, rar_error_3n)
)
conn.commit()
return cur.lastrowid
finally:
conn.close()
def test_detaliu_afiseaza_fix(client):
"""Fragmentul de detaliu al unui submission needs_data contine textul fix-ului."""
sub_id = _creeaza_submission_needs_data(client, "Verifica VIN-ul pe talon (pozitia E)")
resp = client.get(f"/_fragments/trimitere/{sub_id}")
assert resp.status_code == 200
html = resp.text
# Fix-ul trebuie sa apara in HTML
assert "pozitia E" in html, (
f"HTML-ul detaliu nu contine fix-ul ('pozitia E'). Primii 2000 chars:\n{html[:2000]}"
)
def _import_preview_cu_vin_invalid(client):
"""Efectueaza un import cu un rand cu VIN invalid si returneaza HTML-ul preview."""
_seed_mapping()
rows = [
# rand cu VIN de 16 caractere (invalid, trebuie 17) -> needs_data
{"VIN": "VIN16CARACT00000", "Nr inmatriculare": "B001TST",
"Data prestatie": "15.06.2026", "Odometru final": "145000", "Operatie": "OP-1"},
]
data = _csv_bytes(rows)
r = client.post("/_import/upload", files={"file": ("test.csv", data, "text/csv")})
assert r.status_code == 200
m = re.search(r"/_import/(\d+)/mapare-coloane", r.text)
assert m, f"Nu am gasit import_id. Raspuns: {r.text[:1000]}"
import_id = int(m.group(1))
r = client.post(f"/_import/{import_id}/mapare-coloane", data={
"colname": ["VIN", "Nr inmatriculare", "Data prestatie", "Odometru final", "Operatie"],
"canon": ["vin", "nr_inmatriculare", "data_prestatie", "odometru_final", "operatie"],
"format_data": "DD.MM.YYYY",
})
assert r.status_code == 200
return r.text
def test_preview_rand_per_camp_fix(client):
"""Preview rand needs_data -> HTML contine fix-ul per camp (VIN)."""
html = _import_preview_cu_vin_invalid(client)
# Randul trebuie sa fie needs_data
assert "needs_data" in html or "needs_review" in html or "VIN" in html.upper(), (
f"HTML-ul preview nu contine starea asteptata. Primii 2000 chars:\n{html[:2000]}"
)
# Fix-ul pentru VIN trebuie sa apara in preview
assert "talon" in html.lower() or "majuscule" in html.lower() or "pozitia" in html.lower(), (
f"HTML-ul preview nu contine fix-ul VIN. Primii 3000 chars:\n{html[:3000]}"
)
# ---------------------------------------------------------------------------
# Teste noi (BUG 1 + BUG 2) — adaugate TDD-style (RED inainte de fix)
# ---------------------------------------------------------------------------
from app.web.labels import motiv_uman # noqa: E402
from app.errors import eroare # noqa: E402
def test_motiv_uman_creds_3niveluri():
"""BUG 1: motiv_uman pe dict 3-niveluri (RAR_CREDS_INVALIDE) -> problema, NU text garbled."""
rar_err = json.dumps(eroare("RAR_CREDS_INVALIDE", cauza="credentiale RAR invalide"))
rezultat = motiv_uman("error", rar_err)
# Nu trebuie sa contina "field:" / "cod:" (text garbled)
assert "field:" not in rezultat, f"Text garbled cu 'field:': {rezultat!r}"
assert "cod:" not in rezultat, f"Text garbled cu 'cod:': {rezultat!r}"
# Trebuie sa returneze textul 'problema' (primul nivel)
data = json.loads(rar_err)
problema = data.get("problema") or ""
assert problema, "eroare() nu a returnat 'problema' — verifica error_codes.py"
assert rezultat == problema[:200], (
f"Asteptat {problema[:200]!r}, obtinut {rezultat!r}"
)
def test_motiv_uman_unmapped_neschimbat():
"""Ramura unmapped inca functioneaza dupa adaugarea ramurii 3-niveluri."""
rar_err = json.dumps({"unmapped": [{"cod_op_service": "OP-99", "denumire": "Test"}]})
rezultat = motiv_uman("needs_mapping", rar_err)
assert rezultat.startswith("Cod RAR lipsa pentru:"), (
f"Ramura unmapped regresta. Obtinut: {rezultat!r}"
)
assert "OP-99" in rezultat
def test_parse_erori_gol_returneaza_lista_goala():
"""BUG 2: parse_erori pe dict/lista goala -> [], nu 1 element cu problema=''."""
r1 = parse_erori("{}")
assert r1 == [], f"parse_erori('{{}}') -> {r1!r}, asteptat []"
r2 = parse_erori("[{}]")
assert r2 == [], f"parse_erori('[{{}}]') -> {r2!r}, asteptat []"