const request = require('supertest'); const { app } = require('../../backend/server'); const { cleanDb, getAuthHeader, seedSession } = require('../helpers/test-utils'); const { computeNotesPreview } = require('../../backend/utils/notes-preview'); describe('computeNotesPreview', () => { test('returns has_notes false and null preview for null input', () => { const result = computeNotesPreview(null); expect(result).toEqual({ has_notes: false, notes_preview: null }); }); test('returns has_notes false and null preview for empty string', () => { const result = computeNotesPreview(''); expect(result).toEqual({ has_notes: false, notes_preview: null }); }); test('returns first paragraph as preview', () => { const notes = 'First paragraph here.\n\nSecond paragraph here.'; const result = computeNotesPreview(notes); expect(result.has_notes).toBe(true); expect(result.notes_preview).toBe('First paragraph here.'); }); test('strips markdown bold formatting', () => { const notes = '**Bold text** and more'; const result = computeNotesPreview(notes); expect(result.notes_preview).toBe('Bold text and more'); }); test('strips markdown italic formatting', () => { const notes = '*Italic text* and _also italic_'; const result = computeNotesPreview(notes); expect(result.notes_preview).toBe('Italic text and also italic'); }); test('strips markdown links', () => { const notes = 'Check [this link](http://example.com) out'; const result = computeNotesPreview(notes); expect(result.notes_preview).toBe('Check this link out'); }); test('strips markdown headers', () => { const notes = '## Header text'; const result = computeNotesPreview(notes); expect(result.notes_preview).toBe('Header text'); }); test('strips markdown list markers', () => { const notes = '- Item one\n- Item two'; const result = computeNotesPreview(notes); expect(result.notes_preview).toBe('Item one Item two'); }); test('truncates to 150 characters with ellipsis', () => { const notes = 'A'.repeat(200); const result = computeNotesPreview(notes); expect(result.notes_preview).toHaveLength(153); // 150 + '...' expect(result.notes_preview.endsWith('...')).toBe(true); }); test('does not truncate text at or under 150 characters', () => { const notes = 'A'.repeat(150); const result = computeNotesPreview(notes); expect(result.notes_preview).toHaveLength(150); expect(result.notes_preview).not.toContain('...'); }); }); describe('GET /api/sessions list', () => { beforeEach(() => { cleanDb(); }); test('includes has_notes and notes_preview in list response', async () => { seedSession({ notes: '**Bold** first paragraph\n\nSecond paragraph' }); seedSession({ notes: null }); const res = await request(app).get('/api/sessions'); expect(res.status).toBe(200); expect(res.body).toHaveLength(2); const withNotes = res.body.find(s => s.has_notes === true); const withoutNotes = res.body.find(s => s.has_notes === false); expect(withNotes.notes_preview).toBe('Bold first paragraph'); expect(withNotes).not.toHaveProperty('notes'); expect(withoutNotes.notes_preview).toBeNull(); expect(withoutNotes).not.toHaveProperty('notes'); }); test('list response preserves existing fields', async () => { seedSession({ is_active: 1, notes: 'Test' }); const res = await request(app).get('/api/sessions'); expect(res.status).toBe(200); expect(res.body[0]).toHaveProperty('id'); expect(res.body[0]).toHaveProperty('created_at'); expect(res.body[0]).toHaveProperty('closed_at'); expect(res.body[0]).toHaveProperty('is_active'); expect(res.body[0]).toHaveProperty('games_played'); }); }); describe('GET /api/sessions/:id notes visibility', () => { beforeEach(() => { cleanDb(); }); test('returns full notes when authenticated', async () => { const session = seedSession({ notes: '**Full notes** here\n\nSecond paragraph' }); const res = await request(app) .get(`/api/sessions/${session.id}`) .set('Authorization', getAuthHeader()); expect(res.status).toBe(200); expect(res.body.notes).toBe('**Full notes** here\n\nSecond paragraph'); expect(res.body.has_notes).toBe(true); expect(res.body.notes_preview).toBe('Full notes here'); }); test('returns only preview when unauthenticated', async () => { const session = seedSession({ notes: '**Full notes** here\n\nSecond paragraph' }); const res = await request(app) .get(`/api/sessions/${session.id}`); expect(res.status).toBe(200); expect(res.body.notes).toBeUndefined(); expect(res.body.has_notes).toBe(true); expect(res.body.notes_preview).toBe('Full notes here'); }); test('returns has_notes false when no notes', async () => { const session = seedSession({ notes: null }); const res = await request(app) .get(`/api/sessions/${session.id}`); expect(res.status).toBe(200); expect(res.body.has_notes).toBe(false); 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); }); });