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>
286 lines
10 KiB
Python
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 []"
|