"""Teste US-001 (PRD 3.6): backend — persista editarea unui rand de preview. Approach B: editarea scrie un patch CANONIC in import_rows.override_json (criptat Fernet), aplicat ULTIMUL in _resolve_row_for_preview + commit. raw_json/idempotency raman neatinse. Ruta editare = mutatie PURA (nu re-deriva status, nu atinge submissions). TDD: testele se scriu inainte de implementare (RED -> GREEN). """ from __future__ import annotations import io import os import pathlib import tempfile import pytest from fastapi.testclient import TestClient _FIXTURES = pathlib.Path(__file__).parent / "fixtures" @pytest.fixture() def client(monkeypatch): tmp = tempfile.mkdtemp() monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "edit.db")) from app.config import get_settings get_settings.cache_clear() from app.crypto import reset_cache reset_cache() from app.main import app with TestClient(app) as c: yield c get_settings.cache_clear() reset_cache() # --------------------------------------------------------------------------- # # Helpere # # --------------------------------------------------------------------------- # def _seed_op1(account_id: int = 1) -> None: """Nomenclator + mapare OP-1 -> R-FRANE (auto_send=1) pentru contul dat.""" from app.db import get_connection conn = get_connection() try: conn.execute( "INSERT OR REPLACE INTO nomenclator_rar (cod_prestatie, nume_prestatie) " "VALUES ('R-FRANE','Reparatie frane')" ) conn.execute( "INSERT OR IGNORE INTO operations_mapping (account_id, cod_op_service, cod_prestatie, auto_send) " "VALUES (?, 'OP-1', 'R-FRANE', 1)", (account_id,), ) conn.commit() finally: conn.close() def _upload(client: TestClient, fixture: str, headers: dict | None = None) -> int: data = (_FIXTURES / fixture).read_bytes() r = client.post( "/v1/import", files={"file": (fixture, io.BytesIO(data), "text/csv")}, headers=headers or {}, ) assert r.status_code == 200, r.text return int(r.json()["import_id"]) def _save_mapping(client: TestClient, import_id: int, json_mapare: dict, headers: dict | None = None) -> None: r = client.post( f"/v1/import/{import_id}/column-mapping", json={"json_mapare": json_mapare, "format_data": "YYYY-MM-DD"}, headers=headers or {}, ) assert r.status_code == 200, r.text def _preview(client: TestClient, import_id: int, headers: dict | None = None) -> list[dict]: r = client.get(f"/v1/import/{import_id}/preview", headers=headers or {}) assert r.status_code == 200, r.text return r.json()["rows"] def _status_for(rows: list[dict], row_index: int) -> str: return next(r["resolved_status"] for r in rows if r["row_index"] == row_index) def _resolved_for(rows: list[dict], row_index: int) -> dict: return next(r["resolved"] for r in rows if r["row_index"] == row_index) _MAP_NECANONIC = { "Serie sasiu": "vin", "Nr": "nr_inmatriculare", "Data": "data_prestatie", "KM": "odometru_final", "Operatie": "operatie", } _MAP_LIPSA = { "Serie sasiu": "vin", "Nr": "nr_inmatriculare", "KM": "odometru_final", "Operatie": "operatie", } # --------------------------------------------------------------------------- # # Teste # # --------------------------------------------------------------------------- # def test_editeaza_rand_antet_necanonic_devine_ok(client): """Fisier cu antet ne-canonic (Serie sasiu/Data): rand needs_data -> editez data -> ok. Prinde bug-ul de stocare: o editare pe cheia canonica trebuie sa se aplice chiar daca raw_json e cheiat pe anteturi ne-canonice. """ _seed_op1() iid = _upload(client, "import_antet_necanonic.csv") _save_mapping(client, iid, _MAP_NECANONIC) assert _status_for(_preview(client, iid), 0) == "needs_data" r = client.post(f"/v1/import/{iid}/rand/0/editeaza", json={"data_prestatie": "2026-06-10"}) assert r.status_code == 200, r.text assert _status_for(_preview(client, iid), 0) == "ok" def test_editeaza_completeaza_coloana_absenta(client): """Fisier FARA coloana de data: editarea adauga data_prestatie -> ok. Demonstreaza ca override poate exprima un camp a carui coloana LIPSESTE din fisier. """ _seed_op1() iid = _upload(client, "import_lipsa_coloana.csv") _save_mapping(client, iid, _MAP_LIPSA) assert _status_for(_preview(client, iid), 0) == "needs_data" r = client.post(f"/v1/import/{iid}/rand/0/editeaza", json={"data_prestatie": "2026-06-10"}) assert r.status_code == 200, r.text assert _status_for(_preview(client, iid), 0) == "ok" def test_editeaza_status_identic_cu_GET_preview(client): """Ruta editare NU re-deriva status (intoarce doar override); statusul vine din GET preview.""" _seed_op1() iid = _upload(client, "import_antet_necanonic.csv") _save_mapping(client, iid, _MAP_NECANONIC) r = client.post(f"/v1/import/{iid}/rand/0/editeaza", json={"data_prestatie": "2026-06-10"}) assert r.status_code == 200 body = r.json() # Mutatie pura: raspunsul nu contine un status rederivat in ruta. assert "resolved_status" not in body assert body.get("override", {}).get("data_prestatie") == "2026-06-10" # Statusul se rederiva DOAR prin preview. assert _status_for(_preview(client, iid), 0) == "ok" def test_editeaza_rand_scoped_alt_cont_404(client): """Editarea unui rand al altui cont -> 404 (scoping JOIN, fara leak).""" from app.db import get_connection from app.accounts import create_account from app.auth import create_api_key _seed_op1() iid = _upload(client, "import_antet_necanonic.csv") # cont 1 (default) _save_mapping(client, iid, _MAP_NECANONIC) conn = get_connection() try: acct2 = create_account(conn, "Alt cont", active=True) key2 = create_api_key(conn, acct2) conn.commit() finally: conn.close() r = client.post( f"/v1/import/{iid}/rand/0/editeaza", json={"data_prestatie": "2026-06-10"}, headers={"X-API-Key": key2}, ) assert r.status_code == 404 def test_editeaza_batch_inexistent_404(client): r = client.post("/v1/import/99999/rand/0/editeaza", json={"vin": "WVWZZZ1KZAW000123"}) assert r.status_code == 404 def test_editeaza_row_index_invalid_pe_batch_valid_404(client): _seed_op1() iid = _upload(client, "import_antet_necanonic.csv") _save_mapping(client, iid, _MAP_NECANONIC) r = client.post(f"/v1/import/{iid}/rand/9999/editeaza", json={"vin": "WVWZZZ1KZAW000123"}) assert r.status_code == 404 def test_editeaza_pastreaza_campuri_neatinse(client): """Editarea unui camp nu pierde prestatiile/operatia rezolvate ale randului.""" _seed_op1() iid = _upload(client, "import_antet_necanonic.csv") _save_mapping(client, iid, _MAP_NECANONIC) client.post(f"/v1/import/{iid}/rand/0/editeaza", json={"data_prestatie": "2026-06-10"}) res = _resolved_for(_preview(client, iid), 0) prestatii = res.get("prestatii") or [] assert prestatii, "prestatiile au disparut dupa editare" assert (prestatii[0].get("cod_prestatie") or prestatii[0].get("cod_op_service")) in ("R-FRANE", "OP-1") def test_editeaza_batch_committed_409(client): """Editarea pe un batch deja comis -> 409 (nu mai are efect downstream).""" from app.db import get_connection _seed_op1() iid = _upload(client, "import_antet_necanonic.csv") _save_mapping(client, iid, _MAP_NECANONIC) conn = get_connection() try: conn.execute("UPDATE import_batches SET status='committed' WHERE id=?", (iid,)) conn.commit() finally: conn.close() r = client.post(f"/v1/import/{iid}/rand/0/editeaza", json={"data_prestatie": "2026-06-10"}) assert r.status_code == 409 def test_editeaza_raw_corupt_no_op(client): """Override curent ilizibil (decrypt fail) -> 422/no-op, fara scriere goala, fara crash.""" from app.db import get_connection _seed_op1() iid = _upload(client, "import_antet_necanonic.csv") _save_mapping(client, iid, _MAP_NECANONIC) conn = get_connection() try: conn.execute( "UPDATE import_rows SET override_json=? WHERE batch_id=? AND row_index=0", ("token-corupt-nedecriptabil", iid), ) conn.commit() finally: conn.close() r = client.post(f"/v1/import/{iid}/rand/0/editeaza", json={"data_prestatie": "2026-06-10"}) assert r.status_code == 422 # override_json NU a fost suprascris cu o valoare goala conn = get_connection() try: oj = conn.execute( "SELECT override_json FROM import_rows WHERE batch_id=? AND row_index=0", (iid,) ).fetchone()["override_json"] finally: conn.close() assert oj == "token-corupt-nedecriptabil" def test_editeaza_empty_input_sterge_campul(client): """Semantica empty: input gol = STERGE cheia din override (revine la valoarea din fisier).""" _seed_op1() iid = _upload(client, "import_antet_necanonic.csv") _save_mapping(client, iid, _MAP_NECANONIC) # Randul 1 are odometru_final 55000 in fisier. Suprascriem cu 77000 prin override. client.post(f"/v1/import/{iid}/rand/1/editeaza", json={"odometru_final": "77000"}) assert _resolved_for(_preview(client, iid), 1).get("odometru_final") == "77000" # Input gol -> sterge override-ul -> revine la valoarea din fisier (55000). client.post(f"/v1/import/{iid}/rand/1/editeaza", json={"odometru_final": ""}) assert _resolved_for(_preview(client, iid), 1).get("odometru_final") == "55000"