const request = require('supertest'); const { app } = require('../../backend/server'); const { getAuthHeader, cleanDb, seedGame, seedSession, seedSessionGame } = require('../helpers/test-utils'); describe('POST /api/votes/live (regression)', () => { let game, session, sessionGame; const baseTime = '2026-03-15T20:00:00.000Z'; beforeEach(() => { cleanDb(); game = seedGame({ title: 'Quiplash 3', pack_name: 'Party Pack 7' }); session = seedSession({ is_active: 1 }); sessionGame = seedSessionGame(session.id, game.id, { status: 'playing', played_at: baseTime, }); }); test('returns 200 with correct response shape for upvote', async () => { 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(200); expect(res.body.success).toBe(true); expect(res.body.session).toEqual( expect.objectContaining({ id: session.id }) ); expect(res.body.game).toEqual( expect.objectContaining({ id: game.id, title: 'Quiplash 3', upvotes: 1, downvotes: 0, popularity_score: 1, }) ); expect(res.body.vote).toEqual({ username: 'viewer1', type: 'up', timestamp: '2026-03-15T20:05:00.000Z', }); }); test('increments downvotes and decrements popularity_score for downvote', async () => { const res = 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.000Z', }); expect(res.status).toBe(200); expect(res.body.game.downvotes).toBe(1); expect(res.body.game.popularity_score).toBe(-1); }); test('returns 400 for missing username', async () => { const res = await request(app) .post('/api/votes/live') .set('Authorization', getAuthHeader()) .set('Content-Type', 'application/json') .send({ vote: 'up', timestamp: '2026-03-15T20:05:00.000Z' }); expect(res.status).toBe(400); expect(res.body.error).toMatch(/Missing required fields/); }); test('returns 400 for missing vote', async () => { const res = await request(app) .post('/api/votes/live') .set('Authorization', getAuthHeader()) .set('Content-Type', 'application/json') .send({ username: 'viewer1', timestamp: '2026-03-15T20:05:00.000Z' }); expect(res.status).toBe(400); }); test('returns 400 for missing timestamp', async () => { const res = await request(app) .post('/api/votes/live') .set('Authorization', getAuthHeader()) .set('Content-Type', 'application/json') .send({ username: 'viewer1', vote: 'up' }); expect(res.status).toBe(400); }); test('returns 400 for invalid vote value', async () => { const res = await request(app) .post('/api/votes/live') .set('Authorization', getAuthHeader()) .set('Content-Type', 'application/json') .send({ username: 'viewer1', vote: 'maybe', timestamp: '2026-03-15T20:05:00.000Z', }); expect(res.status).toBe(400); expect(res.body.error).toMatch(/vote must be either/); }); test('returns 400 for invalid timestamp format', async () => { const res = await request(app) .post('/api/votes/live') .set('Authorization', getAuthHeader()) .set('Content-Type', 'application/json') .send({ username: 'viewer1', vote: 'up', timestamp: 'not-a-date', }); expect(res.status).toBe(400); expect(res.body.error).toMatch(/Invalid timestamp/); }); test('returns 404 when no active session', async () => { cleanDb(); seedGame({ title: 'Unused' }); seedSession({ is_active: 0 }); 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); expect(res.body.error).toMatch(/No active session/); }); test('returns 404 when vote timestamp does not match any game', async () => { const res = await request(app) .post('/api/votes/live') .set('Authorization', getAuthHeader()) .set('Content-Type', 'application/json') .send({ username: 'viewer1', vote: 'up', timestamp: '2020-01-01T00:00:00.000Z', }); expect(res.status).toBe(404); expect(res.body.error).toMatch(/does not match any game/); }); test('returns 409 for duplicate vote within 1 second', async () => { 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 res = 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(res.status).toBe(409); expect(res.body.error).toMatch(/Duplicate vote/); }); test('returns 401 without auth token', async () => { const res = await request(app) .post('/api/votes/live') .set('Content-Type', 'application/json') .send({ username: 'viewer1', vote: 'up', timestamp: '2026-03-15T20:05:00.000Z', }); expect(res.status).toBe(401); }); });