"""US-011 (PRD 5.15): account-scope pe GET-urile de listare web (securitate). Verifica: - /_fragments/submissions: un cont nu vede randurile altui cont - /_fragments/trimitere/{id}: 404-before-leak pe id strain - /_fragments/nomenclator: necesita autentificare (fragment dashboard, nu endpoint public) - /_fragments/trimiteri-versiune: necesita autentificare - Unauthenticated access -> redirect 303 pe ORICE fragment cu date Legatura cu implementare: mecanismul de scope existent (require_login + account_scope_clause) se reutilizeaza fara logica noua. Nomenclatorul primeste require_login din US-011 (fragment dashboard, nu endpoint public). """ from __future__ import annotations import json import os import re import tempfile import pytest from starlette.testclient import TestClient 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, "csrf_token negasit in pagina de login" resp = client.post("/login", data={"email": email, "parola": password, "csrf_token": m.group(1)}) assert resp.status_code == 303 def _insert_submission( acct: int, status: str = "sent", vin: str = "WVWZZZ1JZXW000777", nr: str = "B777TST", ) -> int: from app.db import get_connection conn = get_connection() try: p = { "vin": vin, "nr_inmatriculare": nr, "data_prestatie": "2026-06-18", "odometru_final": "50000", "prestatii": [{"cod_prestatie": "OE-1", "denumire": "Revizie"}], } cur = conn.execute( "INSERT INTO submissions (idempotency_key, account_id, status, payload_json) " "VALUES (?, ?, ?, ?)", (f"scope-{acct}-{os.urandom(4).hex()}", acct, status, json.dumps(p)), ) conn.commit() rid = cur.lastrowid assert rid is not None return int(rid) finally: conn.close() @pytest.fixture() def client(monkeypatch): tmp = tempfile.mkdtemp() monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "scope.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() # --------------------------------------------------------------------------- # Test 1: cross-account isolation pe listare submissions # --------------------------------------------------------------------------- def test_get_listare_scoped_cont(client): """Un cont NU vede randurile (VIN/PII) ale altui cont in /_fragments/submissions. Contul A are un submission cu nr. inmatriculare unic NR_A; contul B nu trebuie sa vada NR_A in listarea sa. Verificam izolarea atat prin nr. cat si prin VIN (ultimele 6 caractere afisate ca vin_scurt in template). """ NR_A = "BV01SCO" NR_B = "BV02SCO" VIN_A = "WVWZZZ1JZXW111AAA" # vin_scurt va fi '...111AAA' VIN_B = "WVWZZZ1JZXW222BBB" # vin_scurt va fi '...222BBB' acct_a = _create_account_user("scope-a@test.com", name="ContA") acct_b = _create_account_user("scope-b@test.com", name="ContB") _insert_submission(acct_a, vin=VIN_A, nr=NR_A) _insert_submission(acct_b, vin=VIN_B, nr=NR_B) # Login ca cont B _login(client, "scope-b@test.com") resp = client.get("/_fragments/submissions") assert resp.status_code == 200 html = resp.text # Contul B vede propriul nr inmatriculare assert NR_B in html, "Contul B ar trebui sa vada propriul nr inmatriculare" # Contul B NU vede nr inmatriculare si VIN (vin_scurt) ale contului A assert NR_A not in html, "Scurgere cross-account: nr_inmatriculare contului A vizibil contului B" assert "111AAA" not in html, "Scurgere cross-account: VIN (vin_scurt) contului A vizibil contului B" # --------------------------------------------------------------------------- # Test 2: unauthenticated -> redirect pe listare submissions # --------------------------------------------------------------------------- def test_get_listare_neautentificat_redirect_submissions(client): """Fara sesiune activa, /_fragments/submissions returneaza 303 (redirect /login).""" resp = client.get("/_fragments/submissions") assert resp.status_code == 303, ( f"Asteptat 303 redirect, primit {resp.status_code}. " "/_fragments/submissions trebuie sa necesite autentificare." ) # --------------------------------------------------------------------------- # Test 3: 404-before-leak pe detaliu id strain # --------------------------------------------------------------------------- def test_get_detaliu_scoped_404(client): """Detaliul unui submission apartinand altui cont returneaza 404 (fara leak). Acelasi 404 pentru id inexistent = nu confirmam existenta. """ acct_a = _create_account_user("detscope-a@test.com", name="DetA") _create_account_user("detscope-b@test.com", name="DetB") sid_a = _insert_submission(acct_a, vin="WVWZZZ1JZXWAAA111") # Login ca cont B _login(client, "detscope-b@test.com") # Cerere detaliu pe submission-ul contului A resp = client.get(f"/_fragments/trimitere/{sid_a}") assert resp.status_code == 404, ( f"Asteptat 404, primit {resp.status_code}. " "Nu trebuie confirmata existenta unui submission al altui cont." ) # Id inexistent -> acelasi 404 (nu confirmam existenta) resp2 = client.get("/_fragments/trimitere/999999") assert resp2.status_code == 404 # --------------------------------------------------------------------------- # Test 4: nomenclator necesita autentificare (RED inainte de fix) # --------------------------------------------------------------------------- def test_get_nomenclator_neautentificat_redirect(client): """/_fragments/nomenclator este un fragment al dashboard-ului autentificat. Fara sesiune, trebuie sa returneze 303 redirect la /login. RED inainte de fix: in prezent fragmentul nu apeleaza require_login si returneaza 200 chiar fara autentificare. """ resp = client.get("/_fragments/nomenclator") assert resp.status_code == 303, ( f"Asteptat 303 redirect pentru fragment dashboard neautentificat, " f"primit {resp.status_code}. " "/_fragments/nomenclator trebuie sa necesite autentificare." ) def test_get_nomenclator_autentificat_ok(client): """/_fragments/nomenclator accesibil dupa autentificare.""" _create_account_user("nom-auth@test.com", name="NomAuth") _login(client, "nom-auth@test.com") resp = client.get("/_fragments/nomenclator") assert resp.status_code == 200 # --------------------------------------------------------------------------- # Test 5: trimiteri-versiune necesita autentificare # --------------------------------------------------------------------------- def test_get_trimiteri_versiune_neautentificat_redirect(client): """/_fragments/trimiteri-versiune necesita autentificare (redirect 303 fara sesiune).""" resp = client.get("/_fragments/trimiteri-versiune") assert resp.status_code == 303 # --------------------------------------------------------------------------- # Test 6: izolare paginare - filtru nu poate scoate randuri strain # --------------------------------------------------------------------------- def test_get_listare_filtru_nu_sparge_scope(client): """Filtrele (status, vehicul) nu pot scoate randuri din alt cont. Un cont B cu filtru vehicul=VIN_A nu trebuie sa vada niciodata VIN_A. """ VIN_A = "WVWZZZ1JZXW333CCC" NR_A = "BV03FLT" acct_a = _create_account_user("filtru-a@test.com", name="FiltrA") _create_account_user("filtru-b@test.com", name="FiltrB") _insert_submission(acct_a, vin=VIN_A, nr=NR_A) _login(client, "filtru-b@test.com") # Cont B incearca sa filtreze dupa nr inmatriculare al contului A resp = client.get(f"/_fragments/submissions?vehicul={NR_A}") assert resp.status_code == 200 assert NR_A not in resp.text, ( "Filtrul vehicul a scos date din alt cont (scurgere cross-account prin filtru)." )