Files
flowdeck/tests/conftest.py
T
bruno 0861f1fdbf fix: A19 (partiel) — CSRF réservé aux vrais cas d'exemption (v7.3.5)
- Scan de tous les appels `fetch` non-GET du front par préfixe : 12 préfixes
  n'ont AUCUN call site sans `X-CSRF-Token` → sortis de `EXCLUDED_PATHS`
  (`/db/`, `/workspace`, `/workspace/automations`, `/board/api/favorites`,
  `/api/pages`, `/api/recents`, `/api/notifications`, `/api/comments`,
  `/api/automations`, `/api/github`, `/api/admin`, `/api/onboarding`)
- `welcome.html` : les 2 POST `/api/onboarding/*` reçoivent le header
  (`adminFetch` prouve que `/api/admin` était déjà couvert)
- reste A19 (front à équiper) : `/api/workspace` (+`/api/workspaces`),
  `/api/local-workspace`, `/api/settings`, `/api/gitea`, `/api/agent` — 49 fetch
- tests : helper `anon_csrf()` (anonyme + CSRF valide) pour isoler les 401 de
  route du 403 middleware — 4 tests d'anonymat ajustés
- suite **1026/1026** · `ruff check app tests` OK
2026-09-30 23:38:03 -04:00

146 lines
4.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""FlowDeck — pytest fixtures and configuration.
Each test gets a fresh, isolated SQLite database and backup directory so the
suite is safe to run in parallel (``pytest -n auto``): workers never share a
database file, and no state leaks between tests.
"""
import os
import re
import tempfile
from pathlib import Path
import httpx
import pytest
from fastapi.testclient import TestClient
class _TestSessionAuth(httpx.Auth):
"""Session + CSRF injectés à la volée (jamais dans le cookie jar du client).
- `flowdeck_session` ajouté seulement s'il est absent de la requête (un test
peut fournir la sienne via `cookies=`) ;
- `csrf_token` idem, et l'en-tête `X-CSRF-Token` suit TOUJOURS le cookie
courant (le token tourne quand `/api/csrf-token` est appelé) ;
- un test qui veut l'anonymat fait `anon(client)` → `client.auth = None`.
"""
CSRF_FALLBACK = "csrf-test-token"
def __init__(self, session_token: str):
self.session_token = session_token
def auth_flow(self, request):
ch = request.headers.get("cookie", "")
add = []
if "flowdeck_session=" not in ch:
add.append(f"flowdeck_session={self.session_token}")
if "csrf_token=" not in ch:
add.append(f"csrf_token={self.CSRF_FALLBACK}")
if add:
request.headers["cookie"] = "; ".join(([ch] if ch else []) + add)
if "X-CSRF-Token" not in request.headers:
m = re.search(r"csrf_token=([^;]+)", request.headers.get("cookie", ""))
if m:
request.headers["X-CSRF-Token"] = m.group(1)
yield request
def login_test_client(tc, user_id: int = 1, login: str = "tester", is_admin: int = 1):
"""Connecte un TestClient (A3–A7 : les routes testées exigent une session)."""
from app.auth.session import SessionManager
from app.db import get_conn
with get_conn() as conn:
conn.execute(
"INSERT OR IGNORE INTO users (id, login, full_name, is_admin) VALUES (?,?,?,?)",
(user_id, login, login.title(), is_admin),
)
conn.commit()
tc.auth = _TestSessionAuth(
SessionManager.create_session(
{"id": user_id, "login": login, "full_name": login.title(), "is_admin": is_admin}
)
)
return tc
def anon(client):
"""Test d'anonymat : plus de session, plus de CSRF par défaut."""
client.cookies.clear()
client.headers.pop("X-CSRF-Token", None)
client.auth = None
return client
def anon_csrf(client):
"""Anonyme MAIS CSRF valide — comme un navigateur qui a déjà chargé une page.
Sert aux tests d'"isolation auth" : on veut le 401 de la route, pas le 403
du middleware CSRF qui passerait avant.
"""
anon(client)
client.cookies.set("csrf_token", "csrf-anon")
client.headers["X-CSRF-Token"] = "csrf-anon"
return client
@pytest.fixture
def client():
"""FastAPI TestClient with a fresh temporary SQLite database."""
db_file = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
db_path = db_file.name
db_file.close()
backup_dir = tempfile.mkdtemp(prefix="fd_backups_")
data_dir = tempfile.mkdtemp(prefix="fd_data_")
# Set env BEFORE importing app modules (config reads at import time).
os.environ["DATABASE_URL"] = f"sqlite:///{db_path}"
os.environ["APP_SECRET_KEY"] = "test-secret-for-tests"
os.environ["RATE_LIMIT_ENABLED"] = "false"
os.environ["PUBLIC_API_INSECURE_OK"] = "true"
os.environ["BACKUP_ENABLED"] = "true"
os.environ["BACKUP_DIR"] = backup_dir
os.environ["PROJECT_SYNC_ENABLED"] = "false"
# Point data-root (uploads/, emoji/, covers/) at a writable temp dir so tests
# don't depend on the container's /data path existing on a dev host.
os.environ["FLOWDECK_DATA_DIR"] = data_dir
# IMPORTANT: mutate the existing Settings singleton in place — do NOT rebind
# `app.config.settings`. Modules such as `app.services.backup` and
# `app.routers.auth` hold a direct reference imported at load time, so
# rebinding would leave them pointing at the stale defaults (this was the
# cause of the previously-skipped flaky backup tests).
import app.config
s = app.config.settings
s.database_url = f"sqlite:///{db_path}"
s.app_secret_key = "test-secret-for-tests"
s.rate_limit_enabled = False
s.public_api_insecure_ok = True
s.backup_enabled = True
s.backup_dir = backup_dir
s.backup_interval_hours = 24
s.backup_keep = 30
s.project_sync_enabled = False
from app.db import init_db
from app.main import app
init_db()
yield login_test_client(TestClient(app))
# Cleanup
try:
os.unlink(db_path)
except PermissionError:
pass # Windows: file may still be open in another thread
for p in Path(backup_dir).glob("*.db"):
try:
p.unlink()
except PermissionError:
pass
try:
Path(backup_dir).rmdir()
except OSError:
pass