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>
This commit is contained in:
277
tests/test_admin_role.py
Normal file
277
tests/test_admin_role.py
Normal file
@@ -0,0 +1,277 @@
|
||||
"""Teste US-010 (PRD 3.3b): rol admin + bootstrap + guard require_admin.
|
||||
|
||||
Fisiere testate: app/users.py (helper-e admin), app/web/session.py (require_admin),
|
||||
tools/account.py (subcomanda set-admin).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixture DB
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture()
|
||||
def conn(monkeypatch):
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "test_admin_role.db"))
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
from app.db import get_connection, init_db
|
||||
init_db()
|
||||
c = get_connection()
|
||||
yield c
|
||||
c.close()
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def account_id(conn):
|
||||
"""Cont de test (nu default id=1)."""
|
||||
from app.accounts import create_account
|
||||
return create_account(conn, "Service Test Admin")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def user_id(conn, account_id):
|
||||
"""User de test pe contul de test."""
|
||||
from app.users import create_user
|
||||
return create_user(conn, account_id, "admin_test@exemplu.ro", "parola_sigura_123")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def env_cli(monkeypatch):
|
||||
"""Fixture pentru CLI: DB separata + clear settings cache."""
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "test_admin_cli.db"))
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
from app.db import init_db
|
||||
init_db()
|
||||
yield
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper app minimal pentru require_admin
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _make_app_admin() -> FastAPI:
|
||||
"""App FastAPI minimal cu ruta protejata de require_admin."""
|
||||
mini = FastAPI()
|
||||
mini.add_middleware(
|
||||
SessionMiddleware,
|
||||
secret_key="test-secret-admin",
|
||||
session_cookie="autopass_session",
|
||||
https_only=False,
|
||||
same_site="strict",
|
||||
)
|
||||
|
||||
from app.web.session import LoginRequired, AdminRequired
|
||||
|
||||
@mini.exception_handler(LoginRequired)
|
||||
async def login_required_handler(request: Request, exc: LoginRequired):
|
||||
return JSONResponse(status_code=401, content={"detail": "neautentificat"})
|
||||
|
||||
@mini.exception_handler(AdminRequired)
|
||||
async def admin_required_handler(request: Request, exc: AdminRequired):
|
||||
return JSONResponse(status_code=403, content={"detail": "acces interzis (necesita admin)"})
|
||||
|
||||
@mini.get("/set-session")
|
||||
def set_sess(request: Request, account_id: int = 1, user_id: int = 1):
|
||||
from app.web.session import set_session
|
||||
set_session(request, account_id, user_id)
|
||||
return {"ok": True}
|
||||
|
||||
@mini.get("/admin-only")
|
||||
def admin_only(request: Request):
|
||||
from app.web.session import require_admin
|
||||
aid = require_admin(request)
|
||||
return {"account_id": aid}
|
||||
|
||||
return mini
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Teste helper-e app/users.py
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_count_admins_initial_zero(conn):
|
||||
"""Fara niciun user cu is_admin=1, count_admins returneaza 0."""
|
||||
from app.users import count_admins
|
||||
assert count_admins(conn) == 0
|
||||
|
||||
|
||||
def test_set_admin_marcheaza_userii_contului(conn, account_id, user_id):
|
||||
"""set_admin(conn, account_id) seteaza is_admin=1 pe toti userii contului."""
|
||||
from app.users import set_admin, count_admins
|
||||
assert count_admins(conn) == 0
|
||||
set_admin(conn, account_id, is_admin=True)
|
||||
assert count_admins(conn) == 1
|
||||
|
||||
|
||||
def test_create_user_is_admin_flag(conn, account_id):
|
||||
"""create_user cu is_admin=True seteaza coloana la 1."""
|
||||
from app.users import create_user, count_admins
|
||||
create_user(conn, account_id, "newadmin@exemplu.ro", "parola_sigura_456", is_admin=True)
|
||||
assert count_admins(conn) == 1
|
||||
|
||||
|
||||
def test_is_account_admin(conn, account_id, user_id):
|
||||
"""is_account_admin returneaza False inainte si True dupa set_admin."""
|
||||
from app.users import is_account_admin, set_admin
|
||||
assert is_account_admin(conn, account_id) is False
|
||||
set_admin(conn, account_id, is_admin=True)
|
||||
assert is_account_admin(conn, account_id) is True
|
||||
|
||||
|
||||
def test_list_admin_emails(conn, account_id):
|
||||
"""list_admin_emails returneaza emailurile userilor cu is_admin=1."""
|
||||
from app.users import create_user, set_admin, list_admin_emails
|
||||
uid = create_user(conn, account_id, "admin1@exemplu.ro", "parola_sigura_789")
|
||||
# Inainte de set_admin -> lista goala
|
||||
assert list_admin_emails(conn) == []
|
||||
set_admin(conn, account_id, is_admin=True)
|
||||
emails = list_admin_emails(conn)
|
||||
assert "admin1@exemplu.ro" in emails
|
||||
|
||||
|
||||
def test_set_admin_cont_inexistent_valueerror(conn):
|
||||
"""set_admin pe cont care nu exista ridica ValueError."""
|
||||
from app.users import set_admin
|
||||
with pytest.raises(ValueError, match="cont inexistent"):
|
||||
set_admin(conn, 9999, is_admin=True)
|
||||
|
||||
|
||||
def test_set_admin_cont_fara_users_silentios(conn):
|
||||
"""set_admin pe cont existent fara useri e no-op silentios (nu ridica exceptie)."""
|
||||
from app.accounts import create_account
|
||||
from app.users import set_admin, count_admins
|
||||
acct_fara_user = create_account(conn, "Cont Fara User")
|
||||
# Nu trebuie sa ridice
|
||||
set_admin(conn, acct_fara_user, is_admin=True)
|
||||
assert count_admins(conn) == 0 # niciun user modificat
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Teste require_admin (app/web/session.py) prin TestClient
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture()
|
||||
def client_admin(monkeypatch):
|
||||
"""TestClient pe app cu require_admin; DB cu cont admin + cont non-admin."""
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "test_require_admin.db"))
|
||||
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "true")
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
from app.db import get_connection, init_db
|
||||
init_db()
|
||||
conn = get_connection()
|
||||
from app.accounts import create_account
|
||||
from app.users import create_user, set_admin
|
||||
# Cont admin (id=2)
|
||||
acct_admin = create_account(conn, "Admin Corp")
|
||||
uid_admin = create_user(conn, acct_admin, "admin@corp.ro", "parola_admin_001")
|
||||
set_admin(conn, acct_admin, is_admin=True)
|
||||
# Cont non-admin (id=3)
|
||||
acct_user = create_account(conn, "User Corp")
|
||||
uid_user = create_user(conn, acct_user, "user@corp.ro", "parola_user_001")
|
||||
conn.close()
|
||||
|
||||
app = _make_app_admin()
|
||||
with TestClient(app, follow_redirects=False) as c:
|
||||
yield c, acct_admin, uid_admin, acct_user, uid_user
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_require_admin_blocheaza_non_admin(client_admin):
|
||||
"""User logat NON-admin pe ruta admin -> 403."""
|
||||
client, acct_admin, uid_admin, acct_user, uid_user = client_admin
|
||||
client.get(f"/set-session?account_id={acct_user}&user_id={uid_user}")
|
||||
resp = client.get("/admin-only")
|
||||
assert resp.status_code == 403
|
||||
assert "admin" in resp.json()["detail"]
|
||||
|
||||
|
||||
def test_require_admin_lasa_admin(client_admin):
|
||||
"""User logat ADMIN pe ruta admin -> 200 cu account_id."""
|
||||
client, acct_admin, uid_admin, acct_user, uid_user = client_admin
|
||||
client.get(f"/set-session?account_id={acct_admin}&user_id={uid_admin}")
|
||||
resp = client.get("/admin-only")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["account_id"] == acct_admin
|
||||
|
||||
|
||||
def test_require_admin_nelogat_ridica_login_required(client_admin):
|
||||
"""Fara sesiune, require_admin ridica LoginRequired (-> 401 in app-ul nostru de test)."""
|
||||
client, *_ = client_admin
|
||||
resp = client.get("/admin-only")
|
||||
# In app-ul de test, LoginRequired -> 401
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Teste CLI tools/account.py set-admin
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _run_account(argv):
|
||||
from tools.account import main
|
||||
return main(argv)
|
||||
|
||||
|
||||
def test_cli_set_admin_marcheaza_contul(env_cli, capsys):
|
||||
"""CLI set-admin --account N seteaza is_admin=1 pe userii contului."""
|
||||
# Creeaza cont cu user
|
||||
from app.db import get_connection
|
||||
from app.accounts import create_account
|
||||
from app.users import create_user, is_account_admin
|
||||
conn = get_connection()
|
||||
acct_id = create_account(conn, "Service CLI Admin")
|
||||
create_user(conn, acct_id, "cli_admin@corp.ro", "parola_cli_admin_1")
|
||||
conn.close()
|
||||
|
||||
rc = _run_account(["set-admin", "--account", str(acct_id)])
|
||||
out = capsys.readouterr().out
|
||||
assert rc == 0
|
||||
assert str(acct_id) in out or "admin" in out.lower()
|
||||
|
||||
conn2 = get_connection()
|
||||
assert is_account_admin(conn2, acct_id) is True
|
||||
conn2.close()
|
||||
|
||||
|
||||
def test_cli_set_admin_remove(env_cli, capsys):
|
||||
"""CLI set-admin --account N --remove scoate adminul."""
|
||||
from app.db import get_connection
|
||||
from app.accounts import create_account
|
||||
from app.users import create_user, set_admin, is_account_admin
|
||||
conn = get_connection()
|
||||
acct_id = create_account(conn, "Service CLI Remove")
|
||||
create_user(conn, acct_id, "remove@corp.ro", "parola_remove_001")
|
||||
set_admin(conn, acct_id, is_admin=True)
|
||||
conn.close()
|
||||
|
||||
rc = _run_account(["set-admin", "--account", str(acct_id), "--remove"])
|
||||
assert rc == 0
|
||||
|
||||
conn2 = get_connection()
|
||||
assert is_account_admin(conn2, acct_id) is False
|
||||
conn2.close()
|
||||
|
||||
|
||||
def test_cli_set_admin_cont_inexistent_exit_2(env_cli, capsys):
|
||||
"""CLI set-admin pe cont inexistent -> exit code 2 + mesaj pe stderr."""
|
||||
rc = _run_account(["set-admin", "--account", "9999"])
|
||||
err = capsys.readouterr().err
|
||||
assert rc == 2
|
||||
assert "inexistent" in err or "eroare" in err.lower()
|
||||
Reference in New Issue
Block a user