From b9206b6cfef05d04a23d0ef271bc2bc3be7f7e9e Mon Sep 17 00:00:00 2001 From: cottongin Date: Mon, 23 Mar 2026 00:13:09 -0400 Subject: [PATCH] feat: add PUT and DELETE /api/sessions/:id/notes endpoints Made-with: Cursor --- backend/routes/sessions.js | 42 ++++++++++++ tests/api/session-notes.test.js | 112 ++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/backend/routes/sessions.js b/backend/routes/sessions.js index ce28e9f..9f262df 100644 --- a/backend/routes/sessions.js +++ b/backend/routes/sessions.js @@ -245,6 +245,48 @@ router.delete('/:id', authenticateToken, (req, res) => { } }); +// Update session notes (admin only) +router.put('/:id/notes', authenticateToken, (req, res) => { + try { + const { notes } = req.body; + + const session = db.prepare('SELECT id FROM sessions WHERE id = ?').get(req.params.id); + if (!session) { + return res.status(404).json({ error: 'Session not found' }); + } + + db.prepare('UPDATE sessions SET notes = ? WHERE id = ?').run(notes, req.params.id); + + const updated = db.prepare(` + SELECT s.*, COUNT(sg.id) as games_played + FROM sessions s + LEFT JOIN session_games sg ON s.id = sg.session_id + WHERE s.id = ? + GROUP BY s.id + `).get(req.params.id); + + res.json(updated); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Clear session notes (admin only) +router.delete('/:id/notes', authenticateToken, (req, res) => { + try { + const session = db.prepare('SELECT id FROM sessions WHERE id = ?').get(req.params.id); + if (!session) { + return res.status(404).json({ error: 'Session not found' }); + } + + db.prepare('UPDATE sessions SET notes = NULL WHERE id = ?').run(req.params.id); + + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + // Get games played in a session router.get('/:id/games', (req, res) => { try { diff --git a/tests/api/session-notes.test.js b/tests/api/session-notes.test.js index 4d0a4e8..06dbe42 100644 --- a/tests/api/session-notes.test.js +++ b/tests/api/session-notes.test.js @@ -145,3 +145,115 @@ describe('GET /api/sessions/:id notes visibility', () => { expect(res.body.notes_preview).toBeNull(); }); }); + +describe('PUT /api/sessions/:id/notes', () => { + beforeEach(() => { + cleanDb(); + }); + + test('updates notes when authenticated', async () => { + const session = seedSession({ notes: 'Old notes' }); + + const res = await request(app) + .put(`/api/sessions/${session.id}/notes`) + .set('Authorization', getAuthHeader()) + .send({ notes: 'New notes here' }); + + expect(res.status).toBe(200); + expect(res.body.notes).toBe('New notes here'); + }); + + test('overwrites notes completely (no merge)', async () => { + const session = seedSession({ notes: 'Original notes' }); + + const res = await request(app) + .put(`/api/sessions/${session.id}/notes`) + .set('Authorization', getAuthHeader()) + .send({ notes: 'Replacement' }); + + expect(res.status).toBe(200); + expect(res.body.notes).toBe('Replacement'); + }); + + test('returns 404 for nonexistent session', async () => { + const res = await request(app) + .put('/api/sessions/99999/notes') + .set('Authorization', getAuthHeader()) + .send({ notes: 'test' }); + + expect(res.status).toBe(404); + }); + + test('returns 401 without auth header', async () => { + const session = seedSession({}); + + const res = await request(app) + .put(`/api/sessions/${session.id}/notes`) + .send({ notes: 'test' }); + + expect(res.status).toBe(401); + }); + + test('returns 403 with invalid token', async () => { + const session = seedSession({}); + + const res = await request(app) + .put(`/api/sessions/${session.id}/notes`) + .set('Authorization', 'Bearer invalid-token') + .send({ notes: 'test' }); + + expect(res.status).toBe(403); + }); +}); + +describe('DELETE /api/sessions/:id/notes', () => { + beforeEach(() => { + cleanDb(); + }); + + test('clears notes when authenticated', async () => { + const session = seedSession({ notes: 'Some notes' }); + + const res = await request(app) + .delete(`/api/sessions/${session.id}/notes`) + .set('Authorization', getAuthHeader()); + + expect(res.status).toBe(200); + expect(res.body.success).toBe(true); + + // Verify notes are actually cleared + const check = await request(app) + .get(`/api/sessions/${session.id}`) + .set('Authorization', getAuthHeader()); + expect(check.body.notes).toBeNull(); + expect(check.body.has_notes).toBe(false); + }); + + test('returns 404 for nonexistent session', async () => { + const res = await request(app) + .delete('/api/sessions/99999/notes') + .set('Authorization', getAuthHeader()); + + expect(res.status).toBe(404); + }); + + test('returns 401 without auth header', async () => { + const session = seedSession({ notes: 'test' }); + + const res = await request(app) + .delete(`/api/sessions/${session.id}/notes`); + + expect(res.status).toBe(401); + }); + + test('returns 403 with invalid token', async () => { + const session = seedSession({ notes: 'test' }); + + const res = await request(app) + .delete(`/api/sessions/${session.id}/notes`) + .set('Authorization', 'Bearer invalid-token'); + + expect(res.status).toBe(403); + }); +}); +