Files
jackboxpartypack-gamepicker/tests/api/votes-live-websocket.test.js

167 lines
5.0 KiB
JavaScript
Raw Normal View History

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();
});
});