feat(T3): validare completa prezentari + 29 teste
- app/validation.py: reguli de continut (VIN ^[A-HJ-NPR-Z0-9]{17}$ fara O/I/Q,
nrInm ^[A-Z0-9]{1,10}$, dataPrestatie ∈ [2024-12-01, azi] TZ Bucuresti,
R-ODO/I-ODO -> odometruInitial obligatoriu, odometruInitial<=odometruFinal,
odometruFinal numeric, prestatii nevide, b64Image base64 valid)
- erori structurate {field, message} (aceeasi forma ca raspunsul RAR), fara exceptii
- modele Pydantic: normalizare strip/upper pe vin/nrInm/coduri
- router /v1/prezentari: validare inainte de enqueue; esec continut -> needs_data
(tinut, vizibil in dashboard cu motiv), NU 422; JSON malformat -> 422 (shape)
- tests/: 29 teste (per regula + rutare API + idempotenta)
Verify: pytest 29 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ from fastapi import APIRouter, HTTPException
|
|||||||
from ...db import get_connection
|
from ...db import get_connection
|
||||||
from ...idempotency import idempotency_key
|
from ...idempotency import idempotency_key
|
||||||
from ...models import PrezentareRequest, PrezentariResponse, SubmissionResult
|
from ...models import PrezentareRequest, PrezentariResponse, SubmissionResult
|
||||||
|
from ...validation import validate_prezentare
|
||||||
|
|
||||||
router = APIRouter(prefix="/v1", tags=["v1"])
|
router = APIRouter(prefix="/v1", tags=["v1"])
|
||||||
|
|
||||||
@@ -26,8 +27,9 @@ router = APIRouter(prefix="/v1", tags=["v1"])
|
|||||||
def create_prezentari(req: PrezentareRequest) -> PrezentariResponse:
|
def create_prezentari(req: PrezentareRequest) -> PrezentariResponse:
|
||||||
"""Enqueue una/mai multe prezentari. Idempotent: continut identic -> acelasi submission.
|
"""Enqueue una/mai multe prezentari. Idempotent: continut identic -> acelasi submission.
|
||||||
|
|
||||||
TODO(T3): validare Pydantic completa inainte de enqueue (VIN/data/nrInm),
|
Validarea de continut (T3, app.validation) ruleaza inainte de enqueue:
|
||||||
ruteaza needs_data/needs_mapping.
|
esecurile NU resping cererea, ci enqueue-aza cu status `needs_data` + motiv
|
||||||
|
(plan.md sect. 3). JSON malformat -> 422 din Pydantic (validare de shape).
|
||||||
TODO(auth): rezolva account_id din API key (acum None).
|
TODO(auth): rezolva account_id din API key (acum None).
|
||||||
Nota: rar_credentials NU se persista (zero-storage) — worker-ul le va primi
|
Nota: rar_credentials NU se persista (zero-storage) — worker-ul le va primi
|
||||||
pe alt canal (T2); in schelet enqueue-ul doar stocheaza prezentarea.
|
pe alt canal (T2); in schelet enqueue-ul doar stocheaza prezentarea.
|
||||||
@@ -53,12 +55,20 @@ def create_prezentari(req: PrezentareRequest) -> PrezentariResponse:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# T3: validare de continut -> queued daca e curat, altfel needs_data + motiv.
|
||||||
|
errors = validate_prezentare(content)
|
||||||
|
if errors:
|
||||||
|
status, rar_error = "needs_data", json.dumps(errors, ensure_ascii=False)
|
||||||
|
else:
|
||||||
|
status, rar_error = "queued", None
|
||||||
|
|
||||||
cur = conn.execute(
|
cur = conn.execute(
|
||||||
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json) "
|
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_error) "
|
||||||
"VALUES (?, ?, 'queued', ?)",
|
"VALUES (?, ?, ?, ?, ?)",
|
||||||
(key, account_id, json.dumps(content, ensure_ascii=False)),
|
(key, account_id, status, json.dumps(content, ensure_ascii=False), rar_error),
|
||||||
)
|
)
|
||||||
results.append(SubmissionResult(submission_id=int(cur.lastrowid), status="queued"))
|
results.append(SubmissionResult(submission_id=int(cur.lastrowid), status=status))
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
return PrezentariResponse(results=results)
|
return PrezentariResponse(results=results)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ obligatoriu, odometruInitial <= odometruFinal, normalizare strip/upper) este
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
|
||||||
|
|
||||||
class RarCredentials(BaseModel):
|
class RarCredentials(BaseModel):
|
||||||
@@ -21,13 +21,24 @@ class RarCredentials(BaseModel):
|
|||||||
class PrestatieItem(BaseModel):
|
class PrestatieItem(BaseModel):
|
||||||
cod_prestatie: str = Field(..., description="cod din nomenclator RAR, ex. OE-1")
|
cod_prestatie: str = Field(..., description="cod din nomenclator RAR, ex. OE-1")
|
||||||
|
|
||||||
|
@field_validator("cod_prestatie")
|
||||||
|
@classmethod
|
||||||
|
def _norm_cod(cls, v: str) -> str:
|
||||||
|
return v.strip().upper()
|
||||||
|
|
||||||
|
|
||||||
class PrezentareIn(BaseModel):
|
class PrezentareIn(BaseModel):
|
||||||
"""O prezentare de declarat la RAR (inainte de validarea T3)."""
|
"""O prezentare de declarat la RAR.
|
||||||
|
|
||||||
|
Pydantic doar NORMALIZEAZA aici (strip/upper pe vin/nrInm). Validarea de
|
||||||
|
continut (regex VIN, interval data, R-ODO/I-ODO, odometru) e in
|
||||||
|
app.validation.validate_prezentare si NU resping cererea — marcheaza
|
||||||
|
`needs_data` (plan.md sect. 3).
|
||||||
|
"""
|
||||||
|
|
||||||
vin: str
|
vin: str
|
||||||
nr_inmatriculare: str
|
nr_inmatriculare: str
|
||||||
data_prestatie: str # YYYY-MM-DD; validare interval = T3
|
data_prestatie: str # YYYY-MM-DD
|
||||||
odometru_final: str # string per contract
|
odometru_final: str # string per contract
|
||||||
odometru_initial: str | None = None
|
odometru_initial: str | None = None
|
||||||
prestatii: list[PrestatieItem]
|
prestatii: list[PrestatieItem]
|
||||||
@@ -35,6 +46,16 @@ class PrezentareIn(BaseModel):
|
|||||||
obs: str | None = None
|
obs: str | None = None
|
||||||
b64_image: str | None = None
|
b64_image: str | None = None
|
||||||
|
|
||||||
|
@field_validator("vin", "nr_inmatriculare")
|
||||||
|
@classmethod
|
||||||
|
def _norm_upper(cls, v: str) -> str:
|
||||||
|
return v.strip().upper()
|
||||||
|
|
||||||
|
@field_validator("data_prestatie", "odometru_final")
|
||||||
|
@classmethod
|
||||||
|
def _norm_strip(cls, v: str) -> str:
|
||||||
|
return v.strip()
|
||||||
|
|
||||||
|
|
||||||
class PrezentareRequest(BaseModel):
|
class PrezentareRequest(BaseModel):
|
||||||
"""Body pentru POST /v1/prezentari — una sau mai multe prezentari + creds RAR."""
|
"""Body pentru POST /v1/prezentari — una sau mai multe prezentari + creds RAR."""
|
||||||
|
|||||||
136
app/validation.py
Normal file
136
app/validation.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
"""Validare de domeniu pentru prezentari (T3).
|
||||||
|
|
||||||
|
Replica regulile RAR (docs/api-rar-contract.md sect. "Reguli de validare") ÎNAINTE
|
||||||
|
de enqueue, ca sa nu primim 4xx de la RAR. Spre deosebire de validarea de SHAPE
|
||||||
|
(Pydantic, da 422 la JSON malformat), aceasta e validare de CONTINUT: esecurile NU
|
||||||
|
resping cererea, ci marcheaza submission-ul `needs_data` (plan.md sect. 3 — masina de
|
||||||
|
stari + failure registry). Asa apar in dashboard cu motiv, corectabile.
|
||||||
|
|
||||||
|
Functiile sunt pure (dict -> listă erori), unit-testabile fara DB/HTTP.
|
||||||
|
Erorile au forma {field, message} — aceeasi ca raspunsul de eroare RAR.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import re
|
||||||
|
from datetime import date
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
# VIN: 17 caractere, majuscule, fara O/I/Q (plan §2 + contract).
|
||||||
|
VIN_RE = re.compile(r"^[A-HJ-NPR-Z0-9]{17}$")
|
||||||
|
# Numar inmatriculare: max 10, litere + cifre majuscule.
|
||||||
|
NRINM_RE = re.compile(r"^[A-Z0-9]{1,10}$")
|
||||||
|
# Coduri care fac odometruInitial obligatoriu.
|
||||||
|
ODOMETER_CODES = {"R-ODO", "I-ODO"}
|
||||||
|
# Interval dataPrestatie.
|
||||||
|
MIN_DATA_PRESTATIE = date(2024, 12, 1)
|
||||||
|
TZ_BUCURESTI = ZoneInfo("Europe/Bucharest")
|
||||||
|
|
||||||
|
|
||||||
|
def _norm(value: object) -> str:
|
||||||
|
return str(value or "").strip().upper()
|
||||||
|
|
||||||
|
|
||||||
|
def _codes(prestatii: list | None) -> list[str]:
|
||||||
|
out: list[str] = []
|
||||||
|
for p in prestatii or []:
|
||||||
|
cod = p.get("cod_prestatie") if isinstance(p, dict) else getattr(p, "cod_prestatie", None)
|
||||||
|
if cod:
|
||||||
|
out.append(str(cod).strip().upper())
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_int(value: object) -> int | None:
|
||||||
|
s = str(value or "").strip()
|
||||||
|
if s.isdigit():
|
||||||
|
return int(s)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def today_bucuresti() -> date:
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
return datetime.now(TZ_BUCURESTI).date()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_prezentare(content: dict) -> list[dict]:
|
||||||
|
"""Intoarce lista erorilor de continut [{field, message}]. Goala = valid.
|
||||||
|
|
||||||
|
`content` = PrezentareIn.model_dump() (campuri snake_case interne).
|
||||||
|
"""
|
||||||
|
errors: list[dict] = []
|
||||||
|
|
||||||
|
# --- VIN ---
|
||||||
|
vin = _norm(content.get("vin"))
|
||||||
|
if not VIN_RE.match(vin):
|
||||||
|
errors.append({
|
||||||
|
"field": "vin",
|
||||||
|
"message": "VIN trebuie sa aiba exact 17 caractere majuscule, fara spatii/caractere speciale si fara O, I, Q.",
|
||||||
|
})
|
||||||
|
|
||||||
|
# --- nrInmatriculare ---
|
||||||
|
nrinm = _norm(content.get("nr_inmatriculare"))
|
||||||
|
if not NRINM_RE.match(nrinm):
|
||||||
|
errors.append({
|
||||||
|
"field": "nr_inmatriculare",
|
||||||
|
"message": "Numarul de inmatriculare trebuie sa aiba max 10 caractere, doar litere si cifre majuscule.",
|
||||||
|
})
|
||||||
|
|
||||||
|
# --- dataPrestatie ∈ [2024-12-01, azi] TZ Bucuresti ---
|
||||||
|
raw_data = str(content.get("data_prestatie") or "").strip()
|
||||||
|
try:
|
||||||
|
d = date.fromisoformat(raw_data)
|
||||||
|
except ValueError:
|
||||||
|
errors.append({"field": "data_prestatie", "message": "Format data invalid; foloseste YYYY-MM-DD."})
|
||||||
|
d = None
|
||||||
|
if d is not None:
|
||||||
|
if d < MIN_DATA_PRESTATIE:
|
||||||
|
errors.append({"field": "data_prestatie", "message": "Data prestatiei nu poate fi anterioara datei de 01.12.2024."})
|
||||||
|
elif d > today_bucuresti():
|
||||||
|
errors.append({"field": "data_prestatie", "message": "Data prestatiei nu poate fi in viitor."})
|
||||||
|
|
||||||
|
# --- odometruFinal (string numeric) ---
|
||||||
|
odo_final = _parse_int(content.get("odometru_final"))
|
||||||
|
if odo_final is None:
|
||||||
|
errors.append({"field": "odometru_final", "message": "odometruFinal trebuie sa fie un numar intreg (ca string)."})
|
||||||
|
|
||||||
|
# --- odometruInitial: obligatoriu daca prestatii ∋ R-ODO/I-ODO; <= odometruFinal ---
|
||||||
|
codes = _codes(content.get("prestatii"))
|
||||||
|
needs_initial = bool(set(codes) & ODOMETER_CODES)
|
||||||
|
raw_initial = content.get("odometru_initial")
|
||||||
|
has_initial = str(raw_initial or "").strip() != ""
|
||||||
|
if needs_initial and not has_initial:
|
||||||
|
errors.append({
|
||||||
|
"field": "odometru_initial",
|
||||||
|
"message": "odometruInitial este obligatoriu cand prestatiile contin R-ODO sau I-ODO.",
|
||||||
|
})
|
||||||
|
if has_initial:
|
||||||
|
odo_initial = _parse_int(raw_initial)
|
||||||
|
if odo_initial is None:
|
||||||
|
errors.append({"field": "odometru_initial", "message": "odometruInitial trebuie sa fie un numar intreg."})
|
||||||
|
elif odo_final is not None and odo_initial > odo_final:
|
||||||
|
errors.append({"field": "odometru_initial", "message": "odometruInitial trebuie sa fie <= odometruFinal."})
|
||||||
|
|
||||||
|
# --- prestatii nevide ---
|
||||||
|
if not codes:
|
||||||
|
errors.append({"field": "prestatii", "message": "Lista de prestatii nu poate fi goala."})
|
||||||
|
|
||||||
|
# --- b64Image: optional, dar daca e prezent trebuie base64 valid ---
|
||||||
|
b64 = content.get("b64_image")
|
||||||
|
if b64:
|
||||||
|
if not _is_valid_base64(str(b64)):
|
||||||
|
errors.append({"field": "b64_image", "message": "b64Image nu este base64 valid."})
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def _is_valid_base64(value: str) -> bool:
|
||||||
|
s = value.strip()
|
||||||
|
if not s:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
base64.b64decode(s, validate=True)
|
||||||
|
return True
|
||||||
|
except (ValueError, base64.binascii.Error): # type: ignore[attr-defined]
|
||||||
|
return False
|
||||||
@@ -195,8 +195,10 @@ Nimic din cod nu e scris încă (`app/`, `tools/` nu există). Ordine recomandat
|
|||||||
- [x] **Schelet repo** — ✅ 2026-06-15. `app/api/v1`, `app/rar_client.py` (cu User-Agent), `app/worker`, `app/web`, SQLite (WAL),
|
- [x] **Schelet repo** — ✅ 2026-06-15. `app/api/v1`, `app/rar_client.py` (cu User-Agent), `app/worker`, `app/web`, SQLite (WAL),
|
||||||
`Dockerfile` + `docker compose`, `/healthz` verde. Verificat: login prin client OK, nomenclator 18 coduri,
|
`Dockerfile` + `docker compose`, `/healthz` verde. Verificat: login prin client OK, nomenclator 18 coduri,
|
||||||
worker heartbeat → `worker_alive=True`, enqueue + dedup idempotency funcționale.
|
worker heartbeat → `worker_alive=True`, enqueue + dedup idempotency funcționale.
|
||||||
- [ ] **T3 (P1) — validare Pydantic completă** (VIN `^[A-HJ-NPR-Z0-9]{17}$`, nrInm, dată ∈ [2024-12-01,azi] TZ Bucharest,
|
- [x] **T3 (P1) — validare completă** ✅ 2026-06-15. `app/validation.py` (VIN `^[A-HJ-NPR-Z0-9]{17}$`, nrInm, dată ∈
|
||||||
R-ODO/I-ODO→odometruInitial, `odometruInitial<=odometruFinal`, normalize strip/upper). Verify: unit tests per regulă.
|
[2024-12-01,azi] TZ Bucharest, R-ODO/I-ODO→odometruInitial, `odometruInitial<=odometruFinal`) + normalize strip/upper în
|
||||||
|
modelele Pydantic. **Eșecurile de conținut → `needs_data` (ținute, nu 422)** per masina de stări; JSON malformat → 422.
|
||||||
|
Verify: 29 teste pass (`tests/test_validation.py` per regulă + `tests/test_api.py` rutare/idempotenta).
|
||||||
- [ ] **T4 (P1) — payload builder** cu `status:"FINALIZATA"`, `sistemReparat:"null"`, fără `tipPrestatie`, `odometruFinal` string.
|
- [ ] **T4 (P1) — payload builder** cu `status:"FINALIZATA"`, `sistemReparat:"null"`, fără `tipPrestatie`, `odometruFinal` string.
|
||||||
Verify: snapshot payload == exemplul oficial din contract.
|
Verify: snapshot payload == exemplul oficial din contract.
|
||||||
- [ ] **T2 (P1) — `app/worker` reconciliere** VIN+dată+odometru înainte de re-send pe `sending` + lease/timeout orfane.
|
- [ ] **T2 (P1) — `app/worker` reconciliere** VIN+dată+odometru înainte de re-send pe `sending` + lease/timeout orfane.
|
||||||
|
|||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
71
tests/test_api.py
Normal file
71
tests/test_api.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
"""Teste API /v1/prezentari — rutare validare (T3) + idempotenta."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def client(monkeypatch):
|
||||||
|
# DB temporara izolata per test.
|
||||||
|
tmp = tempfile.mkdtemp()
|
||||||
|
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.db"))
|
||||||
|
from app.config import get_settings
|
||||||
|
get_settings.cache_clear() # reincarca settings cu noul env
|
||||||
|
from app.main import app
|
||||||
|
with TestClient(app) as c:
|
||||||
|
yield c
|
||||||
|
get_settings.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
|
def _body(**over):
|
||||||
|
prez = {
|
||||||
|
"vin": "WVWZZZ1KZAW000123",
|
||||||
|
"nr_inmatriculare": "B999TST",
|
||||||
|
"data_prestatie": "2026-06-15",
|
||||||
|
"odometru_final": "123456",
|
||||||
|
"prestatii": [{"cod_prestatie": "OE-1"}],
|
||||||
|
}
|
||||||
|
prez.update(over)
|
||||||
|
return {"rar_credentials": {"email": "x@y.ro", "password": "s"}, "prezentari": [prez]}
|
||||||
|
|
||||||
|
|
||||||
|
def test_prezentare_valida_queued(client):
|
||||||
|
r = client.post("/v1/prezentari", json=_body())
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["results"][0]["status"] == "queued"
|
||||||
|
|
||||||
|
|
||||||
|
def test_vin_invalid_needs_data(client):
|
||||||
|
r = client.post("/v1/prezentari", json=_body(vin="WVWZZZ1OZIQ45678"))
|
||||||
|
assert r.status_code == 200
|
||||||
|
sid = r.json()["results"][0]
|
||||||
|
assert sid["status"] == "needs_data"
|
||||||
|
|
||||||
|
|
||||||
|
def test_data_viitoare_needs_data(client):
|
||||||
|
r = client.post("/v1/prezentari", json=_body(data_prestatie="2099-01-01"))
|
||||||
|
assert r.json()["results"][0]["status"] == "needs_data"
|
||||||
|
|
||||||
|
|
||||||
|
def test_idempotenta_dedup(client):
|
||||||
|
b = _body()
|
||||||
|
r1 = client.post("/v1/prezentari", json=b)
|
||||||
|
r2 = client.post("/v1/prezentari", json=b)
|
||||||
|
id1 = r1.json()["results"][0]["submission_id"]
|
||||||
|
res2 = r2.json()["results"][0]
|
||||||
|
assert res2["submission_id"] == id1
|
||||||
|
assert res2["deduped"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_json_malformat_422(client):
|
||||||
|
# Lipseste vin -> validare de shape Pydantic -> 422 (NU needs_data).
|
||||||
|
bad = {"rar_credentials": {"email": "x", "password": "y"},
|
||||||
|
"prezentari": [{"nr_inmatriculare": "B1", "data_prestatie": "2026-06-15",
|
||||||
|
"odometru_final": "1", "prestatii": [{"cod_prestatie": "OE-1"}]}]}
|
||||||
|
r = client.post("/v1/prezentari", json=bad)
|
||||||
|
assert r.status_code == 422
|
||||||
139
tests/test_validation.py
Normal file
139
tests/test_validation.py
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
"""Teste T3 — 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_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)
|
||||||
Reference in New Issue
Block a user