const WebSocket = require('ws'); const request = require('supertest'); const { app, server } = require('../../backend/server'); const { getAuthToken, getAuthHeader, cleanDb, seedGame, seedSession, seedSessionGame } = 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); } }); }); } beforeAll((done) => { server.listen(0, () => done()); }); afterAll((done) => { server.close(done); }); describe('vote.received WebSocket event', () => { const baseTime = '2026-03-15T20:00:00.000Z'; beforeEach(() => { cleanDb(); }); test('broadcasts vote.received to session subscribers on live vote', async () => { const game = seedGame({ title: 'Quiplash 3', pack_name: 'Party Pack 7' }); const session = seedSession({ is_active: 1 }); seedSessionGame(session.id, game.id, { status: 'playing', played_at: baseTime }); 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 })); await waitForMessage(ws, 'subscribed'); const eventPromise = waitForMessage(ws, 'vote.received'); await request(app) .post('/api/votes/live') .set('Authorization', getAuthHeader()) .set('Content-Type', 'application/json') .send({ username: 'viewer1', vote: 'up', timestamp: '2026-03-15T20:05:00.000Z', }); const event = await eventPromise; expect(event.data.sessionId).toBe(session.id); expect(event.data.game).toEqual({ id: game.id, title: 'Quiplash 3', pack_name: 'Party Pack 7', }); expect(event.data.vote).toEqual({ username: 'viewer1', type: 'up', timestamp: '2026-03-15T20:05:00.000Z', }); expect(event.data.totals).toEqual({ upvotes: 1, downvotes: 0, popularity_score: 1, }); ws.close(); }); test('does not broadcast on duplicate vote (409)', async () => { const game = seedGame({ title: 'Quiplash 3', pack_name: 'Party Pack 7' }); const session = seedSession({ is_active: 1 }); seedSessionGame(session.id, game.id, { status: 'playing', played_at: baseTime }); 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 })); await waitForMessage(ws, 'subscribed'); // First vote succeeds - set up listener before POST to catch the event const firstEventPromise = waitForMessage(ws, 'vote.received'); await request(app) .post('/api/votes/live') .set('Authorization', getAuthHeader()) .set('Content-Type', 'application/json') .send({ username: 'viewer1', vote: 'up', timestamp: '2026-03-15T20:05:00.000Z', }); await firstEventPromise; // Duplicate vote (within 1 second) const dupRes = await request(app) .post('/api/votes/live') .set('Authorization', getAuthHeader()) .set('Content-Type', 'application/json') .send({ username: 'viewer1', vote: 'down', timestamp: '2026-03-15T20:05:00.500Z', }); expect(dupRes.status).toBe(409); // Verify no vote.received event comes (wait briefly) const noEvent = await Promise.race([ waitForMessage(ws, 'vote.received', 500).then(() => 'received').catch(() => 'timeout'), new Promise((resolve) => setTimeout(() => resolve('timeout'), 500)), ]); expect(noEvent).toBe('timeout'); ws.close(); }); test('does not broadcast when no active session (404)', 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 res = await request(app) .post('/api/votes/live') .set('Authorization', getAuthHeader()) .set('Content-Type', 'application/json') .send({ username: 'viewer1', vote: 'up', timestamp: '2026-03-15T20:05:00.000Z', }); expect(res.status).toBe(404); const noEvent = await Promise.race([ waitForMessage(ws, 'vote.received', 500).then(() => 'received').catch(() => 'timeout'), new Promise((resolve) => setTimeout(() => resolve('timeout'), 500)), ]); expect(noEvent).toBe('timeout'); ws.close(); }); });