From 8ddbd1440f3efb4a946a4e8c0eb9472d020b9b1a Mon Sep 17 00:00:00 2001 From: cottongin Date: Sun, 15 Mar 2026 18:53:25 -0400 Subject: [PATCH] test: regression tests for POST /api/votes/live Made-with: Cursor --- tests/api/regression-votes-live.test.js | 199 ++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 tests/api/regression-votes-live.test.js diff --git a/tests/api/regression-votes-live.test.js b/tests/api/regression-votes-live.test.js new file mode 100644 index 0000000..a337053 --- /dev/null +++ b/tests/api/regression-votes-live.test.js @@ -0,0 +1,199 @@ +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); + }); +});