fix: A3–A8 — bloc « fallback admin » de l'audit sécurité (401 sans session, ACL, CSRF)
FlowDeck CI / lint (push) Successful in 1m49s
FlowDeck CI / test (push) Successful in 21m47s
FlowDeck CI / docker (push) Successful in 1m45s

- A3 : helper `_require_user_id()` (401 sans session) sur profile/password/token/forge ; `PUT /api/user/password` exige `current_password` vérifié ; `/api/user` sorti de la liste CSRF exemptée
- A4 : `POST /api/v1/token` → 401 sans session, chemin legacy `user_id=0` supprimé
- A5 : CRUD membres d'espace (POST/PUT/DELETE) : session + rôle admin de l'espace (ou admin global), placeholder user créé en `is_admin=0`, GET membres sans session → 401
- A6 : `_require_view` → 404 et `_require_edit` → 401 quand il n'y a pas de session (fin du legacy single-user sur les collections)
- A7 : création ET lecture de page → 401 sans session (PermissionManager conservé) ; `/board/api/pages` sorti de `EXCLUDED_PATHS` ; header CSRF manquant ajouté sur setItemIcon (local_workspace)
- A8 : seed admin sans mot de passe codé en dur — aléatoire au premier boot loggé une fois, ou `FLOWDECK_ADMIN_PASSWORD` ; re-seed seulement si absent
- tests : client connecté par défaut via `_TestSessionAuth` (session + CSRF injectés à la volée, jamais dans le cookie jar → plus de CookieConflict), helper `anon()` sur les 40 tests d'anonymat ; 1016/1016 verts, `ruff check app tests` OK
This commit is contained in:
2026-09-30 22:04:13 -04:00
parent e6c1f7dbb3
commit d125eb399e
36 changed files with 260 additions and 67 deletions
+61 -1
View File
@@ -5,13 +5,73 @@ 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
@pytest.fixture
def client():
"""FastAPI TestClient with a fresh temporary SQLite database."""
@@ -55,7 +115,7 @@ def client():
from app.main import app
init_db()
yield TestClient(app)
yield login_test_client(TestClient(app))
# Cleanup
try:
+2 -1
View File
@@ -10,6 +10,7 @@ import os
import tempfile
import pytest
from conftest import login_test_client
from fastapi.testclient import TestClient
@@ -45,7 +46,7 @@ def client():
)
conn.commit()
yield TestClient(app)
yield login_test_client(TestClient(app))
try:
os.unlink(db_path)
+2 -1
View File
@@ -10,6 +10,7 @@ import os
import tempfile
import pytest
from conftest import login_test_client
from fastapi.testclient import TestClient
@@ -41,7 +42,7 @@ def client():
)
conn.commit()
yield TestClient(app)
yield login_test_client(TestClient(app))
try:
os.unlink(db_path)
+14 -1
View File
@@ -4,6 +4,7 @@ import os
import tempfile
import pytest
from conftest import anon, login_test_client
from fastapi.testclient import TestClient
@@ -40,7 +41,7 @@ def client():
)
conn.commit()
yield TestClient(app)
yield login_test_client(TestClient(app))
try:
os.unlink(db_path)
@@ -88,6 +89,7 @@ def test_board_404(client):
def test_csrf_rejected(client):
anon(client)
resp = client.post("/api/move?owner=x&repo=y&issue_id=1&column=Test")
assert resp.status_code == 403
@@ -134,6 +136,7 @@ def test_card_detail_html(client):
def test_create_issue_api(client):
anon(client)
resp = client.post("/api/issues/test/test?title=Test%20Issue&body=Test%20body")
# 403 CSRF or 500 if Gitea down
assert resp.status_code in (403, 500)
@@ -201,11 +204,13 @@ def test_get_ai_keywords(client):
def test_csrf_protects_properties_post(client):
anon(client)
resp = client.post("/board/api/properties/test/test?name=Priority&prop_type=select")
assert resp.status_code == 403 # CSRF
def test_csrf_protects_sync(client):
anon(client)
resp = client.post("/board/api/sync/test/test")
assert resp.status_code == 403 # CSRF
@@ -1354,6 +1359,7 @@ def _create_regular_session():
def test_admin_list_users_unauthorized(client):
anon(client)
"""GET /api/admin/users — 403 without admin session."""
resp = client.get("/api/admin/users")
assert resp.status_code == 403
@@ -1588,6 +1594,7 @@ def test_admin_stats(client):
def test_admin_stats_unauthorized(client):
anon(client)
"""GET /api/admin/stats — 403 for non-admin."""
resp = client.get("/api/admin/stats")
assert resp.status_code == 403
@@ -1663,6 +1670,7 @@ def test_gitea_status_with_expired_token(client):
def test_gitea_disconnect_no_auth(client):
anon(client)
"""DELETE /api/gitea/disconnect — 401 without session."""
resp = client.delete("/api/gitea/disconnect")
assert resp.status_code == 401
@@ -1883,6 +1891,7 @@ def test_auth_user_authenticated(client):
def test_auth_user_unauthenticated(client):
anon(client)
"""GET /auth/user — returns authenticated=false without session."""
resp = client.get("/auth/user")
assert resp.status_code == 200
@@ -1943,6 +1952,7 @@ def test_gitea_private_pages_list_no_auth(client):
def test_gitea_private_pages_create_no_auth(client):
anon(client)
"""POST private-pages — 401 without session."""
resp = client.post("/api/gitea/projects/owner/repo/private-pages", json={"title": "Test"})
assert resp.status_code == 401
@@ -2019,6 +2029,7 @@ def test_page_renders_breadcrumb_data(client):
def test_landing_page_no_auth(client):
anon(client)
"""Visiting / without auth shows the landing page."""
resp = client.get("/", follow_redirects=False)
assert resp.status_code == 200
@@ -2095,6 +2106,7 @@ def test_register_duplicate_rejected(client):
def test_session_expired_redirect(client):
anon(client)
"""Unauthenticated access to protected page redirects with expired param."""
resp = client.get("/workspaces", follow_redirects=False)
assert resp.status_code == 302
@@ -3464,6 +3476,7 @@ def test_v490_page_mentions_endpoint(client):
def test_v490_notifications_require_auth(client):
anon(client)
r = client.get("/api/notifications")
assert r.status_code == 401
+2 -1
View File
@@ -8,6 +8,7 @@ import os
import tempfile
import pytest
from conftest import login_test_client
from fastapi.testclient import TestClient
@@ -31,7 +32,7 @@ def client():
conn.commit()
tc = TestClient(app)
yield tc
yield login_test_client(tc)
os.unlink(db_path)
+2 -1
View File
@@ -10,6 +10,7 @@ import os
import tempfile
import pytest
from conftest import login_test_client
from fastapi.testclient import TestClient
@@ -34,7 +35,7 @@ def client():
conn.commit()
tc = TestClient(app, raise_server_exceptions=False)
yield tc
yield login_test_client(tc)
os.unlink(db_path)
+2 -1
View File
@@ -4,6 +4,7 @@ import os
import tempfile
import pytest
from conftest import login_test_client
from fastapi.testclient import TestClient
@@ -28,7 +29,7 @@ def client():
from app.main import app
init_db()
yield TestClient(app)
yield login_test_client(TestClient(app))
os.unlink(db_path)
+4 -1
View File
@@ -10,6 +10,7 @@ import os
import tempfile
import pytest
from conftest import anon, login_test_client
from fastapi.testclient import TestClient
from starlette.websockets import WebSocketDisconnect
@@ -38,7 +39,7 @@ def client():
manager._rooms = {}
tc = TestClient(app, raise_server_exceptions=False)
yield tc
yield login_test_client(tc)
os.unlink(db_path)
@@ -113,6 +114,7 @@ def test_merge_ops_sequential():
# ── Auth & présence de page ──
def test_ws_requires_auth(client):
anon(client)
_make_page()
with pytest.raises(WebSocketDisconnect) as exc:
with client.websocket_connect("/ws/pages/1") as ws:
@@ -121,6 +123,7 @@ def test_ws_requires_auth(client):
def test_ws_auth_rejected(client):
anon(client)
_make_page()
with pytest.raises(WebSocketDisconnect) as exc:
with client.websocket_connect("/ws/pages/1") as ws:
+4 -1
View File
@@ -16,6 +16,7 @@ import os
import tempfile
import pytest
from conftest import anon, login_test_client
from fastapi.testclient import TestClient
from starlette.websockets import WebSocketDisconnect
@@ -49,7 +50,7 @@ def client():
manager.stat_connections_total = 0
tc = TestClient(app, raise_server_exceptions=False)
yield tc
yield login_test_client(tc)
os.unlink(db_path)
@@ -193,6 +194,7 @@ def test_apply_op_still_lww_without_base():
# ── protocole WS ─────────────────────────────────────────────────────────
def test_ws_requires_auth(client):
anon(client)
_make_page()
with pytest.raises(WebSocketDisconnect) as exc:
with client.websocket_connect("/ws/pages/1") as ws:
@@ -392,6 +394,7 @@ def test_ws_stats_endpoint(client):
def test_ws_stats_requires_auth(client):
anon(client)
r = client.get("/api/realtime/stats")
assert r.status_code == 200
assert r.json() == {"error": "unauthorized"}
+2 -1
View File
@@ -7,6 +7,7 @@ import os
import tempfile
import pytest
from conftest import login_test_client
from fastapi.testclient import TestClient
@@ -31,7 +32,7 @@ def client():
from app.main import app
init_db()
yield TestClient(app)
yield login_test_client(TestClient(app))
os.unlink(db_path)
+4
View File
@@ -7,6 +7,8 @@ import json
import re
from pathlib import Path
from conftest import anon
ROOT = Path(__file__).resolve().parent.parent
@@ -47,6 +49,7 @@ def test_sw_served_via_static_mount(client):
def test_sw_registration_present_on_landing(client):
anon(client)
"""Anonymous entry point (/) registers the SW (assets are public)."""
resp = client.get("/")
assert resp.status_code in (200, 302)
@@ -67,6 +70,7 @@ def test_sw_registration_with_background_sync_in_base(client):
def test_base_has_pwa_meta_tags(client):
anon(client)
resp = client.get("/")
body = resp.text
assert 'rel="manifest"' in body
+3 -1
View File
@@ -8,6 +8,7 @@ import os
import tempfile
import pytest
from conftest import anon, login_test_client
from fastapi.testclient import TestClient
@@ -41,7 +42,7 @@ def client():
conn.commit()
tc = TestClient(app, raise_server_exceptions=False)
yield tc
yield login_test_client(tc)
os.unlink(db_path)
@@ -109,6 +110,7 @@ def test_share_invalid_permission_rejected(client):
def test_share_requires_auth(client):
anon(client)
pid = _make_page(client)
r = client.post(f"/api/pages/{pid}/share", json={"email": "[email protected]", "permission": "view"})
assert r.status_code == 401
+3
View File
@@ -1,6 +1,8 @@
"""FlowDeck — v6.0.0 PWA offline sync: engine + /api/v2/sync endpoints."""
import time
from conftest import anon
# ── helpers ────────────────────────────────────────────────────────────────
@@ -58,6 +60,7 @@ def _page_updated_epoch(pid) -> float:
def test_sync_requires_auth(client):
anon(client)
assert client.get("/api/v2/sync/status").status_code == 401
assert client.post("/api/v2/sync/batch", json={"mutations": []}).status_code == 401
assert client.get("/api/v2/sync/delta").status_code == 401
+13 -4
View File
@@ -13,6 +13,7 @@ import secrets
import tempfile
import pytest
from conftest import login_test_client
from fastapi.testclient import TestClient
@@ -36,7 +37,7 @@ def client():
from app.main import app
init_db()
yield TestClient(app)
yield login_test_client(TestClient(app))
try:
os.unlink(db_path)
@@ -58,7 +59,11 @@ def _login(client):
uid = conn.execute("SELECT id FROM users WHERE login=?", (login,)).fetchone()["id"]
conn.commit()
session = SessionManager.create_session({"id": uid, "login": login, "is_admin": 0})
csrf = client.get("/api/csrf-token", cookies={"flowdeck_session": session}).json()["csrf_token"]
# Réutilise le CSRF du jar : /api/csrf-token fait tourner le token à chaque
# appel, un token rendu par un login précédent deviendrait invalide (403).
csrf = client.cookies.get("csrf_token")
if not csrf:
csrf = client.get("/api/csrf-token", cookies={"flowdeck_session": session}).json()["csrf_token"]
return session, csrf, uid
@@ -76,13 +81,17 @@ def _login_admin(client):
uid = conn.execute("SELECT id FROM users WHERE login=?", (login,)).fetchone()["id"]
conn.commit()
session = SessionManager.create_session({"id": uid, "login": login, "is_admin": 1})
csrf = client.get("/api/csrf-token", cookies={"flowdeck_session": session}).json()["csrf_token"]
# Réutilise le CSRF du jar : /api/csrf-token fait tourner le token à chaque
# appel, un token rendu par un login précédent deviendrait invalide (403).
csrf = client.cookies.get("csrf_token")
if not csrf:
csrf = client.get("/api/csrf-token", cookies={"flowdeck_session": session}).json()["csrf_token"]
return session, csrf, uid
def _mk_page(client, session, title, blocks=None):
r = client.post("/board/api/pages", params={"title": title, "section": "Private"},
cookies={"flowdeck_session": session}, headers={"X-CSRF-Token": "x"})
cookies={"flowdeck_session": session})
assert r.status_code == 200
pid = r.json()["id"]
if blocks is not None:
+8 -1
View File
@@ -8,6 +8,7 @@ import asyncio
import os
import pytest
from conftest import anon
from fastapi import HTTPException
from fastapi.testclient import TestClient
@@ -66,6 +67,7 @@ def test_api_token_lifecycle(client):
def test_api_tokens_require_authentication(client):
anon(client)
r1 = client.get("/api/settings/tokens")
assert r1.status_code == 401
r2 = client.post("/api/settings/tokens", json={"name": "x"})
@@ -88,7 +90,11 @@ def test_sessions_listed_and_revocable(client):
from app.db import get_conn
with get_conn() as conn:
count = conn.execute("SELECT COUNT(*) FROM user_sessions").fetchone()[0]
count = conn.execute(
"SELECT COUNT(*) FROM user_sessions WHERE user_id IN "
"(SELECT id FROM users WHERE login IN (?, ?))",
("[email protected]", "[email protected]"),
).fetchone()[0]
assert count == 2
# Revoke alice's session using alice's cookie (the test client now has bob's cookie).
@@ -192,6 +198,7 @@ def test_backup_disabled_returns_none(client):
def test_backup_admin_api(client):
anon(client)
"""The backup admin API is admin-only and snapshots on demand."""
# Unauthenticated → forbidden.
assert client.post("/api/settings/backups/run").status_code == 403
+3
View File
@@ -14,6 +14,8 @@ import secrets
import time
import zipfile
from conftest import anon
from app.db import get_conn
@@ -382,6 +384,7 @@ class TestMappingAndWizard:
assert props["Code"] == "007"
def test_wizard_page_requires_auth(self, client):
anon(client)
r = client.get("/import", follow_redirects=False)
assert r.status_code in (302, 307)
+4 -3
View File
@@ -12,6 +12,7 @@ import secrets
import tempfile
import pytest
from conftest import login_test_client
from fastapi.testclient import TestClient
@@ -35,7 +36,7 @@ def client():
from app.main import app
init_db()
yield TestClient(app)
yield login_test_client(TestClient(app))
os.unlink(db_path)
@@ -142,8 +143,8 @@ def test_auto_properties_filled_on_create_and_update(client):
pv = json.loads(client.get(f"/db/pages/{page_id}/api").json()["property_values_json"])
assert pv[str(ct)]
assert pv[str(lt)]
assert pv[str(cb)]["login"] == "admin"
assert pv[str(lb)]["login"] == "admin"
assert pv[str(cb)]["login"] == "tester"
assert pv[str(lb)]["login"] == "tester"
created = pv[str(ct)]
# Partial update keeps created_time and refreshes last_edited_time.
+3 -1
View File
@@ -14,6 +14,7 @@ import secrets
import tempfile
import pytest
from conftest import anon, login_test_client
from fastapi.testclient import TestClient
@@ -37,7 +38,7 @@ def client():
from app.main import app
init_db()
yield TestClient(app)
yield login_test_client(TestClient(app))
try:
os.unlink(db_path)
@@ -265,6 +266,7 @@ def test_user_timezone_endpoints(client):
def test_notifications_unauth(client):
anon(client)
assert client.get("/api/notifications/timezone").status_code == 401
+3
View File
@@ -5,6 +5,8 @@ restricted, private), explicit grants (user + group), inheritance, property
visibility, user groups and the audit log.
"""
from conftest import anon
from app.auth.session import SessionManager
# ═══════════════ helpers ═══════════════
@@ -516,6 +518,7 @@ def test_audit_log_records_changes(client):
def test_permissions_endpoints_require_auth(client):
anon(client)
assert client.get("/api/v2/pages/1/permissions").status_code == 401
assert client.post("/api/v2/pages/1/permissions", json={"user_id": 2, "role": "viewer"}).status_code == 401
assert client.get("/api/v2/groups").status_code == 401
+2 -1
View File
@@ -11,6 +11,7 @@ import os
import tempfile
import pytest
from conftest import login_test_client
from app.services import skill_gallery
from app.services.webhook_outbound import EVENTS
@@ -50,7 +51,7 @@ def client():
conn.commit()
from fastapi.testclient import TestClient
yield TestClient(app)
yield login_test_client(TestClient(app))
try:
os.unlink(db_path)
+3 -1
View File
@@ -17,6 +17,7 @@ import secrets as pysecrets
from urllib.parse import parse_qs, urlparse
import pytest
from conftest import anon
# ── Mock IdP constants ─────────────────────────────────────────────────────
IDP_ENTITY = "https://idp.corp.test/saml/metadata"
@@ -289,6 +290,7 @@ def test_providers_endpoint_empty_without_config(client):
def test_config_requires_admin(client):
anon(client)
assert client.get("/api/v2/sso/config").status_code == 401
_admin_session(client)
# demote to plain user → 403
@@ -1284,7 +1286,7 @@ def test_history_endpoint_requires_admin_and_lists_attempts(client, idp_keypair)
assert failures[0]["error_message"]
# anonymous → 401
client.cookies.delete("flowdeck_session")
anon(client)
assert client.get("/api/v2/sso/history").status_code == 401
+3
View File
@@ -9,6 +9,8 @@ from __future__ import annotations
import json
import secrets
from conftest import anon
from app.db import get_conn
@@ -126,6 +128,7 @@ def test_site_slug_validation_and_conflict(client):
def test_site_requires_auth(client):
anon(client)
pid = _make_page("Root")
r = client.post("/api/v2/sites", json={"root_page_id": pid})
assert r.status_code == 401
+4
View File
@@ -12,6 +12,8 @@ import math
import secrets
import struct
from conftest import anon
from app.db import get_conn
from app.services import semantic_search as sem
@@ -188,6 +190,7 @@ def test_hybrid_finds_by_keyword(client):
def test_hybrid_requires_auth(client):
anon(client)
r = client.get("/api/v2/search/hybrid?q=test")
assert r.status_code == 401
@@ -265,6 +268,7 @@ def test_ask_offline_with_citations(client):
def test_ask_requires_auth(client):
anon(client)
r = client.post("/api/v2/search/ask", json={"question": "hi"})
assert r.status_code == 401
+3
View File
@@ -10,6 +10,7 @@ from __future__ import annotations
import secrets
import pytest
from conftest import anon
from app.db import get_conn
from app.services import automations as auto_svc
@@ -113,6 +114,7 @@ def test_steps_crud_and_order(client):
def test_steps_validation_and_auth(client):
anon(client)
session, _ = _login(client)
aid = _mkauto(client, session)
r = client.post(f"/workspace/automations/{aid}/steps",
@@ -423,6 +425,7 @@ def _mkworker(client, session, **kw):
def test_workers_crud_and_auth(client):
anon(client)
session, _ = _login(client)
w = _mkworker(client, session, name="Hello")
assert w["slug"].startswith("hello") or w["slug"]
+3
View File
@@ -11,6 +11,7 @@ import json
import secrets
import pytest
from conftest import anon
from app.db import get_conn
from app.services import calendar_sync as cal
@@ -117,6 +118,7 @@ def test_link_crud_and_encryption(client):
def test_link_validation_and_auth(client):
anon(client)
session, _ = _login(client)
cid = _mkcollection(client)
r = client.post("/api/v2/calendar-links",
@@ -279,6 +281,7 @@ def test_freebusy_basic(client):
def test_freebusy_validation(client):
anon(client)
session, _ = _login(client)
cid = _mkcollection(client)
r = client.get(f"/db/{cid}/calendar/freebusy?from=2026-10-07&to=2026-10-01",
+6
View File
@@ -10,6 +10,8 @@ from __future__ import annotations
import json
import secrets
from conftest import anon
from app.db import get_conn
# ── helpers ────────────────────────────────────────────────────────────────
@@ -289,6 +291,7 @@ def test_2fa_disable(client):
def test_2fa_routes_require_session(client):
anon(client)
assert client.get("/auth/2fa/status").status_code == 401
assert client.post("/auth/2fa/setup").status_code == 401
@@ -362,6 +365,7 @@ def test_webauthn_register_begin(client):
def test_webauthn_register_begin_requires_session(client):
anon(client)
assert client.post("/auth/webauthn/register/begin").status_code == 401
@@ -411,6 +415,7 @@ def test_webauthn_keys_empty_and_delete_404(client):
# ── unified audit log ──────────────────────────────────────────────────────
def test_audit_requires_admin(client):
anon(client)
uid, login = _make_user()
c = {"flowdeck_session": _session(uid, login)}
assert client.get("/api/v2/audit/logs", cookies=c).status_code == 403
@@ -608,5 +613,6 @@ def test_approval_rejection(client):
def test_governance_routes_require_auth(client):
anon(client)
assert client.get("/api/v2/agent-policies").status_code == 401
assert client.get("/api/v2/agent-approvals").status_code == 401
+5
View File
@@ -10,6 +10,8 @@ from __future__ import annotations
import datetime
import secrets
from conftest import anon
from app.db import get_conn
# ── helpers ────────────────────────────────────────────────────────────────
@@ -133,6 +135,7 @@ def test_teamspace_requires_name(client):
def test_teamspace_requires_auth(client):
anon(client)
assert client.get("/api/v2/wiki/teamspaces?workspace_id=1").status_code == 401
assert client.post("/api/v2/wiki/teamspaces", json={}).status_code == 401
@@ -548,6 +551,7 @@ def test_guest_share_bad_role(client):
def test_guest_share_requires_session(client):
anon(client)
assert client.post("/api/v2/wiki/pages/1/guests", json={}).status_code == 401
@@ -749,6 +753,7 @@ def test_blocks_preview_endpoint(client):
def test_blocks_preview_validation(client):
anon(client)
_, _, c = _user()
assert client.post("/api/v2/wiki/blocks/preview", cookies=c, json={}).status_code == 400
assert client.post("/api/v2/wiki/blocks/preview", cookies=c,
+5 -2
View File
@@ -3,6 +3,8 @@ from __future__ import annotations
import json
from conftest import anon
from app.auth.session import SessionManager
@@ -81,6 +83,7 @@ def test_extract_article(client):
# ── API tests ──
def test_clip_requires_auth(client):
anon(client)
r = client.post("/api/v2/web-clipper/clip", json={"url": "https://example.com", "title": "T"})
assert r.status_code == 401
@@ -225,7 +228,7 @@ def test_clip_bearer_token_auth(client):
conn.execute("INSERT INTO api_tokens (user_id, name, token_hash, token_prefix) VALUES (?,?,?,?)", (uid, "test", th, token[:12]))
conn.commit()
# No session cookie
client.cookies.clear()
anon(client)
r = client.post(
"/api/v2/web-clipper/clip",
json={"url": "https://example.com/bearer", "title": "BearerClip", "content": "<p>via bearer</p>", "device_id": "bearer-dev"},
@@ -241,7 +244,7 @@ def test_status_and_devices_flow(client):
uid = _insert_user("clip_status")
_insert_workspace(uid)
# unauth status
client.cookies.clear()
anon(client)
r = client.get("/api/v2/web-clipper/status")
assert r.status_code == 200
assert r.json()["authenticated"] is False