Étape 7 : authentification par lien magique + JWT + rôles
- app/auth/ : jetons (lien magique aléatoire haché SHA-256, JWT HS256), dépendances get_current_account / current_account_in_edition / require_roles + require_coordinator / require_staff, et ensure_can_write_position/_shift - Endpoints app/api/auth.py : POST .../auth/magic-link (202 constant, pas de divulgation), POST .../auth/verify (JWT, jeton à usage unique), GET /auth/me - Envoi d'email stubé : le lien est journalisé (logger app.auth, identifiants seulement, aucune donnée nominative) - app/api/accounts.py : CRUD des comptes réservé à la COORDINATION - Rôles appliqués sur toute l'API : COORDINATION accès complet ; RÉFÉRENT lecture globale + écriture sur ses seuls postes/créneaux/affectations ; BÉNÉVOLE sa fiche et ses affectations, aucune écriture - imports.py aligné sur la dépendance get_edition (404 avant 403) - Secret JWT porté à >= 32 octets (config, .env.example, docker-compose) - seed : crée un compte VOLUNTEER pour chaque bénévole avec email (104 comptes) - conftest : fixtures make_account / coordinator / as_account / raw_client ; la suite existante s'exécute en COORDINATION - 13 tests d'authentification + de rôles ; 119 tests au total ; alembic check toujours vert (Account / MagicLinkToken déjà dans la migration 0001) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c9dda65361
commit
ced922f785
@@ -1,14 +1,16 @@
|
||||
import datetime as dt
|
||||
import itertools
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlmodel import Session, SQLModel, create_engine, select
|
||||
|
||||
from app import models # noqa: F401 (enregistre les tables sur SQLModel.metadata)
|
||||
from app.auth.deps import get_current_account
|
||||
from app.config import get_settings
|
||||
from app.db import get_session
|
||||
from app.main import app
|
||||
from app.models import DayPart, Edition
|
||||
from app.models import Account, DayPart, Edition, Role
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -36,14 +38,6 @@ def session(engine):
|
||||
connection.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(session):
|
||||
app.dependency_overrides[get_session] = lambda: session
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def edition(session) -> Edition:
|
||||
"""Édition de 4 jours avec 3 demi-journées paramétrées."""
|
||||
@@ -83,3 +77,60 @@ def day_parts(session, edition) -> dict[str, DayPart]:
|
||||
select(DayPart).where(DayPart.edition_id == edition.id)
|
||||
)
|
||||
return {p.code: p for p in parts}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_account(session, edition):
|
||||
"""Fabrique de comptes rattachés à l'édition de test."""
|
||||
counter = itertools.count()
|
||||
|
||||
def _make(
|
||||
role: Role, *, volunteer_id=None, email: str | None = None
|
||||
) -> Account:
|
||||
account = Account(
|
||||
edition_id=edition.id,
|
||||
role=role,
|
||||
email=email or f"{role.value.lower()}{next(counter)}@test",
|
||||
volunteer_id=volunteer_id,
|
||||
)
|
||||
session.add(account)
|
||||
session.commit()
|
||||
session.refresh(account)
|
||||
return account
|
||||
|
||||
return _make
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def coordinator(make_account) -> Account:
|
||||
return make_account(Role.COORDINATOR)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def as_account(session):
|
||||
"""Fabrique de clients HTTP authentifiés. Fixe l'identité **active** de
|
||||
l'app : le dernier appel l'emporte. Pour rejouer un autre rôle après en
|
||||
avoir simulé un, rappeler `as_account(...)`."""
|
||||
|
||||
def _make(account: Account) -> TestClient:
|
||||
app.dependency_overrides[get_session] = lambda: session
|
||||
app.dependency_overrides[get_current_account] = lambda: account
|
||||
return TestClient(app)
|
||||
|
||||
yield _make
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(coordinator, as_account) -> TestClient:
|
||||
"""Client authentifié en COORDINATION (rôle par défaut des tests)."""
|
||||
return as_account(coordinator)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def raw_client(session):
|
||||
"""Client sans authentification simulée (pour tester le lien magique)."""
|
||||
app.dependency_overrides[get_session] = lambda: session
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
Reference in New Issue
Block a user