Files
rar-autopass/tests/test_web_import_e2e.py
Claude Agent c9f9a1ca0e feat(5.16+5.17): tipografie/antet branded + tipuri cont, planuri si enforcement
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>
2026-06-29 06:02:40 +00:00

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}"
)