const request = require('supertest'); // Mock Prisma before importing app so the app uses the mock jest.mock('../src/lib/prisma', () => require('./__mocks__/prisma')); const prisma = require('../src/lib/prisma'); const app = require('../src/app'); const EXPERIMENT = { id: 'aaaaaaaa-0000-0000-0000-000000000001', title: 'Test Study', created_at: new Date('2024-01-01').toISOString(), _count: { animals: 0 }, }; beforeEach(() => { jest.clearAllMocks(); }); describe('GET /api/experiments', () => { it('returns list of experiments', async () => { prisma.experiment.findMany.mockResolvedValue([EXPERIMENT]); const res = await request(app).get('/api/experiments'); expect(res.status).toBe(200); expect(res.body).toHaveLength(1); expect(res.body[0].title).toBe('Test Study'); }); it('returns empty array when no experiments', async () => { prisma.experiment.findMany.mockResolvedValue([]); const res = await request(app).get('/api/experiments'); expect(res.status).toBe(200); expect(res.body).toEqual([]); }); }); describe('GET /api/experiments/:id', () => { it('returns single experiment', async () => { prisma.experiment.findUnique.mockResolvedValue({ ...EXPERIMENT, animals: [] }); const res = await request(app).get(`/api/experiments/${EXPERIMENT.id}`); expect(res.status).toBe(200); expect(res.body.id).toBe(EXPERIMENT.id); }); it('returns 404 for unknown id', async () => { prisma.experiment.findUnique.mockResolvedValue(null); const res = await request(app).get('/api/experiments/00000000-0000-0000-0000-000000000000'); expect(res.status).toBe(404); expect(res.body.error).toMatch(/not found/i); }); }); describe('POST /api/experiments', () => { it('creates experiment and returns 201 with UUID', async () => { prisma.experiment.create.mockResolvedValue(EXPERIMENT); prisma.auditLog.create.mockResolvedValue({}); const res = await request(app) .post('/api/experiments') .send({ title: 'Test Study' }); expect(res.status).toBe(201); expect(res.body.id).toBe(EXPERIMENT.id); expect(res.body.title).toBe('Test Study'); // UUID format assertion expect(res.body.id).toMatch( /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i ); expect(prisma.auditLog.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ action: 'CREATE' }) }) ); }); it('returns 422 when title is missing', async () => { const res = await request(app).post('/api/experiments').send({}); expect(res.status).toBe(422); expect(res.body.details).toEqual( expect.arrayContaining([expect.objectContaining({ field: 'title' })]) ); }); it('returns 422 when title exceeds 255 chars', async () => { const res = await request(app) .post('/api/experiments') .send({ title: 'x'.repeat(256) }); expect(res.status).toBe(422); }); }); describe('PUT /api/experiments/:id', () => { it('updates experiment and writes audit log', async () => { const updated = { ...EXPERIMENT, title: 'Renamed Study' }; prisma.experiment.findUnique .mockResolvedValueOnce(EXPERIMENT) // audit middleware fetches before-state .mockResolvedValueOnce(updated); // audit middleware fetches after-state prisma.experiment.update.mockResolvedValue(updated); prisma.auditLog.create.mockResolvedValue({}); const res = await request(app) .put(`/api/experiments/${EXPERIMENT.id}`) .send({ title: 'Renamed Study' }); expect(res.status).toBe(200); expect(res.body.title).toBe('Renamed Study'); expect(prisma.auditLog.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ action: 'UPDATE' }) }) ); }); it('returns 422 with empty title', async () => { prisma.experiment.findUnique.mockResolvedValue(EXPERIMENT); const res = await request(app) .put(`/api/experiments/${EXPERIMENT.id}`) .send({ title: '' }); expect(res.status).toBe(422); }); }); describe('DELETE /api/experiments/:id', () => { it('deletes experiment and writes DELETE audit log', async () => { prisma.experiment.findUnique.mockResolvedValue(EXPERIMENT); prisma.experiment.delete.mockResolvedValue(EXPERIMENT); prisma.auditLog.create.mockResolvedValue({}); const res = await request(app).delete(`/api/experiments/${EXPERIMENT.id}`); expect(res.status).toBe(204); expect(prisma.auditLog.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ action: 'DELETE' }) }) ); }); }); describe('GET /health', () => { it('returns ok status', async () => { const res = await request(app).get('/health'); expect(res.status).toBe(200); expect(res.body.status).toBe('ok'); }); });