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>
This commit is contained in:
149
tests/test_web_session.py
Normal file
149
tests/test_web_session.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user