Files
rar-autopass/tests/test_canonicalize.py
Claude Agent 4ea21a034e feat(import): T9 canonicalize_row + build_key partajat (idempotency)
- canonicalize_row: VIN upper, odometru strip ".0" (Excel float coercion),
  data strip — INAINTE de validare si cheie (§3.4bis)
- build_key: aplica account_or_default(None->1) inainte de hash (OV-2):
  canal API (None) si canal import (1) produc aceeasi cheie
- build_key_legacy: helper dual-lookup pentru randuri DB vechi (pre-T9)
- router.py: POST /v1/prezentari foloseste build_key(account_id, canonicalize_row(content))
- 14 teste: canonicalizare, cross-canal, dedup float/int odometru, legacy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 20:15:59 +00:00

187 lines
6.2 KiB
Python

"""Teste T9: canonicalize_row + build_key partajat (idempotency).
Verify:
(a) cross-canal: build_key(API canal-None) == build_key(import canal-rezolvat) pentru
acelasi rand logic.
(b) regresie: strategia cheilor vechi (dual-lookup legacy) acoperita de test.
(c) canonicalize taie ".0" din odometru inainte de validare.
"""
from __future__ import annotations
import os
import tempfile
import pytest
from app.idempotency import (
build_key,
build_key_legacy,
canonicalize_row,
idempotency_key,
)
# --- canonicalize_row ---
def test_canonicalize_vin_upper():
raw = {"vin": "wvwzzz1kzaw000123", "nr_inmatriculare": "b999tst",
"data_prestatie": "2026-06-15", "odometru_final": "123456"}
c = canonicalize_row(raw)
assert c["vin"] == "WVWZZZ1KZAW000123"
assert c["nr_inmatriculare"] == "B999TST"
def test_canonicalize_odometru_strip_dot_zero():
"""123456.0 (Excel float) -> '123456'."""
raw = {"vin": "X", "nr_inmatriculare": "Y", "data_prestatie": "2026-01-01",
"odometru_final": "123456.0"}
c = canonicalize_row(raw)
assert c["odometru_final"] == "123456"
def test_canonicalize_odometru_numeric_float():
"""Numeric float 123456.0 -> '123456'."""
raw = {"vin": "X", "nr_inmatriculare": "Y", "data_prestatie": "2026-01-01",
"odometru_final": 123456.0}
c = canonicalize_row(raw)
assert c["odometru_final"] == "123456"
def test_canonicalize_odometru_int_unchanged():
"""Integer 123456 -> '123456' (nu e alterat)."""
raw = {"vin": "X", "nr_inmatriculare": "Y", "data_prestatie": "2026-01-01",
"odometru_final": 123456}
c = canonicalize_row(raw)
assert c["odometru_final"] == "123456"
def test_canonicalize_odometru_50_unchanged():
"""'123456.50' nu e coercion pur — nu se taie."""
raw = {"vin": "X", "nr_inmatriculare": "Y", "data_prestatie": "2026-01-01",
"odometru_final": "123456.50"}
c = canonicalize_row(raw)
assert c["odometru_final"] == "123456.50"
def test_canonicalize_odometru_none():
raw = {"vin": "X", "nr_inmatriculare": "Y", "data_prestatie": "2026-01-01"}
c = canonicalize_row(raw)
assert c["odometru_final"] == ""
def test_canonicalize_data_strip():
raw = {"vin": "X", "nr_inmatriculare": "Y", "data_prestatie": " 2026-06-15 ",
"odometru_final": "1"}
c = canonicalize_row(raw)
assert c["data_prestatie"] == "2026-06-15"
# --- build_key cross-canal (a) ---
_RAND = {
"vin": "WVWZZZ1KZAW000123",
"nr_inmatriculare": "B999TST",
"data_prestatie": "2026-06-15",
"odometru_final": "123456",
"prestatii": [{"cod_prestatie": "OE-1"}],
}
def test_cross_canal_none_equals_1():
"""(a) build_key cu account_id=None si account_id=1 dau aceeasi cheie."""
canon = canonicalize_row(_RAND)
k_none = build_key(None, canon)
k_1 = build_key(1, canon)
assert k_none == k_1, "cross-canal divergenta: None vs 1"
def test_cross_canal_odometru_float():
"""Odometru float din Excel: cheia e identica indiferent de canal."""
rand_float = {**_RAND, "odometru_final": "123456.0"}
rand_int = {**_RAND, "odometru_final": "123456"}
k_float_api = build_key(None, canonicalize_row(rand_float))
k_int_import = build_key(1, canonicalize_row(rand_int))
assert k_float_api == k_int_import, "float vs int odometru -> chei diferite"
# --- idempotency_key wrapper ---
def test_idempotency_key_backward_compat():
"""idempotency_key(None, raw) produce aceeasi cheie ca build_key(None, canon)."""
canon = canonicalize_row(_RAND)
k_new = build_key(None, canon)
k_old = idempotency_key(None, _RAND)
assert k_new == k_old
# --- build_key_legacy (b) ---
def test_legacy_key_differs_from_new():
"""(b) Cheia legacy (account_id=None in hash) difera de cheia noua (account_id=1)."""
canon = canonicalize_row(_RAND)
k_new = build_key(None, canon) # None -> 1 in hash
k_legacy = build_key_legacy(None, _RAND) # None AS-PASSED in hash
assert k_new != k_legacy, "legacy si new trebuie sa difere (diferit account_id in hash)"
def test_legacy_dual_lookup_strategy():
"""Strategia dual-lookup: row-uri vechi (cheie-None) gasite via build_key_legacy."""
# Simuleaza un rand cu cheie veche (account_id=None in hash)
old_key = build_key_legacy(None, _RAND)
# Noul build_key (None->1) NU gaseste randul direct
new_key = build_key(None, canonicalize_row(_RAND))
assert new_key != old_key
# Dual-lookup: incearca noul, apoi legacy
found = old_key in {old_key} or new_key in {old_key}
assert found, "dual-lookup trebuie sa gaseasca randul vechi"
# --- Integrare: API route foloseste build_key (OV-2) ---
@pytest.fixture()
def client(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t9.db"))
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 _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_api_dedup_dupa_t9(client):
"""Deduplicarea functioneaza dupa T9: acelasi rand -> acelasi submission."""
r1 = client.post("/v1/prezentari", json=_body())
r2 = client.post("/v1/prezentari", json=_body())
assert r1.status_code == 200
sid1 = r1.json()["results"][0]["submission_id"]
res2 = r2.json()["results"][0]
assert res2["submission_id"] == sid1
assert res2["deduped"] is True
def test_api_odometru_float_dedup(client):
"""Odometru float '123456.0' si '123456' dedup corect dupa canonicalizare."""
r1 = client.post("/v1/prezentari", json=_body(odometru_final="123456"))
r2 = client.post("/v1/prezentari", json=_body(odometru_final="123456.0"))
assert r1.status_code == 200
sid1 = r1.json()["results"][0]["submission_id"]
res2 = r2.json()["results"][0]
assert res2["submission_id"] == sid1, "odometru float si int trebuie sa dea acelasi submission"
assert res2["deduped"] is True