feat(backend): PATCH /animals/:id/csv-config endpoint

Implement TDD for new route that accepts CSV configuration with validated buckets.
Route uses auditMiddleware to log config changes and delegates validation to
validateCsvConfig helper from Task 2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Experiments DB Dev
2026-05-01 07:49:00 -04:00
parent 1ae55591f4
commit fe4e89f497
2 changed files with 91 additions and 0 deletions
+70
View File
@@ -118,3 +118,73 @@ describe('DELETE /api/animals/:id', () => {
);
});
});
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/);
});
});