"""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