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>
196 lines
6.7 KiB
Python
196 lines
6.7 KiB
Python
"""Teste E2E enforcement plan pe canalul web de import (PRD 5.17 T3).
|
|
|
|
Verifica ca limita de volum (60/luna free) e respectata si pe canalul web
|
|
(web_confirma_import in routes.py), nu doar pe canalul API.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
import io
|
|
import os
|
|
import re
|
|
import tempfile
|
|
from datetime import datetime, timezone
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixture client web izolat (WEB_AUTH_REQUIRED=false -> fara login, cont 1)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.fixture()
|
|
def client(monkeypatch):
|
|
"""Client cu DB izolata, WEB_AUTH_REQUIRED=false (dev — fara login necesar)."""
|
|
tmp = tempfile.mkdtemp()
|
|
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "web-e2e-plan.db"))
|
|
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
|
|
from app.config import get_settings
|
|
get_settings.cache_clear()
|
|
from app.crypto import reset_cache
|
|
reset_cache()
|
|
from app.main import app
|
|
with TestClient(app) as c:
|
|
yield c
|
|
get_settings.cache_clear()
|
|
reset_cache()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Utilitare
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _csv_bytes(rows: list[dict], sep: str = ";") -> bytes:
|
|
buf = io.StringIO()
|
|
writer = csv.DictWriter(buf, fieldnames=list(rows[0].keys()), delimiter=sep)
|
|
writer.writeheader()
|
|
writer.writerows(rows)
|
|
return buf.getvalue().encode("utf-8")
|
|
|
|
|
|
def _seed_nomenclator_si_mapare(cod_prestatie: str = "OE-1", cod_op: str = "OP-WEB-PLAN") -> None:
|
|
"""Semeaza nomenclatorul RAR si o mapare operatie->cod (necesare pentru randuri ok)."""
|
|
from app.db import get_connection
|
|
conn = get_connection()
|
|
try:
|
|
conn.execute(
|
|
"INSERT OR REPLACE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?,?)",
|
|
(cod_prestatie, "Operatie test plan"),
|
|
)
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO operations_mapping "
|
|
"(account_id, cod_op_service, cod_prestatie, auto_send) VALUES (1,?,?,1)",
|
|
(cod_op, cod_prestatie),
|
|
)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _insert_60_submissions_luna() -> None:
|
|
"""Insereaza 60 submissions queued in luna curenta pentru contul 1 (la limita free)."""
|
|
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(60):
|
|
conn.execute(
|
|
"INSERT INTO submissions "
|
|
"(idempotency_key, account_id, status, payload_json, created_at) "
|
|
"VALUES (?, 1, 'queued', '{}', ?)",
|
|
(f"web-vol60-{i}-{os.urandom(4).hex()}", now_str),
|
|
)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _upload_preview_si_commit(client: TestClient, rows: list[dict]): # type: ignore[return]
|
|
"""Parcurge fluxul web: upload -> mapare coloane (daca e necesar) -> confirma.
|
|
|
|
Intoarce (import_id, raspuns_confirma). Presupune nomenclatorul si maparea semanate.
|
|
"""
|
|
data = _csv_bytes(rows)
|
|
r = client.post(
|
|
"/_import/upload",
|
|
files={"file": ("plan_test.csv", io.BytesIO(data), "text/csv")},
|
|
)
|
|
assert r.status_code == 200, f"Upload esuat: {r.text[:300]}"
|
|
|
|
m = re.search(r"/_import/(\d+)/", r.text)
|
|
assert m, f"import_id negasit in raspunsul de upload: {r.text[:400]}"
|
|
iid = int(m.group(1))
|
|
|
|
if f"/_import/{iid}/mapare-coloane" in r.text:
|
|
r2 = client.post(
|
|
f"/_import/{iid}/mapare-coloane",
|
|
data={
|
|
"colname": ["VIN", "Nr", "Data", "KM", "Operatie"],
|
|
"canon": ["vin", "nr_inmatriculare", "data_prestatie", "odometru_final", "operatie"],
|
|
"format_data": "YYYY-MM-DD",
|
|
},
|
|
)
|
|
assert r2.status_code == 200, f"Mapare coloane esuata: {r2.text[:300]}"
|
|
|
|
# GET preview pentru n_ok
|
|
rp = client.get(f"/_import/{iid}/preview")
|
|
assert rp.status_code == 200, f"Preview esuat: {rp.text[:300]}"
|
|
m_ok = re.search(r'id="n-confirmat"[^>]*?value="(\d+)"', rp.text)
|
|
n_ok = int(m_ok.group(1)) if m_ok else len(rows)
|
|
|
|
r_conf = client.post(
|
|
f"/_import/{iid}/confirma",
|
|
data={
|
|
"csrf_token": "",
|
|
"n_confirmat": str(n_ok),
|
|
"confirmed_by": "test-plan@autopass.ro",
|
|
},
|
|
)
|
|
return iid, r_conf
|
|
|
|
|
|
# Date CSV: un singur rand ok
|
|
_ROWS_PLAN_WEB = [
|
|
{
|
|
"VIN": "WVWZZZ1KZAW700001",
|
|
"Nr": "B700TST",
|
|
"Data": "2026-06-15",
|
|
"KM": "70000",
|
|
"Operatie": "OP-WEB-PLAN",
|
|
},
|
|
]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test T3 — volum pe canalul web
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_free_peste_60_respins_import_web(client):
|
|
"""Canal WEB de import: free la 60/60 → commit respins cu mesaj de limita plan.
|
|
|
|
T3 PRD 5.17: enforcement volum pe canalul web (web_confirma_import in routes.py).
|
|
Contul 1 e pe tier=free, fara trial, la 60/60 prestatii in luna curenta.
|
|
Commit-ul unui lot nou trebuie respins (intregul lot, nu partial) cu mesaj clar.
|
|
"""
|
|
from app.db import get_connection
|
|
|
|
# Seteaza contul 1 (implicit web in dev mode) pe free fara trial
|
|
conn = get_connection()
|
|
try:
|
|
conn.execute("UPDATE accounts SET tier='free', trial_until=NULL WHERE id=1")
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
# Seed nomenclator si mapare operatie->cod
|
|
_seed_nomenclator_si_mapare()
|
|
|
|
# Insereaza 60 submissions (la limita)
|
|
_insert_60_submissions_luna()
|
|
|
|
# Parcurge fluxul web pana la commit
|
|
_iid, r_conf = _upload_preview_si_commit(client, _ROWS_PLAN_WEB)
|
|
|
|
assert r_conf.status_code == 200, ( # type: ignore[union-attr]
|
|
f"Commit trebuia sa intoarca 200 HTML (nu 5xx): {r_conf.status_code}" # type: ignore[union-attr]
|
|
)
|
|
|
|
# Raspunsul HTML trebuie sa contina mesajul de limita de plan
|
|
html = r_conf.text.lower() # type: ignore[union-attr]
|
|
assert ("limita" in html or "gratuit" in html or "60" in html), (
|
|
f"Mesajul de limita plan lipseste din raspunsul HTML al commit-ului:\n{r_conf.text[:600]}" # type: ignore[union-attr]
|
|
)
|
|
|
|
# Verifica ca nu s-au creat submissions noi (lotul a fost respins total)
|
|
from app.plans import monthly_usage
|
|
conn2 = get_connection()
|
|
try:
|
|
usage = monthly_usage(conn2, 1, datetime.now(timezone.utc))
|
|
finally:
|
|
conn2.close()
|
|
assert usage == 60, (
|
|
f"Lotul respins nu trebuia sa adauge submissions: asteptat usage=60, primit {usage}"
|
|
)
|