"""Teste US-011 (PRD 3.3b): panou admin web /admin — conturi in asteptare + activare. TDD strict: testele se scriu INAINTE de implementare; la inceput pica (RED), dupa implementare trec (GREEN). Fisiere testate: app/web/admin_routes.py, app/web/templates/admin.html. """ from __future__ import annotations import os import re import tempfile import pytest from starlette.testclient import TestClient # --------------------------------------------------------------------------- # Fixture client (web_auth_required=true -> CSRF enforce) # --------------------------------------------------------------------------- @pytest.fixture() def client(monkeypatch): """TestClient pe aplicatia completa, cu DB izolata si web_auth_required=true.""" tmp = tempfile.mkdtemp() monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "test_admin_panel.db")) monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "true") # Ridica limita rate-limit pentru signup ca testele nu se blocheze intre ele monkeypatch.setenv("AUTOPASS_SIGNUP_RATE_MAX", "100") from app.config import get_settings get_settings.cache_clear() # Curata hits-urile rate-limit intre teste from app.web import ratelimit ratelimit._hits.clear() from app.main import app with TestClient(app, follow_redirects=False) as c: yield c get_settings.cache_clear() # --------------------------------------------------------------------------- # Helper-e # --------------------------------------------------------------------------- def _get_csrf(client: TestClient, url: str) -> str: """Extrage csrf_token din pagina HTML.""" resp = client.get(url, follow_redirects=True) m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text) if not m: m = re.search(r'value="([^"]+)"\s+name="csrf_token"', resp.text) assert m, f"csrf_token negasit in {url}: {resp.text[:500]}" return m.group(1) def _signup(client: TestClient, name: str, email: str, password: str = "parola_test_001") -> int: """Creeaza cont via POST /signup si intoarce account_id.""" from tests.conftest import make_test_cui token = _get_csrf(client, "/signup") resp = client.post("/signup", data={ "name": name, "cui": make_test_cui(email), "email": email, "parola": password, "consent": "1", "csrf_token": token, }, follow_redirects=True) assert resp.status_code == 200, f"signup esuat: {resp.text[:300]}" # Extrage account_id din raspuns (pagina afiseaza cheia rfak_ + account_id) m = re.search(r"cont=(\d+)", resp.text) if not m: # fallback: citeste din DB from app.db import get_connection conn = get_connection() row = conn.execute( "SELECT account_id FROM users WHERE email=? COLLATE NOCASE", (email,) ).fetchone() conn.close() assert row, f"userul {email} nu a fost creat" return int(row["account_id"]) return int(m.group(1)) def _login(client: TestClient, email: str, password: str = "parola_test_001") -> None: """Autentifica userul (seteaza sesiunea cookie).""" token = _get_csrf(client, "/login") resp = client.post("/login", data={ "email": email, "parola": password, "csrf_token": token, }, follow_redirects=False) assert resp.status_code == 303, f"login esuat cu {email}: {resp.status_code} {resp.text[:200]}" def _make_admin(account_id: int) -> None: """Marcheaza contul ca admin direct in DB.""" from app.db import get_connection from app.users import set_admin conn = get_connection() try: set_admin(conn, account_id, is_admin=True) conn.commit() finally: conn.close() def _get_account_active(account_id: int) -> bool: """Citeste accounts.active din DB.""" from app.db import get_connection conn = get_connection() try: row = conn.execute("SELECT active FROM accounts WHERE id=?", (account_id,)).fetchone() return bool(row["active"]) if row else False finally: conn.close() # --------------------------------------------------------------------------- # Cazuri de test # --------------------------------------------------------------------------- def test_admin_vede_conturi_pending(client): """Admin logat -> GET /admin contine numele contului pending (active=0).""" # Creeaza un cont pending (active=0 implicit la signup) _signup(client, "Service Pending SRL", "pending@test.ro") # Creeaza contul admin admin_id = _signup(client, "Admin Corp SA", "admin@test.ro") _make_admin(admin_id) # Login ca admin _login(client, "admin@test.ro") resp = client.get("/admin") assert resp.status_code == 200, f"GET /admin a returnat {resp.status_code}" assert "Service Pending SRL" in resp.text, ( f"Contul pending nu apare in /admin. Raspuns: {resp.text[:600]}" ) def test_activare_din_admin(client): """POST /admin/activate cu CSRF -> accounts.active=1; redirect 303.""" # Cont pending pending_id = _signup(client, "Firma De Activat SRL", "firma@test.ro") assert not _get_account_active(pending_id), "contul trebuie sa fie inactiv initial" # Cont admin admin_id = _signup(client, "Admin Activator SA", "activator@test.ro") _make_admin(admin_id) _login(client, "activator@test.ro") # Obtine CSRF din pagina /admin resp = client.get("/admin") assert resp.status_code == 200 m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text) if not m: m = re.search(r'value="([^"]+)"\s+name="csrf_token"', resp.text) assert m, "csrf_token negasit in /admin" csrf = m.group(1) resp2 = client.post("/admin/activate", data={ "account_id": str(pending_id), "csrf_token": csrf, }) assert resp2.status_code == 303, ( f"POST /admin/activate trebuia redirect 303, got {resp2.status_code}: {resp2.text[:300]}" ) assert _get_account_active(pending_id), "contul trebuia sa fie activat dupa POST /admin/activate" def test_non_admin_403(client): """User logat NON-admin -> GET /admin -> 403. Creeaza intai un cont admin (bootstrap: primul user devine admin), apoi un al doilea cont (non-admin) si verifica ca al doilea primeste 403. """ # Primul signup devine automat admin (bootstrap US-010) _signup(client, "Admin Bootstrap SA", "bootstrap@test.ro") # Al doilea user NU e admin _signup(client, "User Simplu SRL", "user@test.ro") _login(client, "user@test.ro") resp = client.get("/admin") assert resp.status_code == 403, ( f"User non-admin trebuia 403 pe /admin, got {resp.status_code}" ) def test_admin_nelogat_redirect(client): """Fara sesiune -> GET /admin -> 303 redirect la /login.""" resp = client.get("/admin") assert resp.status_code == 303, ( f"Nelogat pe /admin trebuia 303, got {resp.status_code}" ) loc = resp.headers.get("location", "") assert "/login" in loc, f"Redirect gresit: {loc}" def test_activate_fara_csrf_403(client): """Admin logat, POST /admin/activate fara token CSRF -> 403.""" pending_id = _signup(client, "Firma Fara CSRF SRL", "nocsrf@test.ro") admin_id = _signup(client, "Admin CSRF Test SA", "csrfadmin@test.ro") _make_admin(admin_id) _login(client, "csrfadmin@test.ro") # POST fara token (sau token gol) resp = client.post("/admin/activate", data={ "account_id": str(pending_id), "csrf_token": "", }) assert resp.status_code == 403, ( f"POST fara CSRF trebuia 403, got {resp.status_code}" ) def test_activare_cont_incomplet_refuzata(client): """Admin nu poate activa un cont incomplet (fara email/CUI) — contul ramane pending. Gate pe account_is_complete: un cont fara companie+email+CUI nu poate fi activat de admin (buton dezactivat in UI + server refuza activarea). """ # Cream cont pending INCOMPLET direct prin create_account (fara email/CUI) from app.accounts import create_account from app.users import create_user from app.db import get_connection conn = get_connection() try: incomplete_id = create_account(conn, "Firma Incompleta SRL", active=False) create_user(conn, incomplete_id, "incompleta@test.ro", "parola_test_001") finally: conn.close() # Admin admin_id = _signup(client, "Admin Gate SA", "admin_gate@test.ro") _make_admin(admin_id) _login(client, "admin_gate@test.ro") # Obtine CSRF din /admin resp = client.get("/admin") assert resp.status_code == 200 m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text) if not m: m = re.search(r'value="([^"]+)"\s+name="csrf_token"', resp.text) assert m, "csrf_token negasit in /admin" csrf = m.group(1) # Incearca sa activeze contul incomplet resp2 = client.post("/admin/activate", data={ "account_id": str(incomplete_id), "csrf_token": csrf, }) # Fie 303 redirect, fie pagina cu eroare — important: contul NU e activat assert resp2.status_code in (200, 303, 422), ( f"Raspuns neasteptat: {resp2.status_code}" ) # Verifica in DB: contul ramane pending (neactivat) assert not _get_account_active(incomplete_id), ( "Contul incomplet (fara email/CUI) a fost activat — gate pe account_is_complete nu functioneaza" ) def _get_tier_trial(account_id: int) -> tuple[str, str | None]: """Citeste (tier, trial_until) din DB.""" from app.db import get_connection conn = get_connection() try: row = conn.execute( "SELECT tier, trial_until FROM accounts WHERE id=?", (account_id,) ).fetchone() return (row["tier"], row["trial_until"]) if row else ("", None) finally: conn.close() def _get_tier(account_id: int) -> str: """Citeste accounts.tier din DB.""" return _get_tier_trial(account_id)[0] def test_set_tier_din_admin_incheie_trial(client): """POST /admin/set-tier -> tier actualizat, trial_until=NULL (trial incheiat), 303. Contul nou are trial Pro 30z; alocarea manuala trebuie sa-l incheie ca alegerea sa aiba efect imediat (decizie user 2026-06-29).""" target_id = _signup(client, "Firma Upgrade SRL", "upgrade@test.ro") tier0, trial0 = _get_tier_trial(target_id) assert tier0 == "free", "cont nou trebuie sa porneasca pe free" assert trial0, "cont nou trebuie sa aiba trial_until setat (trial Pro 30z)" admin_id = _signup(client, "Admin Tier SA", "admintier@test.ro") _make_admin(admin_id) _login(client, "admintier@test.ro") csrf = _get_csrf(client, "/admin") resp = client.post("/admin/set-tier", data={ "account_id": str(target_id), "tier": "pro", "csrf_token": csrf, }) assert resp.status_code == 303, f"asteptat 303 PRG, primit {resp.status_code}" tier1, trial1 = _get_tier_trial(target_id) assert tier1 == "pro", "tier-ul nu a fost mutat pe pro" assert trial1 is None, "trial_until trebuie sters la alocarea manuala (efect imediat)" def test_set_tier_free_opreste_pro_imediat(client): """Setarea pe 'free' pe un cont in trial -> efectiv 'free' acum (trial incheiat). Fara stergerea trial-ului, effective_tier ar fi ramas 'pro' inca ~30 zile.""" from datetime import datetime, timezone from app.plans import effective_tier target_id = _signup(client, "Firma Abuz Trial SRL", "abuztrial@test.ro") admin_id = _signup(client, "Admin Stop SA", "adminstop@test.ro") _make_admin(admin_id) _login(client, "adminstop@test.ro") csrf = _get_csrf(client, "/admin") resp = client.post("/admin/set-tier", data={ "account_id": str(target_id), "tier": "free", "csrf_token": csrf, }) assert resp.status_code == 303 tier1, trial1 = _get_tier_trial(target_id) assert tier1 == "free" and trial1 is None eff = effective_tier({"tier": tier1, "trial_until": trial1}, datetime.now(timezone.utc)) assert eff == "free", "dupa setarea pe free, planul efectiv trebuie sa fie free imediat" def test_set_tier_invalid_respins(client): """Tier invalid -> nu schimba nimic (re-randare cu eroare sau ignorat).""" target_id = _signup(client, "Firma Tier Invalid SRL", "tierinvalid@test.ro") admin_id = _signup(client, "Admin TI SA", "adminti@test.ro") _make_admin(admin_id) _login(client, "adminti@test.ro") csrf = _get_csrf(client, "/admin") resp = client.post("/admin/set-tier", data={ "account_id": str(target_id), "tier": "platinum", # invalid "csrf_token": csrf, }) assert resp.status_code in (200, 422), f"tier invalid ar trebui respins, primit {resp.status_code}" assert _get_tier(target_id) == "free", "tier invalid nu trebuie aplicat" def test_set_tier_fara_csrf_respins(client): """POST /admin/set-tier fara token CSRF valid -> respins, tier neschimbat.""" target_id = _signup(client, "Firma CSRF Tier SRL", "csrftier@test.ro") admin_id = _signup(client, "Admin CSRF SA", "admincsrf@test.ro") _make_admin(admin_id) _login(client, "admincsrf@test.ro") resp = client.post("/admin/set-tier", data={ "account_id": str(target_id), "tier": "pro", "csrf_token": "token-fals", }) assert resp.status_code in (400, 403), f"CSRF invalid trebuie respins, primit {resp.status_code}" assert _get_tier(target_id) == "free", "tier schimbat desi CSRF era invalid" def test_set_trial_din_admin(client): """POST /admin/set-trial -> trial_until setat, tier de baza neschimbat, efectiv pro, 303.""" from datetime import datetime, timezone from app.plans import effective_tier target_id = _signup(client, "Firma Trial SRL", "trialnou@test.ro") # incheie intai orice trial (set-tier free) ca sa pornim de la baza curata admin_id = _signup(client, "Admin Trial SA", "admintrialacord@test.ro") _make_admin(admin_id) _login(client, "admintrialacord@test.ro") csrf = _get_csrf(client, "/admin") client.post("/admin/set-tier", data={ "account_id": str(target_id), "tier": "free", "csrf_token": csrf, }) assert _get_tier_trial(target_id) == ("free", None) # acorda trial Pro 15 zile csrf = _get_csrf(client, "/admin") resp = client.post("/admin/set-trial", data={ "account_id": str(target_id), "trial_days": "15", "csrf_token": csrf, }) assert resp.status_code == 303, f"asteptat 303 PRG, primit {resp.status_code}" tier1, trial1 = _get_tier_trial(target_id) assert tier1 == "free", "tier-ul de baza NU trebuie schimbat de acordarea de trial" assert trial1, "trial_until trebuie setat" eff = effective_tier({"tier": tier1, "trial_until": trial1}, datetime.now(timezone.utc)) assert eff == "pro", "trial activ trebuie sa ridice planul efectiv la pro" def test_set_trial_zile_invalide_respins(client): """trial_days <= 0 -> 422, trial neschimbat.""" target_id = _signup(client, "Firma Trial Invalid SRL", "trialinvalid@test.ro") admin_id = _signup(client, "Admin TInv SA", "admintinv@test.ro") _make_admin(admin_id) _login(client, "admintinv@test.ro") # porneste de la trial sters csrf = _get_csrf(client, "/admin") client.post("/admin/set-tier", data={ "account_id": str(target_id), "tier": "free", "csrf_token": csrf, }) csrf = _get_csrf(client, "/admin") resp = client.post("/admin/set-trial", data={ "account_id": str(target_id), "trial_days": "0", "csrf_token": csrf, }) assert resp.status_code == 422 assert _get_tier_trial(target_id) == ("free", None), "trial nu trebuie setat la zile invalide"