/** * Tests for the web auth token flow (web/lib/auth.ts). * * The auth module runs in the browser, so we verify the source code contains * the expected patterns for token extraction, persistence, and transmission. */ import test from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { join } from 'node:path' const projectRoot = process.cwd() // ─── Source contract tests ────────────────────────────────────────────────── const authSource = readFileSync(join(projectRoot, 'web', 'lib', 'auth.ts'), 'utf-8') test('auth.ts persists token to sessionStorage on extraction', () => { assert.match(authSource, /sessionStorage\.setItem/, 'should persist token to sessionStorage after extracting from hash') }) test('auth.ts falls back to sessionStorage when hash is absent', () => { assert.match(authSource, /sessionStorage\.getItem/, 'should read from sessionStorage when URL hash is empty') }) test('auth.ts defines a sessionStorage key constant', () => { assert.match(authSource, /SESSION_STORAGE_KEY/, 'should use a named constant for the sessionStorage key') }) test('auth.ts clears the URL fragment after token extraction', () => { assert.match(authSource, /replaceState/, 'should clear the hash from the address bar') }) test('auth.ts wraps sessionStorage calls in try/catch for private browsing', () => { // sessionStorage can throw in private browsing when quota is exceeded const setItemIndex = authSource.indexOf('sessionStorage.setItem') const getItemIndex = authSource.indexOf('sessionStorage.getItem') assert.ok(setItemIndex > -1) assert.ok(getItemIndex > -1) // Both sessionStorage accesses should be inside try blocks const beforeSetItem = authSource.slice(Math.max(0, setItemIndex - 200), setItemIndex) const beforeGetItem = authSource.slice(Math.max(0, getItemIndex - 200), getItemIndex) assert.match(beforeSetItem, /try\s*\{/, 'sessionStorage.setItem should be inside a try block') assert.match(beforeGetItem, /try\s*\{/, 'sessionStorage.getItem should be inside a try block') }) // ─── sendBeacon auth token tests ──────────────────────────────────────────── const appShellSource = readFileSync(join(projectRoot, 'web', 'components', 'gsd', 'app-shell.tsx'), 'utf-8') test('app-shell.tsx sendBeacon includes auth token as query parameter', () => { // sendBeacon cannot set custom headers, so the token must be passed // as a _token query parameter for the proxy to accept the request. assert.match(appShellSource, /_token=/, 'sendBeacon URL should include _token query parameter') }) test('app-shell.tsx sendBeacon does not send bare unauthenticated URL', () => { // Every sendBeacon to /api/ should include the auth token const beaconCalls = appShellSource.match(/sendBeacon\([^)]+\)/g) || [] for (const call of beaconCalls) { if (call.includes('/api/')) { // The URL should be constructed with the token, not a bare string literal assert.ok( !call.includes('"/api/shutdown"') && !call.includes("'/api/shutdown'"), `sendBeacon call should not use a bare /api/ URL without auth: ${call}` ) } } }) // ─── proxy.ts contract tests ──────────────────────────────────────────────── const proxySource = readFileSync(join(projectRoot, 'web', 'proxy.ts'), 'utf-8') test('proxy.ts accepts _token query parameter as fallback authentication', () => { assert.match(proxySource, /_token/, 'proxy should support _token query parameter for SSE/sendBeacon') }) test('proxy.ts validates bearer token from Authorization header', () => { assert.match(proxySource, /Bearer/, 'proxy should check Authorization: Bearer header') }) test('proxy.ts skips auth when GSD_WEB_AUTH_TOKEN is not set', () => { assert.match(proxySource, /GSD_WEB_AUTH_TOKEN/, 'proxy should read GSD_WEB_AUTH_TOKEN from env') assert.match(proxySource, /NextResponse\.next\(\)/, 'proxy should pass through when no token is configured') })