5.15 (propagare design + dashboard editare) si 5.14 (mapare LLM distilata) inchise dupa /code-review high. 8 buguri reparate TDD: - HIGH modal nu se deschidea pe randul slim (base.html: trimitere-slim) - HIGH /repune trunchia prestatii (declaratie incompleta la RAR) -> iterare peste existing, codes pozitional - HIGH embeddings incarca model ~230MB degeaba pe corpus gol -> poarta has_corpus() - HIGH picker chips gol pe re-render eroare -> conn/account_id pe toate ramurile - MED obs re-derivat dupa stergere explicita -> _merge_override pastreaza obs='' - MED mapare salvata fara denumire poluă GOLD -> _record_gold_validation guard - MED typo nome_prestatie -> nume_prestatie in select /repune - MED bucketare timp +3h gresita iarna -> SQLite localtime + TZ=Europe/Bucharest Embeddings WIRE-uit functional (PRD #15, decizie user): ensure_embeddings_corpus construieste corpus din nomenclator, gated pe AUTOPASS_EMBEDDINGS_ENABLED (default off). Marime model corectata ~50MB->~230MB (estimare PRD gresita). Cleanup: hoist load_* din bucla bulk-fix; import re la top. Regresie: 1256 passed, 1 deselected (live), 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
232 lines
8.7 KiB
Python
232 lines
8.7 KiB
Python
"""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)."
|
|
)
|