Covers WebSocket vote.received event, GET /api/sessions/:id/votes breakdown, GET /api/votes paginated history, and two-phase TDD testing strategy with regression tests before implementation. Made-with: Cursor
5.5 KiB
Vote Tracking API Design
Overview
Extend the REST and WebSocket APIs so clients can track votes at both session and global levels. The primary consumer is a stream overlay (ticker-style display) that already has the admin JWT.
Approach
Approach B — Split by resource ownership. Session-scoped vote data lives under the session resource. Global vote history lives under the vote resource. The WebSocket emits real-time events for live votes only.
WebSocket: vote.received Event
Trigger: POST /api/votes/live — fires after the vote transaction succeeds, before the HTTP response. Only live votes emit this event; chat-import does not.
Broadcast target: Session subscribers via broadcastEvent('vote.received', data, sessionId).
Payload:
{
"type": "vote.received",
"timestamp": "2026-03-15T20:30:00.000Z",
"data": {
"sessionId": 5,
"game": {
"id": 42,
"title": "Quiplash 3",
"pack_name": "Party Pack 7"
},
"vote": {
"username": "viewer123",
"type": "up",
"timestamp": "2026-03-15T20:29:55.000Z"
},
"totals": {
"upvotes": 14,
"downvotes": 3,
"popularity_score": 11
}
}
}
Implementation notes:
votes.jsneeds access to the WebSocket manager singleton viagetWebSocketManager().- The existing session games JOIN needs to select
pack_namefrom thegamestable.
REST: GET /api/sessions/:id/votes
Per-game vote breakdown for a specific session.
Location: backend/routes/sessions.js
Auth: None (matches GET /api/sessions/:id/games).
Response:
{
"session_id": 5,
"votes": [
{
"game_id": 42,
"title": "Quiplash 3",
"pack_name": "Party Pack 7",
"upvotes": 14,
"downvotes": 3,
"net_score": 11,
"total_votes": 17
}
]
}
Query:
SELECT
lv.game_id,
g.title,
g.pack_name,
SUM(CASE WHEN lv.vote_type = 1 THEN 1 ELSE 0 END) AS upvotes,
SUM(CASE WHEN lv.vote_type = -1 THEN 1 ELSE 0 END) AS downvotes,
SUM(lv.vote_type) AS net_score,
COUNT(*) AS total_votes
FROM live_votes lv
JOIN games g ON lv.game_id = g.id
WHERE lv.session_id = ?
GROUP BY lv.game_id
ORDER BY net_score DESC
Error handling:
- Session not found → 404
- Session exists, no votes → 200 with empty
votesarray
REST: GET /api/votes
Paginated global vote history with flexible filtering.
Location: backend/routes/votes.js
Auth: None.
Query parameters:
| Param | Type | Default | Description |
|---|---|---|---|
session_id |
integer | — | Filter by session |
game_id |
integer | — | Filter by game |
username |
string | — | Filter by voter |
vote_type |
up or down |
— | Filter by direction |
page |
integer | 1 | Page number |
limit |
integer | 50 | Results per page (max 100) |
Response:
{
"votes": [
{
"id": 891,
"session_id": 5,
"game_id": 42,
"game_title": "Quiplash 3",
"pack_name": "Party Pack 7",
"username": "viewer123",
"vote_type": "up",
"timestamp": "2026-03-15T20:29:55.000Z",
"created_at": "2026-03-15T20:29:56.000Z"
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 237,
"total_pages": 5
}
}
Design notes:
vote_typereturned as"up"/"down", not raw1/-1.game_titleandpack_nameincluded via JOIN.- Ordered by
timestamp DESC. limitcapped at 100 server-side.
Error handling:
- Invalid filter values → 400
- No results → 200 with empty array and
total: 0
Testing Strategy
Phase 1: Regression tests (pre-implementation)
Written and passing before any code changes to lock down existing behavior.
tests/api/regression-votes-live.test.js — existing POST /api/votes/live:
- Returns 200 with correct response shape (
success,session,game,vote) gameincludesid,title,upvotes,downvotes,popularity_score- Increments
upvotes/popularity_scorefor upvote - Increments
downvotes/decrementspopularity_scorefor downvote - 400 for missing fields, invalid vote value, invalid timestamp
- 404 when no active session or timestamp doesn't match a game
- 409 for duplicate within 1-second window
- 401 without JWT
tests/api/regression-games.test.js — game aggregate fields:
GET /api/gamesreturnsupvotes,downvotes,popularity_scoreGET /api/games/:idreturns same fields- Aggregates accurate after votes
tests/api/regression-sessions.test.js — session endpoints:
GET /api/sessions/:idreturns session objectGET /api/sessions/:idreturns 404 for nonexistent sessionGET /api/sessions/:id/gamesreturns game list with expected shape
tests/api/regression-websocket.test.js — existing WebSocket events:
- Auth flow (auth → auth_success)
- Subscribe/unsubscribe flow
session.startedbroadcast on session createsession.endedbroadcast on session closegame.addedbroadcast on game add
Phase 2: New feature tests (TDD — written before implementation)
tests/api/votes-get.test.js—GET /api/voteshistory endpointtests/api/sessions-votes.test.js—GET /api/sessions/:id/votesbreakdowntests/api/votes-live-websocket.test.js—vote.receivedWebSocket event
Workflow
- Write Phase 1 regression tests → run → all green
- Write Phase 2 feature tests → run → all red
- Implement features
- Run all tests → Phase 1 still green, Phase 2 now green