Files
rar-autopass/tests/test_web_session.py
Claude Agent 504b490d3b feat(web): self-onboarding multi-tenant + auth sesiune (PRD 3.3a)
Canalul web trece de la 100% deschis (hardcodat cont 1) la autentificat si
multi-tenant. Un service nou se inregistreaza din browser, primeste o cheie API
(o singura data) si o sesiune; contul se creeaza "in asteptare" (active=0) si nu
trimite la RAR pana la activarea de catre admin (tools/account.py activate).

- users + app/users.py: parole scrypt (salt per-user, eticheta parametri onorata
  la verify pentru migrare cost), email unic case-insensitive
- sesiune: SessionMiddleware (same_site=strict, https_only config) + app/web/session.py
  (current_account/web_account/require_login->LoginRequired, set_session clear-inainte)
- CSRF (app/web/csrf.py) enforce in prod inclusiv pe login/signup + rate-limit
  in-proces (app/web/ratelimit.py) pe signup si login
- signup/login/logout (app/web/auth_routes.py): signup tranzactie atomica,
  cheie-o-data, log SIGNUP pentru descoperire admin
- dashboard + import scoped pe contul sesiunii (regula NULL->cont 1); toate rutele
  web care ating date sensibile sub require_login; nomenclator ramane global
- banner "cont in asteptare" pentru conturi active=0
- gate worker: claim_one LEFT JOIN accounts COALESCE(active,1)=1 (account_id NULL=activ)

VERIFY context curat (2 runde): leak cross-account /_fragments/mapari prins+reparat.
/code-review high: csrf_token lipsa pe re-randari de eroare, scrypt_params ignorat,
login fara rate-limit -- toate reparate. 361 teste pass (de la 313).

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

150 lines
5.1 KiB
Python

"""Teste US-002 (PRD 3.3): SessionMiddleware + helper-e sesiune in app/web/session.py."""
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
# ---- App minimal pentru teste (fara init_db, fara DB) ----
def _make_app(web_auth_required: bool = True, session_secret: str = "test-secret-dev") -> FastAPI:
"""Construieste un app FastAPI minimal cu SessionMiddleware + rute de test."""
mini = FastAPI()
mini.add_middleware(
SessionMiddleware,
secret_key=session_secret,
session_cookie="autopass_session",
https_only=False,
same_site="strict",
)
@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("/get-session")
def get_sess(request: Request):
from app.web.session import current_account, current_user_id
return {
"account_id": current_account(request),
"user_id": current_user_id(request),
}
@mini.get("/protected")
def protected(request: Request):
from app.web.session import require_login
aid = require_login(request)
return {"account_id": aid}
@mini.get("/logout")
def logout_ep(request: Request):
from app.web.session import clear_session
clear_session(request)
return {"ok": True}
@mini.get("/web-account")
def web_account_ep(request: Request):
from app.web.session import web_account
return {"account_id": web_account(request)}
# Handler pentru LoginRequired
from app.web.session import LoginRequired
from starlette.responses import RedirectResponse
@mini.exception_handler(LoginRequired)
async def login_required_handler(request: Request, exc: LoginRequired):
return RedirectResponse("/login", status_code=303)
return mini
@pytest.fixture()
def client_auth(monkeypatch):
"""Client cu web_auth_required=True."""
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "true")
from app.config import get_settings
get_settings.cache_clear()
app = _make_app(web_auth_required=True)
with TestClient(app, follow_redirects=False) as c:
yield c
get_settings.cache_clear()
@pytest.fixture()
def client_dev(monkeypatch):
"""Client cu web_auth_required=False (dev bypass)."""
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
from app.config import get_settings
get_settings.cache_clear()
app = _make_app(web_auth_required=False)
with TestClient(app, follow_redirects=False) as c:
yield c
get_settings.cache_clear()
def test_ruta_protejata_redirect_login(client_auth):
"""Fara sesiune si web_auth_required=True -> 303 redirect catre /login."""
resp = client_auth.get("/protected")
assert resp.status_code == 303
assert resp.headers["location"] == "/login"
def test_sesiune_seteaza_si_citeste_cont(client_auth):
"""set_session stocheaza account_id si user_id; current_account/current_user_id le citesc."""
client_auth.get("/set-session?account_id=5&user_id=7")
resp = client_auth.get("/get-session")
assert resp.status_code == 200
data = resp.json()
assert data["account_id"] == 5
assert data["user_id"] == 7
def test_logout_curata_sesiunea(client_auth):
"""clear_session sterge account_id si user_id din sesiune."""
client_auth.get("/set-session?account_id=3&user_id=4")
# Verifica ca sesiunea e setata
data_before = client_auth.get("/get-session").json()
assert data_before["account_id"] == 3
# Logout
client_auth.get("/logout")
data_after = client_auth.get("/get-session").json()
assert data_after["account_id"] is None
assert data_after["user_id"] is None
def test_dev_bypass_cont_1(client_dev, monkeypatch):
"""web_auth_required=False -> web_account() returneaza 1 (DEFAULT_ACCOUNT_ID) fara sesiune."""
resp = client_dev.get("/web-account")
assert resp.status_code == 200
assert resp.json()["account_id"] == 1
def test_set_session_curata_sesiunea_anterioara(client_auth):
"""set_session face clear() inainte de a seta (C3 anti-fixare sesiune)."""
# Seteaza sesiune initiala cu cont 10
client_auth.get("/set-session?account_id=10&user_id=10")
data_initial = client_auth.get("/get-session").json()
assert data_initial["account_id"] == 10
# Re-login cu cont nou 20 — sesiunea veche trebuie stearsa inainte
client_auth.get("/set-session?account_id=20&user_id=20")
data_nou = client_auth.get("/get-session").json()
assert data_nou["account_id"] == 20
assert data_nou["user_id"] == 20
def test_ruta_protejata_cu_sesiune_trece(client_auth):
"""Cu sesiune setata si web_auth_required=True -> ruta protejata raspunde 200."""
client_auth.get("/set-session?account_id=5&user_id=5")
resp = client_auth.get("/protected")
assert resp.status_code == 200
assert resp.json()["account_id"] == 5