"""Teste US-006 (PRD 5.10): editare operatie RAR (cod_prestatie) din formularul de detaliu. Stari editabile: needs_data, needs_mapping (stari cu formular de corectie activ). Read-only: sent/sending/queued/error (fara select cod_prestatie). Cazuri: - test_editabil_arata_select_cod_rar: detaliu needs_data → HTML are """ from __future__ import annotations import json import os import re import tempfile import pytest from starlette.testclient import TestClient # VIN valid: 17 caractere, fara I/O/Q VIN_US006 = "WVWZZZ1JZXW0E6001" # Payload complet valid (trece validate_prezentare) PAYLOAD_VALID = { "vin": VIN_US006, "nr_inmatriculare": "B100AAA", "data_prestatie": "2026-06-10", "odometru_final": "55000", "prestatii": [{"cod_prestatie": "OE-1"}], } def _create_account_user(email: str, name: str = "Service", password: str = "parolasecreta10"): from app.accounts import create_account from app.users import create_user from app.db import get_connection conn = get_connection() try: acct_id = create_account(conn, name, active=True) create_user(conn, acct_id, email, password) return acct_id finally: conn.close() def _login(client, email: str, password: str = "parolasecreta10") -> None: resp = client.get("/login") m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text) or \ re.search(r'value="([^"]+)"\s+name="csrf_token"', resp.text) assert m resp = client.post("/login", data={"email": email, "parola": password, "csrf_token": m.group(1)}) assert resp.status_code == 303 def _ins(acct: int, *, status: str, payload: dict | None = None) -> int: from app.db import get_connection conn = get_connection() try: cur = conn.execute( "INSERT INTO submissions (idempotency_key, account_id, status, payload_json) VALUES (?, ?, ?, ?)", (f"k-us006-{os.urandom(6).hex()}", acct, status, json.dumps(payload or PAYLOAD_VALID)), ) conn.commit() return int(cur.lastrowid) finally: conn.close() def _ins_nomenclator(*codes: str) -> None: """Insereaza coduri RAR in nomenclator_rar (tabelul e gol in DB-ul de test).""" from app.db import get_connection conn = get_connection() try: for cod in codes: conn.execute( "INSERT OR IGNORE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?, ?)", (cod, f"Operatie test {cod}"), ) conn.commit() finally: conn.close() def _row(sid: int): from app.db import get_connection conn = get_connection() try: return conn.execute("SELECT * FROM submissions WHERE id=?", (sid,)).fetchone() finally: conn.close() def _csrf(client) -> str: """CSRF token din pagina principala (sesiune activa necesara).""" resp = client.get("/") m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text) assert m, f"CSRF token negasit in pagina principala: {resp.text[:500]}" return m.group(1) def _detaliu(client, sid: int) -> str: resp = client.get(f"/_fragments/trimitere/{sid}") assert resp.status_code == 200 return resp.text @pytest.fixture() def client(monkeypatch): tmp = tempfile.mkdtemp() monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "editare_rar.db")) monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "true") from app.config import get_settings get_settings.cache_clear() from app.web import ratelimit ratelimit._hits.clear() from app.main import app with TestClient(app, follow_redirects=False) as c: yield c ratelimit._hits.clear() get_settings.cache_clear() def test_editabil_arata_select_cod_rar(client): """needs_data cu nomenclator populat → formularul de detaliu afiseaza in detaliu (read-only).""" acct = _create_account_user("ro5@test.com") _ins_nomenclator("OE-1", "OE-2") sid = _ins(acct, status="sent") _login(client, "ro5@test.com") html = _detaliu(client, sid) assert 'name="cod_prestatie"' not in html, ( "Starea sent trebuie sa fie read-only (fara select cod_prestatie)" ) # ================================================================ # US-006b: extindere la starea error # ================================================================ def test_error_arata_select_cod_rar(client): """needs_data/needs_mapping primeau select (US-006); error trebuie sa primeasca si el un select cod_prestatie in formularul 'Re-pune in coada' (US-006b).""" acct = _create_account_user("err1@test.com") _ins_nomenclator("OE-1", "OE-2") sid = _ins(acct, status="error") _login(client, "err1@test.com") html = _detaliu(client, sid) assert 'name="cod_prestatie"' in html, ( "Starea error trebuie sa afiseze un select cod_prestatie (US-006b)" ) assert " trebuie sa apara in detaliu pentru error" # Codurile din nomenclator trebuie sa fie in optiuni assert "OE-1" in html and "OE-2" in html, ( "Codurile din nomenclator trebuie sa apara in select-ul pentru error" ) # NU trebuie sa afiseze formularul complet de corectie (fara /corecteaza) assert f"/trimitere/{sid}/corecteaza" not in html, ( "Starea error NU trebuie sa aiba formular /corecteaza (US-006b foloseste /repune)" ) # Butonul principal ramane 'Re-pune in coada' (nu 'Salveaza si retrimite') assert "Re-pune in coada" in html assert "Salveaza si retrimite" not in html def test_error_salvare_schimba_cod_si_repune_in_coada(client): """POST /repune cu cod_prestatie=OE-2 pe un rand error → payload actualizat + status=queued.""" acct = _create_account_user("err2@test.com") _ins_nomenclator("OE-1", "OE-2") sid = _ins(acct, status="error") _login(client, "err2@test.com") csrf = _csrf(client) resp = client.post( f"/trimitere/{sid}/repune", data={"csrf_token": csrf, "cod_prestatie": "OE-2"}, ) assert resp.status_code == 200 row = _row(sid) assert row["status"] == "queued", ( f"Dupa repune cu OE-2, randul trebuie sa fie queued, nu '{row['status']}'" ) payload = json.loads(row["payload_json"]) prestatii = payload.get("prestatii") or [] assert prestatii, "Payload-ul trebuie sa contina cel putin o prestatie" cod_nou = (prestatii[0].get("cod_prestatie") or "").strip().upper() assert cod_nou == "OE-2", ( f"cod_prestatie in payload trebuie sa fie OE-2, nu '{cod_nou}'" ) def test_error_idempotency_key_se_schimba(client): """Schimbarea cod_prestatie (OE-1 → OE-2) la repune recalculeaza cheia de idempotency. Randul e inserat CU CHEIA CANONICA pentru OE-1 (nu random), ca sa fie RED inainte de implementare: fara injectare, repune nu schimba cheia (ramane OE-1) → test FAIL. Dupa implementare, POST cu OE-2 → cheie noua (canonicala cu OE-2) ≠ cheie OE-1. """ from app.idempotency import build_key, canonicalize_row from app.db import get_connection as _gc acct = _create_account_user("err3@test.com") _ins_nomenclator("OE-1", "OE-2") # Calculeaza cheia canonicala pentru OE-1 si insereaza randul CU acea cheie. canon_oe1 = canonicalize_row({**PAYLOAD_VALID, "prestatii": [{"cod_prestatie": "OE-1"}]}) key_oe1 = build_key(acct, canon_oe1) # Inserare cu cheia cunoscuta (nu random), ca sa avem un baseline deterministic. conn = _gc() try: cur = conn.execute( "INSERT INTO submissions (idempotency_key, account_id, status, payload_json) VALUES (?, ?, ?, ?)", (key_oe1, acct, "error", json.dumps({**PAYLOAD_VALID, "prestatii": [{"cod_prestatie": "OE-1"}]})), ) conn.commit() sid = int(cur.lastrowid) finally: conn.close() _login(client, "err3@test.com") csrf = _csrf(client) # POST /repune cu OE-2 → implementarea trebuie sa recalculeze cheia client.post( f"/trimitere/{sid}/repune", data={"csrf_token": csrf, "cod_prestatie": "OE-2"}, ) cheie_noua = _row(sid)["idempotency_key"] assert cheie_noua != key_oe1, ( "Cheia idempotency trebuie sa difere dupa schimbarea cod_prestatie la repune (US-006b). " "Daca sunt egale, cod_prestatie nu a fost injectat inainte de build_key." ) def test_queued_nu_arata_select(client): """Trimitere queued → fara select cod_prestatie (read-only; doar error/needs_* primesc select).""" acct = _create_account_user("ro6@test.com") _ins_nomenclator("OE-1", "OE-2") sid = _ins(acct, status="queued") _login(client, "ro6@test.com") html = _detaliu(client, sid) assert 'name="cod_prestatie"' not in html, ( "Starea queued trebuie sa fie read-only (fara select cod_prestatie)" )