feat(5.15+5.14): CLOSE — fix-uri code-review + embeddings functional
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>
This commit is contained in:
226
tests/test_api_scope.py
Normal file
226
tests/test_api_scope.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""US-011 (PRD 5.15): account-scope pe GET-urile de listare API (securitate).
|
||||
|
||||
Verifica:
|
||||
- GET /v1/prezentari: un cont nu vede submissions ale altui cont
|
||||
- GET /v1/prezentari/{id}: 404-before-leak pe id strain
|
||||
- Unauthenticated access cu require_api_key=True -> 401
|
||||
- Filtre si paginare nu sparg scope-ul
|
||||
|
||||
Legatura cu implementare: resolve_account_id + account_scope_clause (mecanisme
|
||||
existente, reutilizate). Testele LOCK DOWN comportamentul deja implementat
|
||||
si verifica alinierea cu AUTOPASS_REQUIRE_API_KEY (dev vs prod).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
|
||||
def _create_account_with_key(name: str):
|
||||
"""Creeaza cont + cheie API. Intoarce (account_id, plaintext_key)."""
|
||||
from app.accounts import create_account
|
||||
from app.auth import create_api_key
|
||||
from app.db import get_connection
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
acct_id = create_account(conn, name, active=True)
|
||||
key = create_api_key(conn, acct_id)
|
||||
conn.commit()
|
||||
return acct_id, key
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _insert_submission(acct: int, vin: str = "WVWZZZ1JZXW000777") -> int:
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
p = {
|
||||
"vin": vin,
|
||||
"nr_inmatriculare": "B777TST",
|
||||
"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"api-scope-{acct}-{os.urandom(4).hex()}", acct, "sent", json.dumps(p)),
|
||||
)
|
||||
conn.commit()
|
||||
rid = cur.lastrowid
|
||||
assert rid is not None
|
||||
return int(rid)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(monkeypatch):
|
||||
"""Client cu DB izolata, REQUIRE_API_KEY=false (dev default)."""
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "api-scope.db"))
|
||||
monkeypatch.setenv("AUTOPASS_REQUIRE_API_KEY", "false")
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
from app.main import app
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client_prod(monkeypatch):
|
||||
"""Client cu require_api_key=True (comportament productie)."""
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "api-scope-prod.db"))
|
||||
monkeypatch.setenv("AUTOPASS_REQUIRE_API_KEY", "true")
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
from app.main import app
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 1: cross-account isolation pe listare submissions (dev mode)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_get_listare_scoped_cont(client):
|
||||
"""Un cont NU vede submissions (VIN/PII) ale altui cont in GET /v1/prezentari.
|
||||
|
||||
Contul A are un submission cu VIN_A; contul B cu cheie proprie nu trebuie
|
||||
sa vada VIN_A in listarea sa.
|
||||
"""
|
||||
VIN_A = "WVWZZZ1JZXWAPI1AA"
|
||||
VIN_B = "WVWZZZ1JZXWAPI2BB"
|
||||
|
||||
acct_a, key_a = _create_account_with_key("ApiContA")
|
||||
acct_b, key_b = _create_account_with_key("ApiContB")
|
||||
_insert_submission(acct_a, vin=VIN_A)
|
||||
_insert_submission(acct_b, vin=VIN_B)
|
||||
|
||||
# Cont B listeaza cu propria cheie
|
||||
resp = client.get("/v1/prezentari", headers={"X-API-Key": key_b})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
|
||||
vins = [s["prezentare"]["vin"] for s in data["submissions"] if s.get("prezentare")]
|
||||
assert VIN_B in vins, "Contul B ar trebui sa vada propriul submission"
|
||||
assert VIN_A not in vins, (
|
||||
"Scurgere cross-account: VIN-ul contului A vizibil contului B prin API"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 2: unauthenticated 401 in prod mode
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_get_listare_neautentificat_401(client_prod):
|
||||
"""Fara cheie API cu require_api_key=True, GET /v1/prezentari -> 401."""
|
||||
resp = client_prod.get("/v1/prezentari")
|
||||
assert resp.status_code == 401, (
|
||||
f"Asteptat 401 fara cheie API in mod prod, primit {resp.status_code}."
|
||||
)
|
||||
|
||||
|
||||
def test_get_listare_cheie_invalida_401(client_prod):
|
||||
"""Cheie API invalida (prezenta dar gresita) -> 401, indiferent de flag."""
|
||||
resp = client_prod.get("/v1/prezentari", headers={"X-API-Key": "rfak_invalida_xxx"})
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 3: 404-before-leak pe detaliu id strain
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_get_detaliu_scoped_404(client):
|
||||
"""GET /v1/prezentari/{id} pe un id al altui cont -> 404 (fara leak).
|
||||
|
||||
Acelasi 404 pentru id inexistent = nu confirmam existenta.
|
||||
"""
|
||||
acct_a, key_a = _create_account_with_key("DetApiA")
|
||||
acct_b, key_b = _create_account_with_key("DetApiB")
|
||||
sid_a = _insert_submission(acct_a, vin="WVWZZZ1JZXWDET100")
|
||||
|
||||
# Cont B incearca sa acceseze submission-ul contului A
|
||||
resp = client.get(f"/v1/prezentari/{sid_a}", headers={"X-API-Key": key_b})
|
||||
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
|
||||
resp2 = client.get("/v1/prezentari/999999", headers={"X-API-Key": key_b})
|
||||
assert resp2.status_code == 404
|
||||
|
||||
|
||||
def test_get_detaliu_neautentificat_401(client_prod):
|
||||
"""GET /v1/prezentari/{id} fara cheie API in prod -> 401."""
|
||||
resp = client_prod.get("/v1/prezentari/1")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 4: contul implicit id=1 in dev (nu trebuie spart de scope)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_get_listare_cont_implicit_dev(client):
|
||||
"""In dev (require_api_key=False), fara cheie -> cont implicit id=1.
|
||||
|
||||
Contul 1 vede propriile submissions (NULL account_id = cont 1 prin
|
||||
account_scope_clause). NU trebuie sa vada submissions cu alt account_id.
|
||||
"""
|
||||
# Inserare submission pt cont id=1 (account_id NULL = legacy cont 1)
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
p = json.dumps({
|
||||
"vin": "WVWZZZ1JZXWDEV001",
|
||||
"nr_inmatriculare": "B001DEV",
|
||||
"data_prestatie": "2026-06-18",
|
||||
"odometru_final": "10000",
|
||||
"prestatii": [{"cod_prestatie": "OE-1"}],
|
||||
})
|
||||
conn.execute(
|
||||
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json) "
|
||||
"VALUES (?, NULL, 'sent', ?)",
|
||||
("dev-null-key-001", p),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# Fara cheie in dev -> cont implicit 1, vede submission-ul cu account_id NULL
|
||||
resp = client.get("/v1/prezentari")
|
||||
assert resp.status_code == 200
|
||||
vins = [s["prezentare"]["vin"] for s in resp.json()["submissions"] if s.get("prezentare")]
|
||||
assert "WVWZZZ1JZXWDEV001" in vins, "Contul implicit 1 trebuie sa vada submissions NULL"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 5: izolare status filter nu sparge scope-ul
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_get_listare_filtru_status_nu_sparge_scope(client):
|
||||
"""Filtrul ?status= nu poate scoate randuri din alt cont."""
|
||||
VIN_A = "WVWZZZ1JZXWSTA1AA"
|
||||
|
||||
acct_a, key_a = _create_account_with_key("StatApiA")
|
||||
acct_b, key_b = _create_account_with_key("StatApiB")
|
||||
_insert_submission(acct_a, vin=VIN_A)
|
||||
|
||||
# Cont B filtreaza dupa status 'sent' - nu trebuie sa vada VIN_A
|
||||
resp = client.get("/v1/prezentari?status=sent", headers={"X-API-Key": key_b})
|
||||
assert resp.status_code == 200
|
||||
vins = [s["prezentare"]["vin"] for s in resp.json()["submissions"] if s.get("prezentare")]
|
||||
assert VIN_A not in vins, (
|
||||
"Filtrul status a scos date din alt cont (scurgere cross-account prin filtru)."
|
||||
)
|
||||
Reference in New Issue
Block a user