const WebSocket = require('ws'); const request = require('supertest'); const { app, server } = require('../../backend/server'); const { getAuthToken, getAuthHeader, cleanDb, seedGame, seedSession } = require('../helpers/test-utils'); function connectWs() { return new WebSocket(`ws://localhost:${server.address().port}/api/sessions/live`); } function waitForMessage(ws, type, timeoutMs = 3000) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error(`Timeout waiting for ${type}`)), timeoutMs); ws.on('message', function handler(data) { const msg = JSON.parse(data.toString()); if (msg.type === type) { clearTimeout(timeout); ws.removeListener('message', handler); resolve(msg); } }); }); } function authenticateAndSubscribe(ws, sessionId) { return new Promise(async (resolve, reject) => { try { ws.send(JSON.stringify({ type: 'auth', token: getAuthToken() })); await waitForMessage(ws, 'auth_success'); ws.send(JSON.stringify({ type: 'subscribe', sessionId })); await waitForMessage(ws, 'subscribed'); resolve(); } catch (err) { reject(err); } }); } beforeAll((done) => { server.listen(0, () => done()); }); afterAll((done) => { server.close(done); }); describe('WebSocket events (regression)', () => { beforeEach(() => { cleanDb(); }); test('auth flow: auth -> auth_success', (done) => { const ws = connectWs(); ws.on('open', () => { ws.send(JSON.stringify({ type: 'auth', token: getAuthToken() })); }); ws.on('message', (data) => { const msg = JSON.parse(data.toString()); if (msg.type === 'auth_success') { ws.close(); done(); } }); }); test('subscribe/unsubscribe flow', async () => { const session = seedSession({ is_active: 1 }); const ws = connectWs(); await new Promise((resolve) => ws.on('open', resolve)); ws.send(JSON.stringify({ type: 'auth', token: getAuthToken() })); await waitForMessage(ws, 'auth_success'); ws.send(JSON.stringify({ type: 'subscribe', sessionId: session.id })); const subMsg = await waitForMessage(ws, 'subscribed'); expect(subMsg.sessionId).toBe(session.id); ws.send(JSON.stringify({ type: 'unsubscribe', sessionId: session.id })); const unsubMsg = await waitForMessage(ws, 'unsubscribed'); expect(unsubMsg.sessionId).toBe(session.id); ws.close(); }); test('session.started broadcasts to all authenticated clients', async () => { const ws = connectWs(); await new Promise((resolve) => ws.on('open', resolve)); ws.send(JSON.stringify({ type: 'auth', token: getAuthToken() })); await waitForMessage(ws, 'auth_success'); const eventPromise = waitForMessage(ws, 'session.started'); await request(app) .post('/api/sessions') .set('Authorization', getAuthHeader()) .set('Content-Type', 'application/json') .send({ notes: 'Test session' }); const event = await eventPromise; expect(event.data.session).toEqual( expect.objectContaining({ is_active: 1, notes: 'Test session', }) ); ws.close(); }); test('session.ended broadcasts to session subscribers', async () => { const session = seedSession({ is_active: 1 }); const ws = connectWs(); await new Promise((resolve) => ws.on('open', resolve)); await authenticateAndSubscribe(ws, session.id); const eventPromise = waitForMessage(ws, 'session.ended'); await request(app) .post(`/api/sessions/${session.id}/close`) .set('Authorization', getAuthHeader()) .set('Content-Type', 'application/json'); const event = await eventPromise; expect(event.data.session.id).toBe(session.id); expect(event.data.session.is_active).toBe(0); ws.close(); }); test('game.added broadcasts to session subscribers', async () => { const game = seedGame({ title: 'Quiplash 3', pack_name: 'Party Pack 7' }); const session = seedSession({ is_active: 1 }); const ws = connectWs(); await new Promise((resolve) => ws.on('open', resolve)); await authenticateAndSubscribe(ws, session.id); const eventPromise = waitForMessage(ws, 'game.added'); await request(app) .post(`/api/sessions/${session.id}/games`) .set('Authorization', getAuthHeader()) .set('Content-Type', 'application/json') .send({ game_id: game.id }); const event = await eventPromise; expect(event.data.game.title).toBe('Quiplash 3'); expect(event.data.session.id).toBe(session.id); ws.close(); }); });