feat(backend): add csv_analysis_config validator
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
const REQUIRED_DEFAULT_BUCKETS = ['Success', 'Failure', 'Other'];
|
||||
const ALLOWED_TYPES = new Set(['note', 'numerical']);
|
||||
|
||||
/**
|
||||
* Validate a csv_analysis_config payload.
|
||||
* @param {unknown} body
|
||||
* @returns {string|null} error message, or null if valid.
|
||||
*/
|
||||
function validateCsvConfig(body) {
|
||||
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
|
||||
return 'Body must be an object';
|
||||
}
|
||||
const { buckets } = body;
|
||||
if (!Array.isArray(buckets)) {
|
||||
return 'buckets must be an array';
|
||||
}
|
||||
|
||||
const seenNames = new Set();
|
||||
for (const b of buckets) {
|
||||
if (b === null || typeof b !== 'object' || Array.isArray(b)) {
|
||||
return 'Each bucket must be an object';
|
||||
}
|
||||
if (typeof b.name !== 'string' || b.name.trim() === '') {
|
||||
return 'Each bucket must have a non-empty name';
|
||||
}
|
||||
if (seenNames.has(b.name)) {
|
||||
return `Duplicate bucket name: ${b.name}`;
|
||||
}
|
||||
seenNames.add(b.name);
|
||||
|
||||
if (!ALLOWED_TYPES.has(b.type)) {
|
||||
return `Invalid type for bucket ${b.name}: ${b.type}`;
|
||||
}
|
||||
if (!Array.isArray(b.notes)) {
|
||||
return `notes must be an array for bucket ${b.name}`;
|
||||
}
|
||||
if (!b.notes.every((n) => typeof n === 'string')) {
|
||||
return `notes must be an array of strings for bucket ${b.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
for (const required of REQUIRED_DEFAULT_BUCKETS) {
|
||||
if (!seenNames.has(required)) {
|
||||
return `Missing required default bucket: ${required}`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = { validateCsvConfig, REQUIRED_DEFAULT_BUCKETS };
|
||||
@@ -0,0 +1,61 @@
|
||||
const { validateCsvConfig } = require('../src/lib/csvConfigValidation');
|
||||
|
||||
const okBuckets = [
|
||||
{ name: 'Success', type: 'note', notes: ['s'] },
|
||||
{ name: 'Failure', type: 'note', notes: ['f'] },
|
||||
{ name: 'Other', type: 'note', notes: [] },
|
||||
];
|
||||
|
||||
describe('validateCsvConfig', () => {
|
||||
it('accepts a valid config with the three default buckets', () => {
|
||||
expect(validateCsvConfig({ buckets: okBuckets })).toBeNull();
|
||||
});
|
||||
|
||||
it('accepts a valid config with extra note + numerical buckets', () => {
|
||||
const buckets = [
|
||||
...okBuckets,
|
||||
{ name: 'Lick latency', type: 'numerical', notes: ['L25'] },
|
||||
{ name: 'Bonus', type: 'note', notes: ['b'] },
|
||||
];
|
||||
expect(validateCsvConfig({ buckets })).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects when body is not an object', () => {
|
||||
expect(validateCsvConfig(null)).toMatch(/object/i);
|
||||
expect(validateCsvConfig('x')).toMatch(/object/i);
|
||||
});
|
||||
|
||||
it('rejects when buckets is not an array', () => {
|
||||
expect(validateCsvConfig({ buckets: 'nope' })).toMatch(/array/i);
|
||||
});
|
||||
|
||||
it('rejects an empty bucket name', () => {
|
||||
const buckets = [...okBuckets, { name: ' ', type: 'note', notes: [] }];
|
||||
expect(validateCsvConfig({ buckets })).toMatch(/name/i);
|
||||
});
|
||||
|
||||
it('rejects duplicate bucket names', () => {
|
||||
const buckets = [...okBuckets, { name: 'Success', type: 'note', notes: [] }];
|
||||
expect(validateCsvConfig({ buckets })).toMatch(/duplicate/i);
|
||||
});
|
||||
|
||||
it('rejects an invalid type value', () => {
|
||||
const buckets = [...okBuckets, { name: 'X', type: 'weird', notes: [] }];
|
||||
expect(validateCsvConfig({ buckets })).toMatch(/type/i);
|
||||
});
|
||||
|
||||
it('rejects when notes is not an array', () => {
|
||||
const buckets = [...okBuckets, { name: 'X', type: 'note', notes: 'oops' }];
|
||||
expect(validateCsvConfig({ buckets })).toMatch(/notes/i);
|
||||
});
|
||||
|
||||
it('rejects when notes contains non-strings', () => {
|
||||
const buckets = [...okBuckets, { name: 'X', type: 'note', notes: [1, 2] }];
|
||||
expect(validateCsvConfig({ buckets })).toMatch(/notes/i);
|
||||
});
|
||||
|
||||
it('rejects when a default bucket is missing', () => {
|
||||
const buckets = okBuckets.filter((b) => b.name !== 'Success');
|
||||
expect(validateCsvConfig({ buckets })).toMatch(/Success/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user