diff --git a/tests/api/regression-websocket.test.js b/tests/api/regression-websocket.test.js new file mode 100644 index 0000000..1150201 --- /dev/null +++ b/tests/api/regression-websocket.test.js @@ -0,0 +1,153 @@ +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(); + }); +});