import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { createSession } from '@/lib/auth'; import { getGameDb } from '@/lib/db/client'; import { GET } from './route'; let dir: string; let sessionId: string; beforeAll(async () => { // Same pattern as app/api/state/route.test.ts: point the DB singleton at a throwaway file so // this test never touches ./db/flight.db. getGameDb() reads the env var on first call, and // vitest gives each test file its own module registry (fresh singleton). dir = await fs.mkdtemp(path.join(os.tmpdir(), 'flight-events-')); process.env.FLIGHT_DB_PATH = path.join(dir, 'events.db'); // v1.1.0 (ISS-107): /api/events is session-guarded — mint a session for the seeded admin user. sessionId = await createSession(await getGameDb(), 'admin-kamilkrzywda'); }); afterAll(async () => { delete process.env.FLIGHT_DB_PATH; await fs.rm(dir, { recursive: true, force: true }); }); describe('GET /api/events', () => { // v1.1.0 (ISS-107): EventSource cannot set headers, so the client passes the session as a query // param — this test exercises exactly that path (requireAuthSse). const req = () => new Request(`http://localhost/api/events?session=${sessionId}`); it('rejects an unauthenticated request with 401', async () => { const res = await GET(new Request('http://localhost/api/events')); expect(res.status).toBe(401); expect(((await res.json()) as { error: string }).error).toMatch(/authentication required/i); }); it('streams SSE with the current snapshot as the first event', async () => { const res = await GET(req()); expect(res.status).toBe(200); expect(res.headers.get('content-type')).toContain('text/event-stream'); expect(res.headers.get('cache-control')).toBe('no-cache'); expect(res.headers.get('x-accel-buffering')).toBe('no'); const reader = (res.body as ReadableStream).getReader(); try { const { value } = await reader.read(); // first chunk: the initial snapshot event const text = new TextDecoder().decode(value); expect(text.startsWith('data: ')).toBe(true); const snap = JSON.parse(text.slice('data: '.length)) as Record; expect(typeof (snap.simState as { clockMs: number }).clockMs).toBe('number'); } finally { await reader.cancel(); // → stream cancel → unsubscribe from the registry } }); });