Files
rar-autopass/tests/test_signup.py
Claude Agent 851f76ca16 feat(signup+admin): aliniere formular signup la landing + plan cerut, GDPR, control tier/trial in panou
Signup:
- /signup aliniat ca format la formularul din landing (campuri, etichete,
  placeholder-uri, select plan, checkbox GDPR, buton). Eticheta `name` = "Companie"
  (corecta: backendul salveaza nume de firma), uniform si in landing.
- Consimtamant GDPR validat server-side (functional, nu doar client-side) + salvat
  cu marca temporala (accounts.consent_at).
- Plan ales la signup salvat in accounts.requested_plan (intentie, NU drept): tier
  ramane sursa de adevar pentru gate-ul API; coloana pregateste integrarea platilor.
- landing: valorile `plan` = coduri tier (free/standard/pro/premium), data-plan
  sincronizat pe butoanele de pret; checkbox consimtamant primeste name.

Schema/DB:
- accounts: coloane noi requested_plan + consent_at (cu migrare aditiva in db.py).

Panou admin:
- Coloane noi: Plan curent (plan EFECTIV acum + zile trial ramase) si Plan cerut.
- Buton "Aplica" (POST /admin/set-tier): aloca plan real si INCHEIE trial-ul
  (efect imediat; altfel trial-ul Pro universal de 30z masca alegerea).
- Control "Trial Pro N zile" (POST /admin/set-trial via accounts.set_trial):
  acorda/prelungeste trial fara a schimba tier-ul de baza.

Teste: signup (consent obligatoriu, requested_plan persistat, tier ramane free),
panou admin (set-tier incheie trial, free opreste Pro imediat, set-trial, validari
+ CSRF). Call-site-urile existente POST /signup actualizate cu consent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 16:02:37 +00:00

210 lines
7.3 KiB
Python

