import { describe, it, expect, vi, beforeEach } from 'vitest'; import request from 'supertest'; import { Readable } from 'node:stream'; import app from '../app.js'; vi.mock('../db/notes.dao.js', () => ({ getAllNotes: vi.fn(), streamAllNotes: vi.fn(), })); vi.mock('../services/clustering.service.js', () => ({ clusterNotes: vi.fn(), })); import { getAllNotes, streamAllNotes } from '../db/notes.dao.js'; import { clusterNotes } from '../services/clustering.service.js'; const MOCK_NOTES = [ { id: 'note_001', text: 'Login flow feels confusing', x: 193, y: 191, author: 'user_5', color: 'yellow' }, { id: 'note_002', text: 'Login flow is broken on mobile', x: 214, y: 281, author: 'user_9', color: 'yellow' }, { id: 'note_003', text: 'Export takes too long', x: 798, y: 211, author: 'user_2', color: 'green' }, ]; const MOCK_CLUSTERS = [ { label: 'Login Issues', noteIds: ['note_001', 'note_002'] }, { label: 'Export Problems', noteIds: ['note_003'] }, ]; const rowStream = (rows) => Readable.from(rows, { objectMode: true }); describe('GET /v1/notes', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should return 200 and an array of notes', async () => { streamAllNotes.mockResolvedValue(rowStream(MOCK_NOTES)); const res = await request(app).get('/v1/notes'); expect(res.status).toBe(200); expect(res.body).toEqual(MOCK_NOTES); expect(Array.isArray(res.body)).toBe(true); }); it('should send JSON incrementally rather than buffering the row set', async () => { streamAllNotes.mockResolvedValue(rowStream(MOCK_NOTES)); const res = await request(app).get('/v1/notes'); expect(res.headers['content-type']).toMatch(/application\/json/); expect(res.headers['content-length']).toBeUndefined(); }); it('should return an empty array when there are no notes', async () => { streamAllNotes.mockResolvedValue(rowStream([])); const res = await request(app).get('/v1/notes'); expect(res.status).toBe(200); expect(res.body).toEqual([]); }); it('should return notes with expected properties', async () => { streamAllNotes.mockResolvedValue(rowStream(MOCK_NOTES)); const res = await request(app).get('/v1/notes'); const note = res.body[0]; expect(note).toHaveProperty('id'); expect(note).toHaveProperty('text'); expect(note).toHaveProperty('x'); expect(note).toHaveProperty('y'); expect(note).toHaveProperty('author'); expect(note).toHaveProperty('color'); }); it('should return 500 when the database query fails', async () => { streamAllNotes.mockRejectedValue(new Error('connection refused')); const res = await request(app).get('/v1/notes'); expect(res.status).toBe(500); expect(res.body).toHaveProperty('error'); expect(res.body.error).toBe('Failed to load notes'); }); it('should abort the response when the row stream fails mid-flight', async () => { const failing = new Readable({ objectMode: true, read() { this.push(MOCK_NOTES[0]); this.destroy(new Error('connection lost')); }, }); streamAllNotes.mockResolvedValue(failing); await expect(request(app).get('/v1/notes')).rejects.toThrow(); }); }); describe('POST /v1/notes/cluster', () => { beforeEach(() => { vi.clearAllMocks(); }); it('should return 200 and clustered results', async () => { getAllNotes.mockResolvedValue(MOCK_NOTES); clusterNotes.mockResolvedValue(MOCK_CLUSTERS); const res = await request(app).post('/v1/notes/cluster'); expect(res.status).toBe(200); expect(res.body).toEqual(MOCK_CLUSTERS); }); it('should return clusters with the expected shape (label, noteIds)', async () => { getAllNotes.mockResolvedValue(MOCK_NOTES); clusterNotes.mockResolvedValue(MOCK_CLUSTERS); const res = await request(app).post('/v1/notes/cluster'); const cluster = res.body[0]; expect(cluster).toHaveProperty('label'); expect(cluster).toHaveProperty('noteIds'); expect(typeof cluster.label).toBe('string'); expect(Array.isArray(cluster.noteIds)).toBe(true); }); it('should pass the loaded notes to clusterNotes', async () => { getAllNotes.mockResolvedValue(MOCK_NOTES); clusterNotes.mockResolvedValue(MOCK_CLUSTERS); await request(app).post('/v1/notes/cluster'); expect(clusterNotes).toHaveBeenCalledOnce(); expect(clusterNotes).toHaveBeenCalledWith( MOCK_NOTES, expect.objectContaining({ signal: expect.any(AbortSignal) }) ); }); it('should pass a signal that is not aborted while the request is open', async () => { getAllNotes.mockResolvedValue(MOCK_NOTES); clusterNotes.mockImplementation(async (_notes, { signal }) => { expect(signal.aborted).toBe(false); return MOCK_CLUSTERS; }); const res = await request(app).post('/v1/notes/cluster'); expect(res.status).toBe(200); }); it('should return 500 when clusterNotes (API call) fails', async () => { getAllNotes.mockResolvedValue(MOCK_NOTES); clusterNotes.mockRejectedValue(new Error('LLM API error')); const res = await request(app).post('/v1/notes/cluster'); expect(res.status).toBe(500); expect(res.body).toHaveProperty('error'); expect(res.body.error).toMatch(/^Clustering failed/); }); it('should return 500 when the database query fails', async () => { getAllNotes.mockRejectedValue(new Error('connection refused')); const res = await request(app).post('/v1/notes/cluster'); expect(res.status).toBe(500); expect(res.body).toHaveProperty('error'); }); });