"""Regression tests for the global CSRF interceptor (F.7c).
Background: previously, dashboard pages defined a local ``getCsrf()`` JS helper
that read the dashboard CSRF cookie via ``document.cookie``. The cookie is
``HttpOnly``, so JS could never read it and every non-GET fetch sent an empty
``X-CSRF-Token`` header — leading to 403 errors when toggling tools or
changing the access scope.
The fix moved CSRF handling into a single global interceptor in
``head_assets.html`` that reads from a ```` tag rendered
server-side in ``base.html``. This file pins down the contract so the bug
cannot silently regress:
1. Every dashboard page renders a non-empty ```` tag.
2. The global ``htmx:configRequest`` + ``window.fetch`` interceptor is
present in ``head_assets.html``.
3. No dashboard template defines its own ``getCsrf()`` helper or sets an
explicit ``x-csrf-token`` header from JS — those would short-circuit the
global interceptor again.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from starlette.testclient import TestClient
import core.dashboard.routes as routes_module
import core.database as db_module
from core.database import Database
TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "core" / "templates"
DASHBOARD_DIR = TEMPLATES_DIR / "dashboard"
# ── Static template checks ──────────────────────────────────────
class TestStaticTemplateContract:
"""Lint-style checks that don't require a running app."""
def test_head_assets_has_global_csrf_interceptor(self):
head = (DASHBOARD_DIR / "partials" / "head_assets.html").read_text()
# The HTMX hook
assert "htmx:configRequest" in head, "missing global HTMX CSRF hook"
# The fetch monkey-patch
assert "window.fetch" in head, "missing global fetch CSRF hook"
# Both must read from the meta tag — not the cookie
assert 'meta[name="csrf-token"]' in head, "interceptor must read from meta tag"
# Sanity: must set the X-CSRF-Token header
assert "X-CSRF-Token" in head
def test_base_template_renders_csrf_meta_tag(self):
base = (DASHBOARD_DIR / "base.html").read_text()
assert '", 1)[0]