diff --git a/scripts/audit_functional.py b/scripts/audit_functional.py new file mode 100644 index 0000000..24e87aa --- /dev/null +++ b/scripts/audit_functional.py @@ -0,0 +1,102 @@ +""" +FlowDeck — Audit fonctionnel de bout-en-bout. +Base SQLite isolée (init schéma) + TestClient FastAPI réel. +Vérifie status code + payload de chaque processus métier. +""" +import os, tempfile, json, sys +from pathlib import Path + +_tmp = tempfile.mkdtemp(prefix="fd_audit_") +os.environ["DATABASE_URL"] = f"sqlite:///{_tmp}/audit.db" +os.environ["APP_SECRET_KEY"] = "audit-secret-0000" +os.environ["RATE_LIMIT_ENABLED"] = "false" +os.environ["BACKUP_ENABLED"] = "false" +os.environ["FLOWDECK_DATA_DIR"] = _tmp +os.environ["PROJECT_SYNC_ENABLED"] = "false" +os.environ["PROJECT_SYNC"] = "false" + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from fastapi.testclient import TestClient +from app.main import app +from app.db import init_db +init_db() + +client = TestClient(app) +results = [] + +def csrf(): + return client.get("/api/csrf-token").json().get("token", "") + +def h(c): + return {"X-CSRF-Token": c, "Content-Type": "application/json"} + +def register_login(email="audit@flowdeck.local", pwd="secret123"): + c = csrf() + client.post("/auth/register", json={"name": "Audit", "email": email, "password": pwd}, headers=h(c)) + c2 = csrf() + r = client.post("/auth/local-login", json={"email": email, "password": pwd}, headers=h(c2)) + return c2 + +def rec(name, ok, detail=""): + results.append((name, ok, detail)) + print(f"{'PASS' if ok else 'FAIL'} {name}" + (f" — {detail}" if detail else "")) + +print("="*72) +print("FLOWDECK — AUDIT FONCTIONNEL (base isolée)") +print("="*72) + +try: + # ── 1. AUTH & WORKSPACE ─────────────────────────────── + c = register_login() + me = client.get("/api/users/me").json() + rec("register + login user local", me.get("login") == "audit@flowdeck.local" or me.get("email") == "audit@flowdeck.local", f"login={me.get('login')} admin={me.get('is_admin')}") + + r = client.post("/api/workspaces", json={"name": "Audit WS"}, headers=h(c)) + rec("POST /api/workspaces", r.status_code in (200, 201, 302), f"HTTP {r.status_code} {str(r.text)[:70]}") + + # ── 2. CRÉATION NOTE ────────────────────────────────── + r = client.post("/api/pages", json={"workspace": "Audit WS", "title": "Note de test", "content": "## MD init", "content_format": "markdown"}, headers=h(c)) + pid = r.json().get("id") if r.status_code in (200, 201) else None + rec("POST /api/pages (création note)", pid is not None, f"HTTP {r.status_code} body={str(r.text)[:60]}") + + # ── 3. CRÉATION BASE DE DONNÉES (collection) ────────── + r = client.post("/db/api", json={"name": "Tâches", "schema": [{"name":"Statut","prop_type":"select","options":["À faire","En cours","Fait"]}]}, headers=h(c)) + coll = r.json() + cid = coll.get("id") + rec("POST /db/api (création DB)", cid is not None, f"HTTP {r.status_code} id={cid}") + + # ── 4. TÂCHES (rows) ────────────────────────────────── + if cid: + r = client.post(f"/db/{cid}/pages/api", json={"title": "Terminer audit", "property_values_json": json.dumps({"Statut":"En cours"})}, headers=h(c)) + rec("POST /{cid}/pages (création row)", r.status_code in (200, 201, 200), f"HTTP {r.status_code} {str(r.text)[:70]}") + + # ── 5. (Kanban board-config testé via instance Gitea réelle — cf. audit live) ── + + # ── 6. SHARE / PUBLISH ──────────────────────────────── + if pid: + r = client.post(f"/api/pages/{pid}/share", json={"email":"other@x.com","permission":"edit"}, headers=h(c)) + rec("POST /pages/{id}/share", r.status_code in (200, 201), f"HTTP {r.status_code} {str(r.text)[:70]}") + r = client.post(f"/api/pages/{pid}/publish", json={}, headers=h(c)) + rec("POST /pages/{id}/publish", r.status_code in (200, 201), f"HTTP {r.status_code} {str(r.text)[:70]}") + + # ── 7. EXPORT ───────────────────────────────────────── + if pid: + r = client.get(f"/api/export/markdown/{pid}") + rec("GET /api/export/markdown", r.status_code == 200, f"HTTP {r.status_code} len={len(r.content)}") + + # ── 8. RECHERCHE ────────────────────────────────────── + r = client.get("/api/search", params={"q": "audit"}) + rec("GET /api/search?q=", r.status_code == 200, f"HTTP {r.status_code}") + + # ── 9. AGENTS ───────────────────────────────────────── + r = client.get("/api/agent") + rec("GET /api/agent (agents)", r.status_code == 200, f"HTTP {r.status_code}") + +except Exception as e: + import traceback + traceback.print_exc() + rec("EXÉCUTION SCRIPT", False, str(e)[:100]) + +print("="*72) +ok = sum(1 for _, o, _ in results if o) +print(f"RÉSULTAT: {ok}/{len(results)} processus OK") \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 22ae76b..8d478f0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,7 @@ def client(): 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}" @@ -28,6 +29,9 @@ def client(): 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 diff --git a/tests/test_app.py b/tests/test_app.py index df02f53..11c6822 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1719,9 +1719,16 @@ def _oauth_request(headers: dict): }) -def test_get_redirect_uri_from_host_header(): - """get_redirect_uri() derives the URI from the Host header.""" +def test_get_redirect_uri_from_host_header(monkeypatch): + """get_redirect_uri() derives the URI from the Host header when used directly. + + NOTE: an explicit ``OAUTH_REDIRECT_URI`` env override (as present in the + project ``.env``) takes priority by design. This test pins that override to + empty so it exercises the Host-header derivation path in isolation. + """ + from app.config import settings from app.routers.auth import get_redirect_uri + monkeypatch.setattr(settings, "oauth_redirect_uri", "") uri = get_redirect_uri(_oauth_request({"host": "192.168.30.101:8080"})) assert uri == "http://192.168.30.101:8080/auth/callback"