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.auth.rate_limit import login_limiter, magic_link_limiter from app.config import get_settings from app.db import get_session from app.main import app from app.models import Account, DayPart, Edition, Role @pytest.fixture(autouse=True) def _never_use_real_smtp(monkeypatch): """Quel que soit le `.env` local, les tests n'ouvrent jamais de connexion SMTP : `EMAIL_ENABLED` est forcé à false.""" monkeypatch.setenv("EMAIL_ENABLED", "false") get_settings.cache_clear() yield get_settings.cache_clear() @pytest.fixture(autouse=True) def _reset_rate_limiters(): """Les limiteurs de `/auth/login` et `/auth/magic-link` sont des singletons en mémoire (voir `app/auth/rate_limit.py`) : un test qui en déclenche un ne doit pas polluer les suivants.""" login_limiter.reset() magic_link_limiter.reset() yield @pytest.fixture(scope="session") def engine(): """Vraie base PostgreSQL de test (voir docker/postgres-init).""" eng = create_engine(get_settings().test_database_url, pool_pre_ping=True) SQLModel.metadata.drop_all(eng) SQLModel.metadata.create_all(eng) yield eng SQLModel.metadata.drop_all(eng) @pytest.fixture def session(engine): """Session isolée : chaque test s'exécute dans une transaction annulée à la fin, y compris ses `commit()` (savepoints).""" connection = engine.connect() transaction = connection.begin() s = Session(bind=connection, join_transaction_mode="create_savepoint") try: yield s finally: s.close() transaction.rollback() connection.close() @pytest.fixture def edition(session) -> Edition: """Édition de 4 jours avec 3 demi-journées paramétrées.""" ed = Edition( year=2026, name="Festival Test", start_date=dt.date(2026, 7, 9), end_date=dt.date(2026, 7, 12), retention_days=365, ) session.add(ed) session.commit() session.refresh(ed) session.add_all( [ DayPart( edition_id=ed.id, code="MORNING", label="Matin", start_time=dt.time(6, 0), end_time=dt.time(13, 0), order=0, ), DayPart( edition_id=ed.id, code="AFTERNOON", label="Après-midi", start_time=dt.time(13, 0), end_time=dt.time(19, 0), order=1, ), DayPart( edition_id=ed.id, code="EVENING", label="Soirée", start_time=dt.time(19, 0), end_time=dt.time(6, 0), order=2, ), ] ) session.commit() return ed @pytest.fixture def day_parts(session, edition) -> dict[str, DayPart]: parts = session.exec( 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, password: str | None = None, ) -> Account: from app.auth.passwords import hash_password account = Account( edition_id=edition.id, role=role, email=email or f"{role.value.lower()}{next(counter)}@test", volunteer_id=volunteer_id, password_hash=hash_password(password) if password else None, ) 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() @pytest.fixture def outbox(monkeypatch) -> list: """Capture les emails au lieu de les remettre (SMTP désactivé de toute façon en test ; ceci permet de les inspecter).""" box: list = [] monkeypatch.setattr( "app.email.sender.deliver_many", lambda messages: box.extend(messages) ) return box