Files
rar-autopass/tests/test_admin_panel.py
Claude Agent b92055eb01 feat(web): self-service cheie/creds + admin web + email signup (PRD 3.3b)
US-007: rute web proprii /cont/roteste-cheie + /cont/rar-creds scoped pe
sesiune (C13), sectiune "Contul meu" cu cheie afisata o data.
US-010: rol admin (users.is_admin) + require_admin->403 + CLI set-admin +
bootstrap primul cont=admin (count_admins in BEGIN IMMEDIATE, anti-race).
US-011: panou /admin (activare/dezactivare conturi, CSRF + PRG), link admin
+ logout pe dashboard.
US-012: app/email.py notify_signup best-effort degradat fara SMTP + config smtp_*.
Fix: migrare defensiva users.is_admin/email_verified in _migrate.

VERIFY x2 context curat (PASS) + /code-review high. 393 teste pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:19:06 +00:00

214 lines
7.4 KiB
Python

"""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."""
token = _get_csrf(client, "/signup")
resp = client.post("/signup", data={
"name": name,
"email": email,
"parola": password,
"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}"
)