import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import type { BrowserContext } from 'playwright'; import { afterEach, describe, expect, it } from 'vitest'; import type { BrowserClient } from '../src/browser/client.js'; import { ActivityLogRepository } from '../src/database/repositories.js'; import { WebServer } from '../src/web/server.js'; import { createMockLogger } from './mockLogger.js'; import { createTestDb, createTestRepositories, getTestConfig } from './testUtils.js'; const AUTH_TOKEN = 'create01'; interface RunningServer { baseUrl: string; server: WebServer; workspace: string; } function createWorkspace(includeReactBuild = true): string { const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'redditbot-web-')); fs.mkdirSync(path.join(workspace, 'public'), { recursive: true }); fs.writeFileSync(path.join(workspace, 'public', 'index.html'), '

Legacy dashboard

'); fs.mkdirSync(path.join(workspace, 'data', 'media'), { recursive: true }); fs.writeFileSync(path.join(workspace, 'data', 'media', 'sample.txt'), 'private media'); if (includeReactBuild) { fs.mkdirSync(path.join(workspace, 'apps', 'dashboard', 'dist', 'assets'), { recursive: true }); fs.writeFileSync(path.join(workspace, 'apps', 'dashboard', 'dist', 'index.html'), '

React dashboard

'); fs.writeFileSync(path.join(workspace, 'apps', 'dashboard', 'dist', 'assets', 'app.js'), 'react-build'); } return workspace; } async function startServer(options: { debugEnabled?: boolean; includeReactBuild?: boolean } = {}): Promise { const database = createTestDb(); const repositories = createTestRepositories(database); const workspace = createWorkspace(options.includeReactBuild); const server = new WebServer( 0, getTestConfig({ web: { enabled: true, port: 0, authToken: AUTH_TOKEN, debugEnabled: options.debugEnabled ?? false } }), repositories.postsRepo, repositories.messagesRepo, repositories.successfulMessagesRepo, new ActivityLogRepository(database), createMockLogger(), undefined, workspace, ); const port = await server.start(); return { baseUrl: `http://127.0.0.1:${port}`, server, workspace }; } describe('authenticated React dashboard cutover', () => { const runningServers: RunningServer[] = []; afterEach(() => { for (const running of runningServers) { running.server.stop(); fs.rmSync(running.workspace, { recursive: true, force: true }); } runningServers.length = 0; }); it('protects API, SSE, media, React assets, and SPA navigation', async () => { const running = await startServer(); runningServers.push(running); const anonymous = await Promise.all([ fetch(`${running.baseUrl}/api/stats`), fetch(`${running.baseUrl}/api/events/stream`), fetch(`${running.baseUrl}/media/sample.txt`), fetch(running.baseUrl, { redirect: 'manual' }), ]); expect(anonymous.map((response) => response.status)).toEqual([401, 401, 401, 302]); const headers = { Authorization: `Bearer ${AUTH_TOKEN}` }; const api = await fetch(`${running.baseUrl}/api/stats`, { headers }); const media = await fetch(`${running.baseUrl}/media/sample.txt`, { headers }); const root = await fetch(running.baseUrl, { headers }); const asset = await fetch(`${running.baseUrl}/assets/app.js`, { headers }); const spa = await fetch(`${running.baseUrl}/conversations/example`, { headers: { ...headers, Accept: 'text/html' } }); expect(api.status).toBe(200); expect(await media.text()).toBe('private media'); expect(await root.text()).toContain('React dashboard'); expect(await asset.text()).toBe('react-build'); expect(await spa.text()).toContain('React dashboard'); }); it('creates a secure session and keeps debug credentials unavailable', async () => { const running = await startServer({ debugEnabled: true }); runningServers.push(running); const accepted = await fetch(`${running.baseUrl}/auth/session`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Forwarded-Proto': 'https' }, body: JSON.stringify({ token: AUTH_TOKEN }), }); const setCookie = accepted.headers.get('set-cookie'); expect(accepted.status).toBe(204); expect(setCookie).toContain('HttpOnly'); expect(setCookie).toContain('SameSite=Strict'); expect(setCookie).toContain('Secure'); const browserContext = { cookies: async () => [{ name: 'reddit_session', value: 'never-return-this-secret', domain: '.reddit.com', path: '/', expires: -1, httpOnly: true, secure: true, sameSite: 'Lax' as const }] } as unknown as BrowserContext; running.server.setBrowserClient({ getContext: () => browserContext } as unknown as BrowserClient); const headers = { Authorization: `Bearer ${AUTH_TOKEN}` }; const cookies = await fetch(`${running.baseUrl}/api/debug/cookies?reveal=1`, { headers }); const upload = await fetch(`${running.baseUrl}/api/debug/matrix-credentials`, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ homeserver: 'https://matrix.example', accessToken: 'secret' }) }); expect(await cookies.text()).not.toContain('never-return-this-secret'); expect(upload.status).toBe(404); }); });