v1.1.0: session guard on all game routes + register + sign-in screen (ISS-107)
CI / build + unit + e2e (pull_request) Failing after 26m20s
CI / build + unit + e2e (pull_request) Failing after 26m20s
This commit is contained in:
@@ -2,9 +2,12 @@ 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
|
||||
@@ -12,6 +15,8 @@ beforeAll(async () => {
|
||||
// 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 () => {
|
||||
@@ -20,8 +25,18 @@ afterAll(async () => {
|
||||
});
|
||||
|
||||
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();
|
||||
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');
|
||||
|
||||
+10
-2
@@ -1,3 +1,4 @@
|
||||
import { requireAuthSse } from '@/lib/auth';
|
||||
import { getStateSnapshot, sseFrame } from '@/lib/game/state';
|
||||
import { subscribe } from '@/lib/sse';
|
||||
|
||||
@@ -5,12 +6,19 @@ import { subscribe } from '@/lib/sse';
|
||||
// then one after every sim tick (broadcast from lib/sim/engine.ts). One-way push; player actions
|
||||
// stay REST POSTs. Heartbeats keep idle proxies from dropping the connection. ISS-133 (v0.59.0):
|
||||
// frames carry the sseFrame projection (terminal package history dropped) — same shape as the
|
||||
// pushed frames, so a connect never costs one full-history frame.
|
||||
// pushed frames, so a connect never costs one full-history frame. v1.1.0 (ISS-107): guarded —
|
||||
// EventSource cannot set headers, so the session comes from X-Flight-Session OR ?session=
|
||||
// (requireAuthSse). ponytail: validated at CONNECT only — a session deleted/expired mid-stream keeps
|
||||
// receiving pushes until it reconnects (private infra, 7-day sessions; per-push re-validation would
|
||||
// be one indexed lookup per tick if that ever matters).
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const HEARTBEAT_MS = 15_000;
|
||||
|
||||
export async function GET() {
|
||||
export async function GET(req: Request) {
|
||||
const auth = await requireAuthSse(req);
|
||||
if (!auth.ok) return auth.response;
|
||||
|
||||
let heartbeat: ReturnType<typeof setInterval> | null = null;
|
||||
let unsubscribe: (() => void) | null = null;
|
||||
const cleanup = () => {
|
||||
|
||||
Reference in New Issue
Block a user