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:
Claude Agent
2026-06-28 20:48:34 +00:00
parent 9e42e7ed6f
commit 3fc53534e2
53 changed files with 9684 additions and 384 deletions

226
tests/test_api_scope.py Normal file
View 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)."
)