/** * Session-init + WebSocket transport tests. * * session-init returns the `{ csrfToken }` no-store shape at parity with the * Django/FastAPI/Axum session endpoint. The WebSocket transport drives the * SAME dispatch core the HTTP path uses, so a function exposed over WS behaves * identically — invalidation, auth, and not-found all carry through. */ import { describe, test, expect, beforeEach } from 'bun:test' import { ReactContext, client, clearRegistry, handleSessionInit, sessionInitRoute, SESSION_INIT_PATH, handleWebSocketMessage, serveWebSocket, type Identity, type WebSocketLike, } from '../src' describe('session-init', () => { test('returns { csrfToken: null } with no-store', () => { const r = handleSessionInit() expect(r.status).toBe(200) expect(r.body).toEqual({ csrfToken: null }) expect(r.headers['Cache-Control']).toBe('no-store') expect(r.headers['Content-Type']).toBe('application/json') }) test('embeds a host-provided CSRF token', () => { const r = handleSessionInit('tok-123') expect(r.body).toEqual({ csrfToken: 'tok-123' }) }) test('route descriptor mounts GET /session/ (parity with Django/FastAPI/Axum)', () => { expect(SESSION_INIT_PATH).toBe('/session/') expect(sessionInitRoute.path).toBe('/session/') expect(sessionInitRoute.method).toBe('GET') // The wired handler returns the session shape. expect(sessionInitRoute.handler().body).toEqual({ csrfToken: null }) }) }) describe('WebSocket transport', () => { beforeEach(() => clearRegistry()) const UserCtx = new ReactContext('user') function setup() { client({ context: UserCtx, websocket: true }, async function user_profile(user_id: number) { return { user_id, name: `user_${user_id}` } }) client({ affects: UserCtx, websocket: true }, async function update_profile(user_id: number, name: string) { return { ok: true, user_id, name } }) } test('call frame routes through mutation dispatch + carries invalidation', async () => { setup() const reply = await handleWebSocketMessage({ id: 1, type: 'call', fn: 'update_profile', args: { user_id: 5, name: 'X' }, }) expect(reply.id).toBe(1) expect(reply.result).toEqual({ ok: true, user_id: 5, name: 'X' }) expect(reply.invalidate).toBeDefined() expect(reply.invalidate[0].context).toBe('user') expect(reply.invalidate[0].params.user_id).toBe(5) }) test('fetch frame routes through context bundle', async () => { setup() const reply = await handleWebSocketMessage({ id: 2, type: 'fetch', context: 'user', params: { user_id: '7' }, }) expect(reply.id).toBe(2) expect(reply.result.user_profile).toEqual({ user_id: '7', name: 'user_7' }) }) test('unknown function returns an error frame, not a throw', async () => { const reply = await handleWebSocketMessage({ id: 3, type: 'call', fn: 'nope' }) expect(reply.error).toBeDefined() expect(reply.error!.code).toBe('NOT_FOUND') expect(reply.id).toBe(3) }) test('auth enforcement carries over the WS transport', async () => { client({ auth: true, websocket: true }, async function secret() { return { ok: true } }) const anon: Identity = { isAuthenticated: false, isStaff: false, isSuperuser: false, id: null } const reply = await handleWebSocketMessage({ id: 4, type: 'call', fn: 'secret' }, anon) expect(reply.error!.code).toBe('UNAUTHORIZED') }) test('malformed JSON frame → error', async () => { const reply = await handleWebSocketMessage('{not json') expect(reply.error!.code).toBe('BAD_REQUEST') }) test('serveWebSocket wires a connection and replies as JSON', async () => { setup() const sent: string[] = [] let listener: ((e: { data: any }) => void) | null = null const ws: WebSocketLike = { send: (d) => sent.push(d), addEventListener: (_t, l) => { listener = l }, } serveWebSocket(ws) expect(listener).not.toBeNull() // Drive a message through the wired listener. await listener!({ data: JSON.stringify({ id: 9, type: 'fetch', context: 'user', params: { user_id: '3' } }) }) // Give the async handler a tick to resolve + send. await new Promise((r) => setTimeout(r, 0)) expect(sent.length).toBe(1) const reply = JSON.parse(sent[0]) expect(reply.id).toBe(9) expect(reply.result.user_profile.name).toBe('user_3') }) })