Doar 13.8% din numerele de inmatriculare istorice treceau validarea stricta
^[A-Z0-9]{1,10}$ (spike SQL), majoritatea din cauza formatelor reale cu
spatii/cratime ("B 99 XYZ", "B-99-XYZ") pe care normalizarea actuala
(strip+upper) nu le curata.
Adauga normalize_nr_inmatriculare() in app.idempotency (deja punctul comun
API+import pentru canonicalize_row) si o foloseste consecvent in:
- models.PrezentareIn (validare shape / normalizare API)
- validation.validate_prezentare (validare continut)
- idempotency.canonicalize_row (cheia de dedup)
Fara aceasta consecventa, validarea si idempotency ar diverge pe acelasi
numar de inmatriculare in formate echivalente.
269 lines
9.2 KiB
Python
269 lines
9.2 KiB
Python
"""Validare de domeniu prezentari (app.validation).
|
|
|
|
Un test per regula din plan.md sect. 2 / contract. Validarea normalizeaza
|
|
(strip/upper) si intoarce erori structurate {field, message}; nu ridica exceptii.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import timedelta
|
|
|
|
import pytest
|
|
|
|
from app.models import PrezentareIn
|
|
from app.validation import today_bucuresti, validate_prezentare
|
|
|
|
|
|
def _base(**overrides) -> dict:
|
|
"""Prezentare valida ca dict (normalizat prin PrezentareIn), cu suprascrieri."""
|
|
data = {
|
|
"vin": "WVWZZZ1KZAW000123",
|
|
"nr_inmatriculare": "B999TST",
|
|
"data_prestatie": "2026-06-15",
|
|
"odometru_final": "123456",
|
|
"odometru_initial": None,
|
|
"prestatii": [{"cod_prestatie": "OE-1"}],
|
|
"sistem_reparat": "null",
|
|
}
|
|
data.update(overrides)
|
|
return PrezentareIn(**data).model_dump()
|
|
|
|
|
|
def _fields(errors: list[dict]) -> set[str]:
|
|
return {e["field"] for e in errors}
|
|
|
|
|
|
def test_prezentare_valida_fara_erori():
|
|
assert validate_prezentare(_base()) == []
|
|
|
|
|
|
def test_normalizare_strip_upper():
|
|
c = _base(vin=" wvwzzz1kzaw000123 ", nr_inmatriculare=" b999tst ")
|
|
assert c["vin"] == "WVWZZZ1KZAW000123"
|
|
assert c["nr_inmatriculare"] == "B999TST"
|
|
assert validate_prezentare(c) == []
|
|
|
|
|
|
@pytest.mark.parametrize("vin", [
|
|
"WVWZZ1KZAW000123", # 16 caractere
|
|
"WVWZZZ1KZAW0001234", # 18 caractere
|
|
"WVWZZZ1OZAW000123", # contine O
|
|
"WVWZZZ1IZAW000123", # contine I
|
|
"WVWZZZ1QZAW000123", # contine Q
|
|
"WVW ZZ1KZAW00012", # spatiu
|
|
])
|
|
def test_vin_invalid(vin):
|
|
errors = validate_prezentare(_base(vin=vin))
|
|
assert "vin" in _fields(errors)
|
|
|
|
|
|
def test_nrinmatriculare_prea_lung():
|
|
errors = validate_prezentare(_base(nr_inmatriculare="ABCDEFGHIJK")) # 11
|
|
assert "nr_inmatriculare" in _fields(errors)
|
|
|
|
|
|
def test_nrinmatriculare_caracter_special():
|
|
errors = validate_prezentare(_base(nr_inmatriculare="B_99"))
|
|
assert "nr_inmatriculare" in _fields(errors)
|
|
|
|
|
|
def test_nrinmatriculare_normalizeaza_spatii():
|
|
c = _base(nr_inmatriculare="B 99 XYZ")
|
|
assert c["nr_inmatriculare"] == "B99XYZ"
|
|
assert validate_prezentare(c) == []
|
|
|
|
|
|
def test_nrinmatriculare_normalizeaza_cratime():
|
|
c = _base(nr_inmatriculare="B-99-XYZ")
|
|
assert c["nr_inmatriculare"] == "B99XYZ"
|
|
assert validate_prezentare(c) == []
|
|
|
|
|
|
def test_nrinmatriculare_normalizeaza_puncte():
|
|
c = _base(nr_inmatriculare="B.99.XYZ")
|
|
assert c["nr_inmatriculare"] == "B99XYZ"
|
|
assert validate_prezentare(c) == []
|
|
|
|
|
|
def test_data_prea_veche():
|
|
errors = validate_prezentare(_base(data_prestatie="2024-11-30"))
|
|
assert "data_prestatie" in _fields(errors)
|
|
|
|
|
|
def test_data_la_limita_inferioara_ok():
|
|
assert validate_prezentare(_base(data_prestatie="2024-12-01")) == []
|
|
|
|
|
|
def test_data_in_viitor():
|
|
maine = (today_bucuresti() + timedelta(days=1)).isoformat()
|
|
errors = validate_prezentare(_base(data_prestatie=maine))
|
|
assert "data_prestatie" in _fields(errors)
|
|
|
|
|
|
def test_data_azi_ok():
|
|
azi = today_bucuresti().isoformat()
|
|
assert validate_prezentare(_base(data_prestatie=azi)) == []
|
|
|
|
|
|
def test_data_format_invalid():
|
|
errors = validate_prezentare(_base(data_prestatie="15-06-2026"))
|
|
assert "data_prestatie" in _fields(errors)
|
|
|
|
|
|
def test_odometru_final_nenumeric():
|
|
errors = validate_prezentare(_base(odometru_final="abc"))
|
|
assert "odometru_final" in _fields(errors)
|
|
|
|
|
|
def test_rodo_fara_odometru_initial():
|
|
errors = validate_prezentare(_base(prestatii=[{"cod_prestatie": "R-ODO"}]))
|
|
assert "odometru_initial" in _fields(errors)
|
|
|
|
|
|
def test_iodo_fara_odometru_initial():
|
|
errors = validate_prezentare(_base(prestatii=[{"cod_prestatie": "I-ODO"}]))
|
|
assert "odometru_initial" in _fields(errors)
|
|
|
|
|
|
def test_rodo_cu_odometru_initial_ok():
|
|
c = _base(prestatii=[{"cod_prestatie": "R-ODO"}], odometru_initial="100000")
|
|
assert validate_prezentare(c) == []
|
|
|
|
|
|
def test_odometru_initial_mai_mare_decat_final():
|
|
c = _base(prestatii=[{"cod_prestatie": "R-ODO"}], odometru_initial="200000", odometru_final="100000")
|
|
assert "odometru_initial" in _fields(errors := validate_prezentare(c))
|
|
assert any("<=" in e["message"] for e in errors)
|
|
|
|
|
|
def test_prestatii_goale():
|
|
errors = validate_prezentare(_base(prestatii=[]))
|
|
assert "prestatii" in _fields(errors)
|
|
|
|
|
|
def test_b64image_invalid():
|
|
errors = validate_prezentare(_base(b64_image="@@@not-base64@@@"))
|
|
assert "b64_image" in _fields(errors)
|
|
|
|
|
|
def test_b64image_valid_ok():
|
|
import base64
|
|
good = base64.b64encode(b"poza odometru").decode()
|
|
assert validate_prezentare(_base(b64_image=good)) == []
|
|
|
|
|
|
def test_erori_multiple_cumulate():
|
|
errors = validate_prezentare(_base(vin="BAD", nr_inmatriculare="X_Y", data_prestatie="2024-01-01"))
|
|
assert {"vin", "nr_inmatriculare", "data_prestatie"} <= _fields(errors)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3 niveluri — coduri stabile, forma aditiva, back-compat
|
|
# ---------------------------------------------------------------------------
|
|
|
|
from app import errors as err_mod # noqa: E402
|
|
|
|
|
|
def test_vin_invalid_are_3niveluri():
|
|
"""VIN cu O/I/Q => eroarea are cod, problema, fix, field, message neschimbat."""
|
|
errs = validate_prezentare(_base(vin="WVWZZZ1OZAW000123"))
|
|
vin_errs = [e for e in errs if e.get("field") == "vin"]
|
|
assert vin_errs, "Trebuie cel putin o eroare cu field==vin"
|
|
e = vin_errs[0]
|
|
assert e["cod"] == "VIN_FORMAT"
|
|
assert e["problema"], "problema trebuie sa fie ne-goala"
|
|
assert e["fix"], "fix trebuie sa fie ne-gol"
|
|
assert e["field"] == "vin"
|
|
# message trebuie sa fie mesajul existent (back-compat)
|
|
assert "17" in e["message"] or "O, I, Q" in e["message"]
|
|
|
|
|
|
def test_data_prea_veche_cod():
|
|
errs = validate_prezentare(_base(data_prestatie="2024-11-30"))
|
|
dp = [e for e in errs if e.get("field") == "data_prestatie"]
|
|
assert dp
|
|
assert dp[0]["cod"] == "DATA_PREA_VECHE"
|
|
|
|
|
|
def test_data_viitor_cod():
|
|
from datetime import timedelta
|
|
maine = (today_bucuresti() + timedelta(days=1)).isoformat()
|
|
errs = validate_prezentare(_base(data_prestatie=maine))
|
|
dp = [e for e in errs if e.get("field") == "data_prestatie"]
|
|
assert dp
|
|
assert dp[0]["cod"] == "DATA_VIITOR"
|
|
|
|
|
|
def test_data_format_cod():
|
|
errs = validate_prezentare(_base(data_prestatie="15-06-2026"))
|
|
dp = [e for e in errs if e.get("field") == "data_prestatie"]
|
|
assert dp
|
|
assert dp[0]["cod"] == "DATA_FORMAT"
|
|
|
|
|
|
def test_odometru_initial_lipsa_cod():
|
|
errs = validate_prezentare(_base(prestatii=[{"cod_prestatie": "R-ODO"}]))
|
|
oi = [e for e in errs if e.get("field") == "odometru_initial"]
|
|
assert oi
|
|
assert oi[0]["cod"] == "ODOMETRU_INITIAL_LIPSA"
|
|
|
|
|
|
def test_odometru_ordine_cod():
|
|
c = _base(prestatii=[{"cod_prestatie": "R-ODO"}], odometru_initial="200000", odometru_final="100000")
|
|
errs = validate_prezentare(c)
|
|
oi = [e for e in errs if e.get("field") == "odometru_initial" and e.get("cod") == "ODOMETRU_INITIAL_ORDINE"]
|
|
assert oi, "Trebuie eroare cu cod ODOMETRU_INITIAL_ORDINE"
|
|
|
|
|
|
def test_prestatii_goale_cod():
|
|
errs = validate_prezentare(_base(prestatii=[]))
|
|
pr = [e for e in errs if e.get("field") == "prestatii"]
|
|
assert pr
|
|
assert pr[0]["cod"] == "PRESTATII_GOALE"
|
|
|
|
|
|
def test_b64_invalid_cod():
|
|
errs = validate_prezentare(_base(b64_image="@@@not-base64@@@"))
|
|
b = [e for e in errs if e.get("field") == "b64_image"]
|
|
assert b
|
|
assert b[0]["cod"] == "B64_INVALID"
|
|
|
|
|
|
def test_back_compat_field_message():
|
|
"""Fiecare eroare are inca field + message (forma veche)."""
|
|
errs = validate_prezentare(_base(
|
|
vin="BAD",
|
|
nr_inmatriculare="X_Y",
|
|
data_prestatie="2024-01-01",
|
|
odometru_final="abc",
|
|
prestatii=[],
|
|
))
|
|
for e in errs:
|
|
assert "field" in e, f"Lipseste 'field' in {e}"
|
|
assert "message" in e, f"Lipseste 'message' in {e}"
|
|
assert e["message"], f"'message' gol in {e}"
|
|
|
|
|
|
def test_toate_codurile_in_catalog():
|
|
"""Fiecare cod emis de validate_prezentare exista in errors.CATALOG."""
|
|
from datetime import timedelta
|
|
|
|
cazuri = [
|
|
_base(vin="WVWZZZ1OZAW000123"), # VIN_FORMAT
|
|
_base(nr_inmatriculare="X_Y"), # NR_INMATRICULARE_FORMAT
|
|
_base(data_prestatie="15-06-2026"), # DATA_FORMAT
|
|
_base(data_prestatie="2024-11-30"), # DATA_PREA_VECHE
|
|
_base(data_prestatie=(today_bucuresti() + timedelta(days=1)).isoformat()), # DATA_VIITOR
|
|
_base(odometru_final="abc"), # ODOMETRU_FINAL_FORMAT
|
|
_base(prestatii=[{"cod_prestatie": "R-ODO"}]), # ODOMETRU_INITIAL_LIPSA
|
|
_base(prestatii=[{"cod_prestatie": "R-ODO"}], odometru_initial="abc"), # ODOMETRU_INITIAL_FORMAT
|
|
_base(prestatii=[{"cod_prestatie": "R-ODO"}], odometru_initial="200000", odometru_final="100000"), # ODOMETRU_INITIAL_ORDINE
|
|
_base(prestatii=[]), # PRESTATII_GOALE
|
|
_base(b64_image="@@@not-base64@@@"), # B64_INVALID
|
|
]
|
|
for caz in cazuri:
|
|
errs = validate_prezentare(caz)
|
|
for e in errs:
|
|
if "cod" in e:
|
|
assert e["cod"] in err_mod.CATALOG, f"Cod {e['cod']!r} absent din CATALOG"
|