feat(api): validare cod_prestatie la nomenclator + optiune on_unmapped_error
Cod_prestatie necunoscut in nomenclator nu se mai trimite raw la RAR (HTTP 500 ORA-12899 + record partial FINALIZATA pe care reconcilierea il marca fals sent): e promovat la cod_op_service si tratat ca operatie de mapat. Optiune top-level boolean on_unmapped_error pe POST /v1/prezentari + /valideaza: - false (default) -> submission needs_mapping (intra in editor) - true -> respinge fara enqueue (status error, submission_id=null, erori) - None -> default per-cont accounts.on_unmapped_error_default (implicit 0) Inlocuieste enum-ul anterior on_unmapped (needs_mapping/error) cu un boolean mai simplu; coloana de cont migrata aditiv la INTEGER on_unmapped_error_default. Izolare teste de .env-ul de dezvoltare: tests/conftest.py fixeaza default sigur pe AUTOPASS_REQUIRE_API_KEY / AUTOPASS_WORKER_USE_TEST_CREDS (precedenta peste .env in pydantic-settings) + fixturile env din test_creds_delivery/test_t1 pineaza explicit aceste flag-uri, ca fallback-ul creds pe cont sa fie atins. Teste: 752 passed (fara flag pe CLI). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
17
tests/conftest.py
Normal file
17
tests/conftest.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Configurare pytest la nivel de suita: izoleaza testele de `.env`-ul de dezvoltare.
|
||||
|
||||
In working-dir exista un `.env` pentru probe live (ex. `AUTOPASS_REQUIRE_API_KEY=true`,
|
||||
`AUTOPASS_WORKER_USE_TEST_CREDS=true`, creds RAR de test) care e citit automat de
|
||||
pydantic Settings. Fara izolare, acele flag-uri ar regla tacit comportamentul
|
||||
testelor: 401 pe rutele protejate si creds <test> in loc de fallback-ul pe cont.
|
||||
|
||||
Fixam un default sigur pe variabilele de mediu — care au PRECEDENTA peste fisierul
|
||||
`.env` in pydantic-settings — deci neutralizam doar valorile din fisier, nu si o
|
||||
variabila exportata explicit in shell. Testele care chiar verifica enforcement-ul
|
||||
(auth pornit, creds <test>) il seteaza punctual prin `monkeypatch`/`object.__setattr__`.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ.setdefault("AUTOPASS_REQUIRE_API_KEY", "false")
|
||||
os.environ.setdefault("AUTOPASS_WORKER_USE_TEST_CREDS", "false")
|
||||
@@ -23,6 +23,10 @@ def env(monkeypatch):
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.db"))
|
||||
monkeypatch.setenv("AUTOPASS_CREDS_KEY", Fernet.generate_key().decode())
|
||||
# Izolare de .env-ul de dezvoltare (creds <test> + cheie API): testele isi
|
||||
# controleaza explicit aceste flag-uri, altfel fallback-ul pe cont nu se atinge.
|
||||
monkeypatch.setenv("AUTOPASS_WORKER_USE_TEST_CREDS", "false")
|
||||
monkeypatch.setenv("AUTOPASS_REQUIRE_API_KEY", "false")
|
||||
from app.config import get_settings
|
||||
from app import crypto
|
||||
|
||||
|
||||
@@ -68,6 +68,46 @@ def test_resolve_op_nemapata_iese_in_unmapped():
|
||||
assert unmapped == [{"cod_op_service": "9999", "denumire": "Operatie noua"}]
|
||||
|
||||
|
||||
def test_resolve_cod_valid_cu_nomenclator_trece():
|
||||
"""cod_prestatie in nomenclator -> pastrat (validare activa)."""
|
||||
resolved, unmapped = resolve_prestatii([{"cod_prestatie": "oe-1"}], {}, valid_codes={"OE-1"})
|
||||
assert resolved[0]["cod_prestatie"] == "OE-1"
|
||||
assert unmapped == []
|
||||
|
||||
|
||||
def test_resolve_cod_necunoscut_devine_unmapped():
|
||||
"""cod_prestatie NECUNOSCUT in nomenclator -> promovat la cod_op_service + needs_mapping.
|
||||
|
||||
Regresie pentru bug-ul real: un cod intern in cod_prestatie (ex. 'DIVERSE
|
||||
VERIFICARI 159002') NU trebuie trimis raw la RAR (HTTP 500 + record partial).
|
||||
"""
|
||||
resolved, unmapped = resolve_prestatii(
|
||||
[{"cod_prestatie": "DIVERSE VERIFICARI 159002"}], {}, valid_codes={"OE-1", "R-ODO"}
|
||||
)
|
||||
assert resolved[0]["cod_prestatie"] is None
|
||||
assert resolved[0]["cod_op_service"] == "DIVERSE VERIFICARI 159002" # promovat
|
||||
assert unmapped == [{"cod_op_service": "DIVERSE VERIFICARI 159002",
|
||||
"denumire": "DIVERSE VERIFICARI 159002"}]
|
||||
|
||||
|
||||
def test_resolve_cod_necunoscut_cu_mapare_se_rezolva():
|
||||
"""Dupa ce codul necunoscut a fost mapat, se rezolva la codul RAR (re-rezolvare)."""
|
||||
resolved, unmapped = resolve_prestatii(
|
||||
[{"cod_prestatie": "DIVERSE VERIFICARI 159002"}],
|
||||
{"DIVERSE VERIFICARI 159002": "OE-1"},
|
||||
valid_codes={"OE-1"},
|
||||
)
|
||||
assert resolved[0]["cod_prestatie"] == "OE-1"
|
||||
assert unmapped == []
|
||||
|
||||
|
||||
def test_resolve_fara_valid_codes_e_backcompat():
|
||||
"""valid_codes=None -> validarea dezactivata: cod direct trece neatins (compat)."""
|
||||
resolved, unmapped = resolve_prestatii([{"cod_prestatie": "ORICE-COD"}], {})
|
||||
assert resolved[0]["cod_prestatie"] == "ORICE-COD"
|
||||
assert unmapped == []
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Flux complet (API) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -163,6 +203,75 @@ def test_item_fara_cod_si_fara_op_e_422(client):
|
||||
assert r.status_code == 422
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Cod_prestatie necunoscut in nomenclator + optiunea on_unmapped #
|
||||
# (RAR accepta NUMAI coduri din nomenclator; cod necunoscut -> 500 + record #
|
||||
# partial. Gateway-ul nu-l mai trimite raw.) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
_COD_INTERN = "DIVERSE VERIFICARI 159002" # >5 car., nu e in nomenclator
|
||||
|
||||
|
||||
def test_cod_prestatie_necunoscut_da_needs_mapping(client):
|
||||
"""Default: cod_prestatie necunoscut -> needs_mapping, apare in pending pentru mapare."""
|
||||
r = client.post("/v1/prezentari", json=_body([{"cod_prestatie": _COD_INTERN}]))
|
||||
assert r.status_code == 200
|
||||
assert r.json()["results"][0]["status"] == "needs_mapping"
|
||||
pend = client.get("/v1/mapari/pending").json()["pending"]
|
||||
assert len(pend) == 1
|
||||
assert pend[0]["cod_op_service"] == _COD_INTERN # promovat din cod_prestatie
|
||||
|
||||
|
||||
def test_cod_necunoscut_mapat_se_trimite(client):
|
||||
"""Flux complet: cod necunoscut -> needs_mapping -> mapezi -> queued."""
|
||||
client.post("/v1/prezentari", json=_body([{"cod_prestatie": _COD_INTERN}]))
|
||||
r = client.post("/v1/mapari", json={"cod_op_service": _COD_INTERN, "cod_prestatie": "OE-1", "auto_send": True})
|
||||
assert r.json()["reresolve"]["requeued"] == 1
|
||||
subs = client.get("/v1/prezentari", params={"status": "queued"}).json()["submissions"]
|
||||
assert len(subs) == 1
|
||||
|
||||
|
||||
def test_on_unmapped_error_respinge_fara_enqueue(client):
|
||||
"""on_unmapped_error=True per-cerere: cod necunoscut -> status error, fara submission."""
|
||||
body = _body([{"cod_prestatie": _COD_INTERN}])
|
||||
body["on_unmapped_error"] = True
|
||||
r = client.post("/v1/prezentari", json=body)
|
||||
assert r.status_code == 200
|
||||
res = r.json()["results"][0]
|
||||
assert res["status"] == "error"
|
||||
assert res["submission_id"] is None
|
||||
assert res["erori"] and res["erori"][0]["cod"] == "COD_NEMAPAT"
|
||||
# Nu s-a creat nimic in coada.
|
||||
assert client.get("/v1/prezentari").json()["submissions"] == []
|
||||
|
||||
|
||||
def test_on_unmapped_default_cont_error(client):
|
||||
"""Default per-cont (on_unmapped_error_default=1) se aplica cand cererea nu specifica optiunea."""
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
conn.execute("UPDATE accounts SET on_unmapped_error_default=1 WHERE id=1")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
r = client.post("/v1/prezentari", json=_body([{"cod_prestatie": _COD_INTERN}]))
|
||||
res = r.json()["results"][0]
|
||||
assert res["status"] == "error" and res["submission_id"] is None
|
||||
# Override per-cerere bate default-ul de cont:
|
||||
body = _body([{"cod_prestatie": _COD_INTERN}], vin="WVWZZZ1KZAW000999")
|
||||
body["on_unmapped_error"] = False
|
||||
r2 = client.post("/v1/prezentari", json=body)
|
||||
assert r2.json()["results"][0]["status"] == "needs_mapping"
|
||||
|
||||
|
||||
def test_valideaza_error_mode(client):
|
||||
"""Dry-run reflecta modul error: status_estimat='error' pentru cod necunoscut."""
|
||||
body = _body([{"cod_prestatie": _COD_INTERN}])
|
||||
body["on_unmapped_error"] = True
|
||||
r = client.post("/v1/prezentari/valideaza", json=body)
|
||||
assert r.status_code == 200
|
||||
res = r.json()["results"][0]
|
||||
assert res["status_estimat"] == "error" and res["valid"] is False
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# US-003: 3 niveluri in classify_prezentare (needs_mapping) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
@@ -21,6 +21,10 @@ def env(monkeypatch):
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t1.db"))
|
||||
monkeypatch.setenv("AUTOPASS_CREDS_KEY", Fernet.generate_key().decode())
|
||||
# Izolare de .env-ul de dezvoltare (creds <test> + cheie API): testele isi
|
||||
# controleaza explicit aceste flag-uri, altfel fallback-ul pe cont nu se atinge.
|
||||
monkeypatch.setenv("AUTOPASS_WORKER_USE_TEST_CREDS", "false")
|
||||
monkeypatch.setenv("AUTOPASS_REQUIRE_API_KEY", "false")
|
||||
from app.config import get_settings
|
||||
from app import crypto
|
||||
get_settings.cache_clear()
|
||||
|
||||
@@ -138,6 +138,51 @@ def test_raspuns_pierdut_reconciliere_fara_duplicat(env):
|
||||
assert rar.finalizate_calls == 1 # a reconciliat
|
||||
|
||||
|
||||
def test_rar_500_definitiv_devine_error_fara_reconciliere(env):
|
||||
"""RAR 500 cu mesaj (ex. ORA-12899) = esec DEFINITIV: NU reconcilia, NU reincerca.
|
||||
|
||||
Bug-ul real: 500 era tratat ca tranzitoriu -> reconciliere -> marca fals 'sent'
|
||||
pe un record PARTIAL creat de RAR (ne-tranzactional). Aici, chiar daca exista un
|
||||
record care s-ar potrivi, NU trebuie marcat sent.
|
||||
"""
|
||||
from app.worker.__main__ import process_one
|
||||
conn, settings = env
|
||||
sid = _insert(conn)
|
||||
rar = FakeRar(
|
||||
finalizate=[{"id": 99999, "vin": "WVWZZZ1KZAW000123",
|
||||
"dataPrestatie": "2026-06-15", "odometruFinal": 123456}],
|
||||
post_exc=RarError(
|
||||
"postPrezentare esuat (HTTP 500)", status_code=500,
|
||||
rar_message="Eroare la adaugarea prezentarii : ORA-12899: value too large for column COD_PRESTATIE",
|
||||
),
|
||||
)
|
||||
out = process_one(conn, settings, rar, "tok", {"id": sid, "content": _CONTENT})
|
||||
assert out == "error"
|
||||
row = _status(conn, sid)
|
||||
assert row["status"] == "error"
|
||||
assert row["id_prezentare"] is None # NU fals sent
|
||||
assert rar.post_calls == 1 # nu re-trimite
|
||||
assert rar.finalizate_calls == 0 # NU reconciliaza
|
||||
err = json.loads(row["rar_error"])
|
||||
assert err["cod"] == "RAR_EROARE_SERVER" and "ORA-12899" in err["cauza"]
|
||||
|
||||
|
||||
def test_rar_503_ramane_tranzitoriu(env):
|
||||
"""Boundary: doar 500-cu-mesaj e permanent; 503 (infra) ramane ambiguu -> reconciliere."""
|
||||
from app.worker.__main__ import process_one
|
||||
conn, settings = env
|
||||
sid = _insert(conn)
|
||||
rar = FakeRar(
|
||||
finalizate=[{"id": 68514, "vin": "WVWZZZ1KZAW000123",
|
||||
"dataPrestatie": "2026-06-15", "odometruFinal": 123456}],
|
||||
post_exc=RarError("service unavailable", status_code=503, rar_message="temporar indisponibil"),
|
||||
)
|
||||
out = process_one(conn, settings, rar, "tok", {"id": sid, "content": _CONTENT})
|
||||
assert out == "sent"
|
||||
assert _status(conn, sid)["id_prezentare"] == 68514
|
||||
assert rar.finalizate_calls == 1
|
||||
|
||||
|
||||
def test_tranzitoriu_neinregistrat_requeue(env):
|
||||
from app.worker.__main__ import process_one
|
||||
conn, settings = env
|
||||
|
||||
Reference in New Issue
Block a user