PRD 5.16 — propagare design finalizata (system font stack, fara IBM Plex self-hostat): - US-001/002/008: tokeni --font-ui/--font-mono (system stack) + scala --fs-*; zero @font-face si zero /static/fonts/; landing aliniat la acelasi stack - US-003: RAR online = dot compact in antet + meniu burger; banda rosie DOAR pe blocat (invariant zero-silent-failures pastrat) - US-010: antet "ROMFAST AUTOPASS" + nume service + /login brandeit 2 coloane + badge plan; meniu burger cu separatoare; gate strict pe is_authenticated - US-011: selector tema pill icon+eticheta (reuse THEMES) - US-004/005/006/007: bug-fix editor prestatii (picker cod+denumire, add_extra in mod operatii, cod ales se salveaza fara "+", Renunta inchide via closest) - US-012/013: landing Autentificare->/login; wizard import colapsat + 4 pasi pe tokeni - fix VERIFY E2E: contoare duplicate pe 390px (inline display:flex batea @media) -> CSS + test-lock PRD 5.17 — tipuri de cont + trial Pro 30z + enforcement DUR: - US-001/002/008: accounts.tier + trial_until (migrare aditiva defensiva); app/plans.py sursa unica (PLANS, FREE_MONTHLY_LIMIT=60, effective_tier(now injectabil), monthly_usage, CONSUMED_STATUSES); create_account trial Pro 30z; CLI set-tier (protejat id=1, audit) - US-003/004/005: enforce volum 60/luna INAINTE de build_key pe ambele canale (PLAN_LIMITA_LUNARA, 3 niveluri + log_event); gate API Pro+ (PLAN_FARA_API 403 actionabil); valideaza/nomenclator raman permise; downgrade lazy; flag AUTOPASS_ENFORCE_PLANS (kill-switch) - US-006: badge plan antet + linie burger + consum N/60 + warn>=80% + 6 stari + copy RO pluralizat + banner one-time trial->Gratuit + pagina Cont Regresie: 1380 passed, 0 failed, 1 deselected (live). E2E browser pe 390/1280 confirmat. Backend trimitere (worker/masina stari/idempotenta/contract RAR) NEATINS. Lucrul 5.18 (corpus kNN) ramane separat, necomis. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
533 lines
20 KiB
Python
533 lines
20 KiB
Python
"""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)."
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# PRD 5.17 — enforcement planuri: gate API (T4) + volum (T3) + kill-switch (T5)
|
|
# ============================================================================
|
|
|
|
_PREZ_PLAN = {
|
|
"vin": "WVWZZZ1KZAW900001",
|
|
"nr_inmatriculare": "B900TST",
|
|
"data_prestatie": "2026-06-15",
|
|
"odometru_final": "50000",
|
|
"prestatii": [{"cod_op_service": "OP-PLAN-T4", "denumire": "Test plan gate"}],
|
|
}
|
|
|
|
|
|
def _set_tier_acct(account_id: int, tier: str, trial_until=None) -> None:
|
|
"""Seteaza tier si trial_until pe un cont existent."""
|
|
from app.db import get_connection
|
|
conn = get_connection()
|
|
try:
|
|
conn.execute(
|
|
"UPDATE accounts SET tier=?, trial_until=? WHERE id=?",
|
|
(tier, trial_until, account_id),
|
|
)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _insert_n_submissions(account_id: int, n: int) -> None:
|
|
"""Insereaza N submissions queued in luna curenta pentru cont."""
|
|
from datetime import datetime, timezone
|
|
from app.db import get_connection
|
|
conn = get_connection()
|
|
try:
|
|
now_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
for i in range(n):
|
|
conn.execute(
|
|
"INSERT INTO submissions "
|
|
"(idempotency_key, account_id, status, payload_json, created_at) "
|
|
"VALUES (?, ?, 'queued', '{}', ?)",
|
|
(f"plan-seed-{account_id}-{i}-{os.urandom(4).hex()}", account_id, now_str),
|
|
)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# T4: Gate API — free/standard NU pot accesa rutele de ingestie API
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_free_fara_api_403(client):
|
|
"""Cont free (non-default, cu cheie API) → 403 PLAN_FARA_API pe POST /v1/prezentari.
|
|
|
|
T4 PRD 5.17: gate API refuza conturi cu api_access=False in PLANS.
|
|
"""
|
|
acct_id, key = _create_account_with_key("FreePlanGate")
|
|
_set_tier_acct(acct_id, "free", trial_until=None)
|
|
|
|
resp = client.post(
|
|
"/v1/prezentari",
|
|
json={"prezentari": [_PREZ_PLAN]},
|
|
headers={"X-API-Key": key},
|
|
)
|
|
assert resp.status_code == 403, f"Asteptat 403, primit {resp.status_code}: {resp.text}"
|
|
detail = resp.json().get("detail", {})
|
|
assert detail.get("cod") == "PLAN_FARA_API", f"Cod eroare gresit: {detail}"
|
|
|
|
|
|
def test_standard_fara_api_403(client):
|
|
"""Cont standard (non-default) → 403 PLAN_FARA_API pe POST /v1/prezentari.
|
|
|
|
Standard are api_access=False, deci gate-ul API respinge la fel ca free.
|
|
"""
|
|
acct_id, key = _create_account_with_key("StdPlanGate")
|
|
_set_tier_acct(acct_id, "standard", trial_until=None)
|
|
|
|
resp = client.post(
|
|
"/v1/prezentari",
|
|
json={"prezentari": [_PREZ_PLAN]},
|
|
headers={"X-API-Key": key},
|
|
)
|
|
assert resp.status_code == 403, f"Asteptat 403, primit {resp.status_code}"
|
|
assert resp.json().get("detail", {}).get("cod") == "PLAN_FARA_API"
|
|
|
|
|
|
def test_pro_api_ok(client):
|
|
"""Cont pro (non-default) → 200 pe POST /v1/prezentari (trece gate-ul API)."""
|
|
acct_id, key = _create_account_with_key("ProPlanGate")
|
|
_set_tier_acct(acct_id, "pro", trial_until=None)
|
|
|
|
resp = client.post(
|
|
"/v1/prezentari",
|
|
json={"prezentari": [_PREZ_PLAN]},
|
|
headers={"X-API-Key": key},
|
|
)
|
|
assert resp.status_code == 200, (
|
|
f"Pro trebuie sa aiba acces API, primit: {resp.status_code}: {resp.text}"
|
|
)
|
|
|
|
|
|
def test_trial_pro_api_ok(client):
|
|
"""Cont free cu trial Pro activ → 200 pe POST /v1/prezentari.
|
|
|
|
effective_tier() intoarce 'pro' cand trial_until > now, deci gate-ul permite.
|
|
"""
|
|
from datetime import datetime, timedelta, timezone
|
|
acct_id, key = _create_account_with_key("TrialPlanGate")
|
|
trial_until = (datetime.now(timezone.utc) + timedelta(days=15)).strftime("%Y-%m-%d %H:%M:%S")
|
|
_set_tier_acct(acct_id, "free", trial_until=trial_until)
|
|
|
|
resp = client.post(
|
|
"/v1/prezentari",
|
|
json={"prezentari": [_PREZ_PLAN]},
|
|
headers={"X-API-Key": key},
|
|
)
|
|
assert resp.status_code == 200, (
|
|
f"Trial Pro trebuie sa treaca gate-ul API, primit: {resp.status_code}"
|
|
)
|
|
|
|
|
|
def test_dry_run_valideaza_ramane_permis(client):
|
|
"""POST /v1/prezentari/valideaza (dry-run) e permis pe orice plan, inclusiv free.
|
|
|
|
Decizie design (PRD 5.17): valideaza nu face enqueue si nu consuma cota,
|
|
deci NU e protejat de gate-ul API — integratorii pot testa fara upgrade.
|
|
"""
|
|
acct_id, key = _create_account_with_key("FreeValideazaTest")
|
|
_set_tier_acct(acct_id, "free", trial_until=None)
|
|
|
|
resp = client.post(
|
|
"/v1/prezentari/valideaza",
|
|
json={"prezentari": [_PREZ_PLAN]},
|
|
headers={"X-API-Key": key},
|
|
)
|
|
assert resp.status_code == 200, (
|
|
f"valideaza trebuie permis pe plan free (fara gate API), "
|
|
f"primit {resp.status_code}: {resp.text}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# T3: Enforce volum — limita 60/luna pe planul Gratuit
|
|
# Dev account (id=1) are bypass la gate-ul API, dar NU la verificarea de volum
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_free_peste_60_respins_api(client):
|
|
"""Dev account (id=1) la 60/60 pe free → a 61-a prezentare respinsa 422 PLAN_LIMITA_LUNARA.
|
|
|
|
Dev account (id=1) in dev mode (require_api_key=False) are bypass la gate-ul API (T4),
|
|
dar NU la verificarea de volum (T3). La 60/60, urmatoarea cerere trebuie respinsa 422.
|
|
"""
|
|
_set_tier_acct(1, "free", trial_until=None)
|
|
_insert_n_submissions(1, 60)
|
|
|
|
# A 61-a cerere (fara cheie = cont 1 in dev mode, bypass gate API, dar volumul e plin)
|
|
resp = client.post("/v1/prezentari", json={"prezentari": [_PREZ_PLAN]})
|
|
assert resp.status_code == 422, (
|
|
f"Asteptat 422 la depasire volum, primit {resp.status_code}: {resp.text}"
|
|
)
|
|
detail = resp.json().get("detail", {})
|
|
assert detail.get("cod") == "PLAN_LIMITA_LUNARA", f"Cod gresit: {detail}"
|
|
|
|
|
|
def test_eroare_3_niveluri_plan_limita(client):
|
|
"""Eroarea PLAN_LIMITA_LUNARA contine toate 3 nivelurile: cod, problema, cauza, fix.
|
|
|
|
Pattern standard (PRD 5.17): eroare structurata pe 3 niveluri — nu 500, nu catch-all.
|
|
"""
|
|
_set_tier_acct(1, "free", trial_until=None)
|
|
_insert_n_submissions(1, 60)
|
|
|
|
resp = client.post("/v1/prezentari", json={"prezentari": [_PREZ_PLAN]})
|
|
assert resp.status_code == 422
|
|
detail = resp.json().get("detail", {})
|
|
assert detail.get("cod") == "PLAN_LIMITA_LUNARA", f"Camp 'cod' gresit: {detail}"
|
|
assert detail.get("problema"), f"Camp 'problema' lipsa: {detail}"
|
|
assert detail.get("cauza"), f"Camp 'cauza' lipsa: {detail}"
|
|
assert detail.get("fix"), f"Camp 'fix' lipsa: {detail}"
|
|
|
|
|
|
def test_pro_si_trial_nelimitat(client):
|
|
"""Pro si trial Pro nu sunt blocati de volum indiferent de numarul de submissions.
|
|
|
|
PLANS['pro']['monthly_limit'] is None -> nelimitat; la fel trial Pro activ.
|
|
"""
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
# Cont pro cu 70 submissions (peste limita free de 60)
|
|
pro_id, pro_key = _create_account_with_key("ProVolumTest")
|
|
_set_tier_acct(pro_id, "pro", trial_until=None)
|
|
_insert_n_submissions(pro_id, 70)
|
|
|
|
# Cont trial Pro cu 70 submissions
|
|
trial_id, trial_key = _create_account_with_key("TrialVolumTest")
|
|
trial_until = (datetime.now(timezone.utc) + timedelta(days=10)).strftime("%Y-%m-%d %H:%M:%S")
|
|
_set_tier_acct(trial_id, "free", trial_until=trial_until)
|
|
_insert_n_submissions(trial_id, 70)
|
|
|
|
# Pro: trebuie 200 (nu 422 de volum)
|
|
r_pro = client.post(
|
|
"/v1/prezentari",
|
|
json={"prezentari": [_PREZ_PLAN]},
|
|
headers={"X-API-Key": pro_key},
|
|
)
|
|
assert r_pro.status_code == 200, f"Pro trebuie sa fie nelimitat, primit: {r_pro.text}"
|
|
|
|
# Trial Pro: trebuie 200 (nu 422 de volum)
|
|
prez2 = dict(_PREZ_PLAN, vin="WVWZZZ1KZAW900002", nr_inmatriculare="B900TS2")
|
|
r_trial = client.post(
|
|
"/v1/prezentari",
|
|
json={"prezentari": [prez2]},
|
|
headers={"X-API-Key": trial_key},
|
|
)
|
|
assert r_trial.status_code == 200, f"Trial Pro trebuie sa fie nelimitat, primit: {r_trial.text}"
|
|
|
|
|
|
def test_retry_idempotent_nu_consuma_cota(client):
|
|
"""Un retry idempotent al aceleiasi prestatii nu consuma cota de doua ori.
|
|
|
|
Invariant idempotenta (arhitectura): monthly_usage creste O SINGURA DATA
|
|
per submission unic. Retryurile (acelasi idempotency_key) sunt dedup-ate,
|
|
deci usage ramine la 1 dupa doua trimiteri identice.
|
|
|
|
Folsim cod_prestatie "OE-1" (in nomenclatorul seed) ca sa obtinem status
|
|
'queued' (statusuri "needs_mapping" nu se numara in monthly_usage).
|
|
"""
|
|
from datetime import datetime, timezone
|
|
from app.plans import monthly_usage
|
|
from app.db import get_connection
|
|
|
|
_set_tier_acct(1, "free", trial_until=None)
|
|
|
|
# cod_prestatie "OE-1" e in nomenclatorul seed -> submission va fi 'queued' (contat in usage)
|
|
prez_unic = {
|
|
"vin": "WVWZZZ1KZAW901001",
|
|
"nr_inmatriculare": "B901TST",
|
|
"data_prestatie": "2026-06-15",
|
|
"odometru_final": "50000",
|
|
"prestatii": [{"cod_prestatie": "OE-1", "denumire": "Revizie"}],
|
|
}
|
|
|
|
# Prima trimitere — submission noua
|
|
r1 = client.post("/v1/prezentari", json={"prezentari": [prez_unic]})
|
|
assert r1.status_code == 200, f"Prima trimitere trebuie sa treaca: {r1.text}"
|
|
assert not r1.json()["results"][0].get("deduped"), "Prima trimitere nu trebuia sa fie deduped"
|
|
|
|
conn = get_connection()
|
|
try:
|
|
usage_1 = monthly_usage(conn, 1, datetime.now(timezone.utc))
|
|
finally:
|
|
conn.close()
|
|
assert usage_1 == 1, f"Dupa prima trimitere: asteptat usage=1, primit {usage_1}"
|
|
|
|
# Retry (acelasi payload -> acelasi idempotency_key -> dedup, fara INSERT nou)
|
|
r2 = client.post("/v1/prezentari", json={"prezentari": [prez_unic]})
|
|
assert r2.status_code == 200, f"Retry trebuie sa treaca (dedup): {r2.text}"
|
|
assert r2.json()["results"][0].get("deduped") is True, "Retry trebuia marcat deduped"
|
|
|
|
conn = get_connection()
|
|
try:
|
|
usage_2 = monthly_usage(conn, 1, datetime.now(timezone.utc))
|
|
finally:
|
|
conn.close()
|
|
assert usage_2 == 1, (
|
|
f"Retry nu trebuia sa creasca usage: inainte={usage_1}, dupa retry={usage_2}"
|
|
)
|
|
|
|
|
|
def test_dev_id1_neblocat(client):
|
|
"""Dev account (id=1) in dev mode (require_api_key=False) nu e blocat de gate-ul API.
|
|
|
|
Bypass explicit in require_api_access: require_api_key=False + account_id==DEFAULT_ACCOUNT_ID
|
|
-> skip gate, indiferent de tier. DB proaspata (0 submissions -> fara blocare volum).
|
|
"""
|
|
_set_tier_acct(1, "free", trial_until=None)
|
|
|
|
resp = client.post("/v1/prezentari", json={"prezentari": [_PREZ_PLAN]})
|
|
assert resp.status_code == 200, (
|
|
f"Dev id=1 nu trebuie blocat de gate-ul API in dev mode, "
|
|
f"primit {resp.status_code}: {resp.text}"
|
|
)
|
|
|
|
|
|
def test_flag_enforce_plans_false_sare_enforcement(client, monkeypatch):
|
|
"""Kill-switch AUTOPASS_ENFORCE_PLANS=false sare toate gate-urile de plan.
|
|
|
|
T5 PRD 5.17: flag de operare pentru debugging sau rollback rapid fara revert de cod.
|
|
Chiar si cu free la 60/60, nu trebuie 422 cand flag-ul e oprit.
|
|
"""
|
|
from app.config import get_settings
|
|
|
|
monkeypatch.setenv("AUTOPASS_ENFORCE_PLANS", "false")
|
|
get_settings.cache_clear()
|
|
|
|
# Cont dev (id=1) pe free la 60/60 (normalmente respins)
|
|
_set_tier_acct(1, "free", trial_until=None)
|
|
_insert_n_submissions(1, 60)
|
|
|
|
resp = client.post("/v1/prezentari", json={"prezentari": [_PREZ_PLAN]})
|
|
assert resp.status_code == 200, (
|
|
f"Cu enforce_plans=False, enforcement trebuia sarat. Primit: {resp.status_code}"
|
|
)
|
|
|
|
get_settings.cache_clear() # curata cache-ul dupa test
|