Banner upload compact/directiv (fara 'mediu global' derutant), text Cont onest (importul web fara mediu activ nu are creds -> esueaza), si aliniere referinta navigare la 'Meniu cont › Cont' in erori/integrare (nu exista 'tab-ul Cont' in UI). Co-Authored-By: Claude Opus 4.8 <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 Meniu cont › 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 []"
|