"""Teste US-001 (PRD 5.12): companie/email/CUI obligatorii la signup.
TDD strict: testele se scriu INAINTE de implementare (RED), dupa implementare trec (GREEN).
"""
from __future__ import annotations
import os
import re
import tempfile
import pytest
from starlette.testclient import TestClient
@pytest.fixture()
def client(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "test_signup_us001.db"))
monkeypatch.setenv("AUTOPASS_SIGNUP_RATE_MAX", "100")
from app.config import get_settings
get_settings.cache_clear()
from app.web import ratelimit
ratelimit._hits.clear()
from app.main import app
with TestClient(app) as c:
yield c
get_settings.cache_clear()
def _csrf(html: str) -> str:
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', html)
if not m:
m = re.search(r'value="([^"]+)"\s+name="csrf_token"', html)
assert m, "csrf_token negasit in HTML"
return m.group(1)
def test_signup_html_cui_obligatoriu_ui(client):
"""GET /signup: campul CUI NU contine '(optional)' si are atribut required (US-001 UI)."""
resp = client.get("/signup")
assert resp.status_code == 200
# (a) NU trebuie sa apara textul "(optional)" langa CUI
assert "(optional)" not in resp.text, "Campul CUI afiseaza '(optional)' — trebuie sa fie obligatoriu"
# (b) input[name=cui] trebuie sa aiba atribut required
assert 'name="cui"' in resp.text
# cautam required pe aceeasi linie cu name="cui" sau intr-un bloc care contine name="cui" required
import re
# fie pe aceeasi linie: <input ... name="cui" ... required ...>
# fie in orice forma cu required si name="cui" in acelasi tag
cui_input_match = re.search(r'<input[^>]*name="cui"[^>]*>', resp.text)
assert cui_input_match, "input name='cui' negasit in HTML"
assert "required" in cui_input_match.group(0), (
f"input[name='cui'] NU are atribut required: {cui_input_match.group(0)}"
)
def test_signup_fara_cui_422(client):
"""POST /signup fara CUI -> 422, formular re-randat cu eroare, fara cont creat."""
resp = client.get("/signup")
token = _csrf(resp.text)
resp = client.post("/signup", data={
"name": "Service Fara CUI",
"cui": "",
"email": "fara_cui@test.com",
"parola": "parolasecreta123",
"consent": "1",
"csrf_token": token,
})
# trebuie sa returneze 422 (sau sa randeze formularul cu eroare)
assert resp.status_code == 422
# cheia API nu trebuie sa apara
assert "rfak_" not in resp.text
# campul name trebuie sa fie pastrat (form re-render cu valorile existente)
assert "Service Fara CUI" in resp.text
# verifica ca nu s-a creat niciun cont
from app.db import get_connection
conn = get_connection()
try:
n = conn.execute(
"SELECT COUNT(*) AS n FROM accounts WHERE name='Service Fara CUI'"
).fetchone()["n"]
assert n == 0, "Cont creat desi CUI lipsea"
finally:
conn.close()
def test_signup_scrie_email_pe_account(client):
"""POST /signup valid -> accounts.email = emailul utilizatorului."""
resp = client.get("/signup")
token = _csrf(resp.text)
resp = client.post("/signup", data={
"name": "Service Cu Email",
"cui": "RO9999001",
"email": "cu_email@test.com",
"parola": "parolasecreta123",
"consent": "1",
"csrf_token": token,
})
assert resp.status_code == 200
assert "rfak_" in resp.text
from app.db import get_connection
conn = get_connection()
try:
acct = conn.execute(
"SELECT * FROM accounts WHERE name='Service Cu Email'"
).fetchone()
assert acct is not None
# emailul trebuie scris pe cont (normalizat: lower + trim)
assert acct["email"] == "cu_email@test.com"
finally:
conn.close()
def test_signup_email_duplicat_mesaj_email(client):
"""POST /signup cu email existent dar CUI nou -> mesaj despre EMAIL, NU despre CUI/firma.
Bug: 'email deja folosit' contine 'deja folosit' -> era prins de conditia CUI duplicat
si afisa gresit 'Aceasta firma (CUI X) e deja inregistrata' (CUI nou, NU cauza reala).
Fix: verifica intai email-ul, apoi CUI-ul.
"""
from tests.conftest import make_test_cui
# primul signup cu email E + CUI C1
resp = client.get("/signup")
token = _csrf(resp.text)
resp1 = client.post("/signup", data={
"name": "Firma Prima SRL",
"cui": make_test_cui("email-dup-c1"),
"email": "emaildup@test.com",
"parola": "parolasecreta123",
"consent": "1",
"csrf_token": token,
})
assert resp1.status_code == 200
assert "rfak_" in resp1.text, "Primul signup trebuia sa reuseasca"
# al doilea signup cu ACELASI email dar CUI NOU
resp = client.get("/signup")
token2 = _csrf(resp.text)
cui_nou = make_test_cui("email-dup-c2")
resp2 = client.post("/signup", data={
"name": "Firma A Doua SRL",
"cui": cui_nou,
"email": "emaildup@test.com",
"parola": "parolasecreta456",
"consent": "1",
"csrf_token": token2,
})
assert resp2.status_code in (200, 422)
assert "rfak_" not in resp2.text, "Nu trebuia creata cheie API la email duplicat"
body_lower = resp2.text.lower()
# mesajul trebuie sa se refere la EMAIL
assert "email" in body_lower, (
f"Mesajul de eroare nu mentioneaza 'email': {resp2.text[:500]}"
)
# mesajul NU trebuie sa afiseze pattern-ul gresit cu firma si CUI-ul nou
# (CUI-ul apare legitim si in campul pre-completat al formularului, dar nu in mesajul de eroare)
wrong_pattern = f"(cui {cui_nou.lower()}) e deja inregistrata"
assert wrong_pattern not in body_lower, (
f"Mesajul arata gresit pattern-ul CUI-duplicat desi problema e emailul: {resp2.text[:500]}"
)
# nu trebuie sa apara mesajul specific CUI-duplicat
assert "e deja inregistrata" not in body_lower, (
f"Mesajul arata 'e deja inregistrata' (mesaj CUI) la eroare de email: {resp2.text[:500]}"
)
def test_signup_cui_existent_mesaj_prietenos(client):
"""POST /signup cu CUI existent -> mesaj prietenos, NU mesaj tehnic cu 'activate --account'."""
resp = client.get("/signup")
token = _csrf(resp.text)
# primul signup
client.post("/signup", data={
"name": "Firma Existenta SRL",
"cui": "RO8888001",
"email": "firma1@test.com",
"parola": "parolasecreta123",
"consent": "1",
"csrf_token": token,
})
# al doilea signup cu acelasi CUI
resp = client.get("/signup")
token2 = _csrf(resp.text)
resp2 = client.post("/signup", data={
"name": "Alt Utilizator SRL",
"cui": "RO8888001",
"email": "firma2@test.com",
"parola": "parolasecreta456",
"consent": "1",
"csrf_token": token2,
})
assert resp2.status_code in (200, 422)
# NU trebuie sa apara mesajul tehnic cu referinta la CLI
assert "activate --account" not in resp2.text
# trebuie sa apara un mesaj prietenos cu CUI-ul
assert "RO8888001" in resp2.text
# trebuie sa contina cuvant cheie de tip "firma" sau "inregistrata"
body_lower = resp2.text.lower()
assert any(kw in body_lower for kw in ["firma", "inregistrat", "cont", "acces"])