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
+21
View File
@@ -3,6 +3,7 @@ const { body, query } = require('express-validator');
const prisma = require('../lib/prisma'); const prisma = require('../lib/prisma');
const auditMiddleware = require('../middleware/auditMiddleware'); const auditMiddleware = require('../middleware/auditMiddleware');
const { validateRequest } = require('../middleware/errorHandler'); const { validateRequest } = require('../middleware/errorHandler');
const { validateCsvConfig } = require('../lib/csvConfigValidation');
const animalValidation = [ const animalValidation = [
body('experiment_id').notEmpty().withMessage('experiment_id is required').matches(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i).withMessage('experiment_id must be a valid UUID'), body('experiment_id').notEmpty().withMessage('experiment_id is required').matches(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i).withMessage('experiment_id must be a valid UUID'),
@@ -112,4 +113,24 @@ router.delete(
} }
); );
// PATCH /api/animals/:id/csv-config — replace entire CSV-analysis bucket config
router.patch(
'/:id/csv-config',
auditMiddleware('animals', prisma.animal),
async (req, res, next) => {
try {
const err = validateCsvConfig(req.body);
if (err) return res.status(400).json({ error: err });
const animal = await prisma.animal.update({
where: { id: req.params.id },
data: { csv_analysis_config: req.body },
});
res.json(animal);
} catch (e) {
next(e);
}
}
);
module.exports = router; module.exports = router;
+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/);
});
});