import json import subprocess from pathlib import Path import pytest from src.views import dashboard ROOT = Path(__file__).resolve().parents[1] SESSION_JS = ROOT / "frontend" / "session.js" def run_session_scenario(scenario: str) -> dict: harness = f""" const createSessionBoundary = require({json.dumps(str(SESSION_JS))}); const state = {{ requests: [], removed: [], deletedDatabases: [], deletedCaches: [], assigned: '', workerMessages: [] }}; const storage = {{ values: new Map([['stackchain.private', 'secret'], ['gitea.preference', 'keep']]), get length() {{ return this.values.size; }}, key(index) {{ return Array.from(this.values.keys())[index] || null; }}, removeItem(key) {{ state.removed.push(key); this.values.delete(key); }}, }}; const boundary = createSessionBoundary({{ cookie: () => 'other=x; stackchain_csrf=csrf-proof; theme=dark', origin: 'https://forge.example', base: '/dashboard/', fetchImpl: async (url, options = {{}}) => {{ state.requests.push({{ url: String(url), method: options.method || 'GET', headers: Object.fromEntries(new Headers(options.headers || {{}})) }}); return new Response('{{}}', {{ status: 200, headers: {{ 'Content-Type': 'application/json' }} }}); }}, localStorage: storage, sessionStorage: storage, indexedDB: {{ deleteDatabase: name => {{ state.deletedDatabases.push(name); return {{ onsuccess: null, onerror: null, onblocked: null }}; }} }}, caches: {{ keys: async () => ['stackchain-dashboard-shell-v15', 'gitea-assets'], delete: async key => {{ state.deletedCaches.push(key); }} }}, serviceWorker: {{ ready: Promise.resolve({{ active: {{ postMessage: message => state.workerMessages.push(message) }} }}) }}, location: {{ assign: value => {{ state.assigned = value; }} }}, }}); (async () => {{ {scenario} }})().catch(error => {{ console.error(error); process.exit(1); }}); """ result = subprocess.run(["node", "-e", harness], text=True, capture_output=True, check=True) return json.loads(result.stdout) def test_mutating_same_origin_fetch_receives_csrf_proof(): result = run_session_scenario( """ await boundary.fetch('/dashboard/api/v1/notifications/7/read', { method: 'PATCH' }); process.stdout.write(JSON.stringify(state)); """ ) assert result["requests"][0]["headers"]["x-csrf-token"] == "csrf-proof" def test_sign_out_clears_only_dashboard_private_device_state(): result = run_session_scenario( """ await boundary.signOut(); state.remaining = Array.from(storage.values.keys()); process.stdout.write(JSON.stringify(state)); """ ) request = result["requests"][0] assert request["url"] == "/dashboard/api/v1/session" assert request["method"] == "DELETE" assert request["headers"]["x-csrf-token"] == "csrf-proof" assert "stackchain.private" in result["removed"] assert result["remaining"] == ["gitea.preference"] assert result["deletedDatabases"] == ["stackchain-background-outbox-v1"] assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"] assert result["assigned"] == "/dashboard/login" def test_authenticated_dashboard_load_requests_queued_delivery_resume(): result = run_session_scenario( """ await boundary.resumeQueuedWork(); process.stdout.write(JSON.stringify(state)); """ ) assert result["workerMessages"] == [{"type": "stackchain-resume-outbox"}] @pytest.mark.anyio async def test_dashboard_loads_session_boundary_first_and_offers_sign_out(): html = await dashboard() assert '' in html assert html.index('static/session.js') < html.index('static/markdown.js') assert '' in html