const request = require('supertest'); jest.mock('../src/lib/prisma', () => require('./__mocks__/prisma')); const prisma = require('../src/lib/prisma'); const app = require('../src/app'); const EXP_ID = 'bbbbbbbb-0000-0000-0000-000000000001'; const ANIMAL = { id: 'cccccccc-0000-0000-0000-000000000001', experiment_id: EXP_ID, animal_id_string: 'M-001', animal_name: 'Whiskers', _count: { daily_statuses: 0 }, }; beforeEach(() => jest.clearAllMocks()); describe('GET /api/animals', () => { it('returns animals filtered by experimentId', async () => { prisma.animal.findMany.mockResolvedValue([ANIMAL]); const res = await request(app).get(`/api/animals?experimentId=${EXP_ID}`); expect(res.status).toBe(200); expect(res.body).toHaveLength(1); expect(res.body[0].animal_id_string).toBe('M-001'); }); it('returns 422 for invalid UUID experimentId', async () => { const res = await request(app).get('/api/animals?experimentId=not-a-uuid'); expect(res.status).toBe(422); }); }); describe('GET /api/animals/:id', () => { it('returns animal with daily_statuses', async () => { prisma.animal.findUnique.mockResolvedValue({ ...ANIMAL, daily_statuses: [] }); const res = await request(app).get(`/api/animals/${ANIMAL.id}`); expect(res.status).toBe(200); expect(res.body.animal_name).toBe('Whiskers'); }); it('returns 404 for unknown animal', async () => { prisma.animal.findUnique.mockResolvedValue(null); const res = await request(app).get('/api/animals/00000000-0000-0000-0000-000000000000'); expect(res.status).toBe(404); }); }); describe('POST /api/animals', () => { it('creates animal with UUID and writes CREATE audit log', async () => { prisma.animal.create.mockResolvedValue(ANIMAL); prisma.auditLog.create.mockResolvedValue({}); const res = await request(app) .post('/api/animals') .send({ experiment_id: EXP_ID, animal_id_string: 'M-001', animal_name: 'Whiskers' }); expect(res.status).toBe(201); expect(res.body.id).toMatch(/^[0-9a-f-]{36}$/i); expect(prisma.auditLog.create).toHaveBeenCalled(); }); it('returns 422 when experiment_id is missing', async () => { const res = await request(app) .post('/api/animals') .send({ animal_id_string: 'M-001', animal_name: 'Whiskers' }); expect(res.status).toBe(422); expect(res.body.details).toEqual( expect.arrayContaining([expect.objectContaining({ field: 'experiment_id' })]) ); }); it('returns 422 when experiment_id is not a UUID', async () => { const res = await request(app) .post('/api/animals') .send({ experiment_id: 'bad-id', animal_id_string: 'M-001', animal_name: 'Whiskers' }); expect(res.status).toBe(422); }); it('returns 422 when animal_name is missing', async () => { const res = await request(app) .post('/api/animals') .send({ experiment_id: EXP_ID, animal_id_string: 'M-001' }); expect(res.status).toBe(422); }); }); describe('PUT /api/animals/:id', () => { it('updates animal and writes UPDATE audit log', async () => { const updated = { ...ANIMAL, animal_name: 'Mr Whiskers' }; prisma.animal.findUnique .mockResolvedValueOnce(ANIMAL) .mockResolvedValueOnce(updated); prisma.animal.update.mockResolvedValue(updated); prisma.auditLog.create.mockResolvedValue({}); const res = await request(app) .put(`/api/animals/${ANIMAL.id}`) .send({ animal_name: 'Mr Whiskers' }); expect(res.status).toBe(200); expect(res.body.animal_name).toBe('Mr Whiskers'); expect(prisma.auditLog.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ action: 'UPDATE' }) }) ); }); }); describe('DELETE /api/animals/:id', () => { it('deletes animal and writes DELETE audit log', async () => { prisma.animal.findUnique.mockResolvedValue(ANIMAL); prisma.animal.delete.mockResolvedValue(ANIMAL); prisma.auditLog.create.mockResolvedValue({}); const res = await request(app).delete(`/api/animals/${ANIMAL.id}`); expect(res.status).toBe(204); expect(prisma.auditLog.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ action: 'DELETE' }) }) ); }); }); describe('PATCH /api/animals/:id/csv-config', () => { const VALID_CONFIG = { buckets: [ { name: 'Success', type: 'note', notes: ['s'] }, { name: 'Failure', type: 'note', notes: ['f'] }, { name: 'Other', type: 'note', notes: [] }, { name: 'Lick latency', type: 'numerical', notes: ['L25', 'L30'] }, ], }; it('updates csv_analysis_config and writes audit log', async () => { prisma.animal.findUnique.mockResolvedValue(ANIMAL); prisma.animal.update.mockResolvedValue({ ...ANIMAL, csv_analysis_config: VALID_CONFIG }); prisma.auditLog.create.mockResolvedValue({}); const res = await request(app) .patch(`/api/animals/${ANIMAL.id}/csv-config`) .send(VALID_CONFIG); expect(res.status).toBe(200); expect(res.body.csv_analysis_config).toEqual(VALID_CONFIG); expect(prisma.animal.update).toHaveBeenCalledWith({ where: { id: ANIMAL.id }, data: { csv_analysis_config: VALID_CONFIG }, }); }); it('returns 400 when buckets is missing', async () => { const res = await request(app) .patch(`/api/animals/${ANIMAL.id}/csv-config`) .send({}); expect(res.status).toBe(400); expect(res.body.error).toMatch(/buckets/); }); it('returns 400 for empty bucket name', async () => { const bad = { buckets: [...VALID_CONFIG.buckets, { name: ' ', type: 'note', notes: [] }] }; const res = await request(app) .patch(`/api/animals/${ANIMAL.id}/csv-config`) .send(bad); expect(res.status).toBe(400); }); it('returns 400 for duplicate names', async () => { const bad = { buckets: [...VALID_CONFIG.buckets, { name: 'Success', type: 'note', notes: [] }] }; const res = await request(app) .patch(`/api/animals/${ANIMAL.id}/csv-config`) .send(bad); expect(res.status).toBe(400); expect(res.body.error).toMatch(/duplicate/i); }); it('returns 400 for invalid type', async () => { const bad = { buckets: [...VALID_CONFIG.buckets, { name: 'X', type: 'weird', notes: [] }] }; const res = await request(app) .patch(`/api/animals/${ANIMAL.id}/csv-config`) .send(bad); expect(res.status).toBe(400); }); it('returns 400 when missing default bucket', async () => { const bad = { buckets: VALID_CONFIG.buckets.filter((b) => b.name !== 'Success') }; const res = await request(app) .patch(`/api/animals/${ANIMAL.id}/csv-config`) .send(bad); expect(res.status).toBe(400); expect(res.body.error).toMatch(/Success/); }); });