diff --git a/src/api.ts b/src/api.ts index f85393f..228f166 100644 --- a/src/api.ts +++ b/src/api.ts @@ -25,7 +25,7 @@ export default class api { console.error(err); } else { - console.info('API ok, version ' + api.info.version); + console.info(process.env.NODE_ENV === 'test' ? '' : 'API ok, version ' + api.info.version); swagger.setup(apiDoc); } }); diff --git a/src/models/measurement_template.ts b/src/models/measurement_template.ts index c55cbc7..1b0f6e9 100644 --- a/src/models/measurement_template.ts +++ b/src/models/measurement_template.ts @@ -2,6 +2,7 @@ import mongoose from 'mongoose'; const MeasurementTemplateSchema = new mongoose.Schema({ name: {type: String, index: {unique: true}}, + version: Number, parameters: [{ name: String, range: mongoose.Schema.Types.Mixed diff --git a/src/models/treatment_template.ts b/src/models/treatment_template.ts index 3b61164..8fc4af8 100644 --- a/src/models/treatment_template.ts +++ b/src/models/treatment_template.ts @@ -2,6 +2,8 @@ import mongoose from 'mongoose'; const TreatmentTemplateSchema = new mongoose.Schema({ name: {type: String, index: {unique: true}}, + version: Number, + number_prefix: String, parameters: [{ name: String, range: mongoose.Schema.Types.Mixed diff --git a/src/routes/template.spec.ts b/src/routes/template.spec.ts index 3c05991..e89e238 100644 --- a/src/routes/template.spec.ts +++ b/src/routes/template.spec.ts @@ -24,7 +24,7 @@ describe('/template', () => { const json = require('../test/db.json'); should(res.body).have.lengthOf(json.collections.treatment_templates.length); should(res.body).matchEach(treatment => { - should(treatment).have.only.keys('_id', 'name', 'parameters'); + should(treatment).have.only.keys('_id', 'name', 'version', 'parameters', 'number_prefix'); should(treatment).have.property('_id').be.type('string'); should(treatment).have.property('name').be.type('string'); should(treatment).have.property('version').be.type('number'); @@ -131,22 +131,44 @@ describe('/template', () => { }).end((err, res) => { if (err) return done(err); should(res.body).be.eql({_id: '200000000000000000000001', name: 'heat aging', version: 2, number_prefix: 'A', parameters: [{name: 'time', range: {min: 1}}]}); - TemplateTreatmentModel.find({name: 'heat aging'}).lean().exec((err, data:any) => { + TemplateTreatmentModel.findById('200000000000000000000001').lean().exec((err, data:any) => { if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.only.keys('_id', 'name', 'version', 'number_prefix', 'parameters', '__v'); - should(data[0]).have.property('name', 'heat aging'); - should(data[0]).have.property('version', 2); - should(data[0]).have.property('number_prefix', 'A'); - should(data[0]).have.property('parameters').have.lengthOf(1); - should(data[0].parameters[0]).have.property('name', 'time'); - should(data[0].parameters[0]).have.property('range'); - should(data[0].parameters[0].range).have.property('min', 1); + should(data).have.only.keys('_id', 'name', 'version', 'number_prefix', 'parameters', '__v'); + should(data).have.property('name', 'heat aging'); + should(data).have.property('version', 2); + should(data).have.property('number_prefix', 'A'); + should(data).have.property('parameters').have.lengthOf(1); + should(data.parameters[0]).have.property('name', 'time'); + should(data.parameters[0]).have.property('range'); + should(data.parameters[0].range).have.property('min', 1); done(); }); }); }); - it('allows changing only one property'); // TODO: adapt PUT to other PUTs and do POST, everything for measurement too + it('allows changing only one property', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/200000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'heat aging'} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '200000000000000000000001', name: 'heat aging', version: 2, number_prefix: 'A', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]}); + TemplateTreatmentModel.find({name: 'heat aging'}).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'version', 'number_prefix', 'parameters', '__v'); + should(data[0]).have.property('name', 'heat aging'); + should(data[0]).have.property('version', 2); + should(data[0]).have.property('number_prefix', 'A'); + should(data[0]).have.property('parameters').have.lengthOf(1); + should(data[0].parameters[0]).have.property('name', 'material'); + should(data[0].parameters[1]).have.property('name', 'weeks'); + done(); + }); + }); + }); it('supports values ranges', done => { TestHelper.request(server, done, { method: 'put', @@ -167,6 +189,16 @@ describe('/template', () => { res: {_id: '200000000000000000000001', name: 'heat treatment', version: 2, number_prefix: 'A', parameters: [{name: 'time', range: {min: 1, max: 11}}]} }); }); + it('supports array type ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/200000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'time', range: {type: 'array'}}]}, + res: {_id: '200000000000000000000001', name: 'heat treatment', version: 2, number_prefix: 'A', parameters: [{name: 'time', range: {type: 'array'}}]} + }); + }); it('supports empty ranges', done => { TestHelper.request(server, done, { method: 'put', @@ -177,6 +209,16 @@ describe('/template', () => { res: {_id: '200000000000000000000001', name: 'heat treatment', version: 2, number_prefix: 'A', parameters: [{name: 'time', range: {}}]} }); }); + it('rejects not specified parameters', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/treatment/200000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'heat treatment', parameters: [{name: 'material', range: {xx: 5}}]}, + res: {} + }); + }) it('rejects an invalid id', done => { TestHelper.request(server, done, { method: 'put', @@ -236,63 +278,81 @@ describe('/template', () => { describe('POST /template/treatment/new', () => { it('returns the right treatment template', done => { TestHelper.request(server, done, { - method: 'put', - url: '/template/treatment/200000000000000000000001', + method: 'post', + url: '/template/treatment/new', auth: {basic: 'admin'}, httpStatus: 200, - req: {}, - res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, number_prefix: 'A', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]} + req: {name: 'heat treatment3', number_prefix: 'C', parameters: [{name: 'material', range: {values: ['copper']}}]} + }).end((err, res) => { + if (err) return done(err); + should(res.body).have.only.keys('_id', 'name', 'version', 'number_prefix', 'parameters'); + should(res.body).have.property('name', 'heat treatment3'); + should(res.body).have.property('version', 1); + should(res.body).have.property('number_prefix', 'C'); + should(res.body).have.property('parameters').have.lengthOf(1); + should(res.body.parameters[0]).have.property('name', 'material'); + should(res.body.parameters[0]).have.property('range'); + should(res.body.parameters[0].range).have.property('values'); + should(res.body.parameters[0].range.values[0]).be.eql('copper'); }); }); it('stores the template', done => { TestHelper.request(server, done, { - method: 'put', - url: '/template/treatment/200000000000000000000001', + method: 'post', + url: '/template/treatment/new', auth: {basic: 'admin'}, httpStatus: 200, - req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} + req: {name: 'heat aging', number_prefix: 'C', parameters: [{name: 'time', range: {min: 1}}]} }).end((err, res) => { if (err) return done(err); - should(res.body).be.eql({_id: '200000000000000000000001', name: 'heat aging', version: 2, number_prefix: 'A', parameters: [{name: 'time', range: {min: 1}}]}); - TemplateTreatmentModel.find({name: 'heat aging'}).lean().exec((err, data:any) => { + TemplateTreatmentModel.findById(res.body._id).lean().exec((err, data:any) => { if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.only.keys('_id', 'name', 'version', 'number_prefix', 'parameters', '__v'); - should(data[0]).have.property('name', 'heat aging'); - should(data[0]).have.property('version', 2); - should(data[0]).have.property('number_prefix', 'A'); - should(data[0]).have.property('parameters').have.lengthOf(1); - should(data[0].parameters[0]).have.property('name', 'time'); - should(data[0].parameters[0]).have.property('range'); - should(data[0].parameters[0].range).have.property('min', 1); + should(data).have.only.keys('_id', 'name', 'version', 'number_prefix', 'parameters', '__v'); + should(data).have.property('name', 'heat aging'); + should(data).have.property('version', 1); + should(data).have.property('number_prefix', 'C'); + should(data).have.property('parameters').have.lengthOf(1); + should(data.parameters[0]).have.property('name', 'time'); + should(data.parameters[0]).have.property('range'); + should(data.parameters[0].range).have.property('min', 1); done(); }); }); }); it('rejects a missing name', done => { TestHelper.request(server, done, { - method: 'put', - url: '/template/treatment/heat%20aging', + method: 'post', + url: '/template/treatment/new', auth: {basic: 'admin'}, httpStatus: 400, - req: {parameters: [{name: 'time', range: {min: 1}}]}, + req: {number_prefix: 'C', parameters: [{name: 'time', range: {min: 1}}]}, res: {status: 'Invalid body format', details: '"name" is required'} }); }); + it('rejects a missing number prefix', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/treatment/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]}, + res: {status: 'Invalid body format', details: '"number_prefix" is required'} + }); + }); it('rejects missing parameters', done => { TestHelper.request(server, done, { - method: 'put', - url: '/template/treatment/heat%20aging', + method: 'post', + url: '/template/treatment/new', auth: {basic: 'admin'}, httpStatus: 400, - req: {name: 'heat aging'}, + req: {name: 'heat aging', number_prefix: 'C'}, res: {status: 'Invalid body format', details: '"parameters" is required'} }); }); it('rejects a missing parameter name', done => { TestHelper.request(server, done, { - method: 'put', - url: '/template/treatment/heat%20aging', + method: 'post', + url: '/template/treatment/new', auth: {basic: 'admin'}, httpStatus: 400, req: {name: 'heat aging', parameters: [{range: {min: 1}}]}, @@ -301,18 +361,18 @@ describe('/template', () => { }); it('rejects a missing parameter range', done => { TestHelper.request(server, done, { - method: 'put', - url: '/template/treatment/heat%20aging', + method: 'post', + url: '/template/treatment/new', auth: {basic: 'admin'}, httpStatus: 400, req: {name: 'heat aging', parameters: [{name: 'time'}]}, res: {status: 'Invalid body format', details: '"parameters[0].range" is required'} }); }); - it('rejects a an invalid parameter range property', done => { + it('rejects an invalid parameter range property', done => { TestHelper.request(server, done, { - method: 'put', - url: '/template/treatment/heat%20aging', + method: 'post', + url: '/template/treatment/new', auth: {basic: 'admin'}, httpStatus: 400, req: {name: 'heat aging', parameters: [{name: 'time', range: {xx: 1}}]}, @@ -321,12 +381,48 @@ describe('/template', () => { }); it('rejects wrong properties', done => { TestHelper.request(server, done, { - method: 'put', - url: '/template/treatment/heat%20aging', + method: 'post', + url: '/template/treatment/new', auth: {basic: 'admin'}, httpStatus: 400, - req: {parameters: [{name: 'time'}], xx: 33}, - res: {status: 'Invalid body format', details: '"name" is required'} + req: {name: 'heat aging', parameters: [{name: 'time', range: {}}], xx: 33}, + res: {} + }); + }); + it('rejects already existing number prefixes', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/treatment/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'heat aging', number_prefix: 'B', parameters: [{name: 'time', range: {min: 1}}]}, + res: {status: 'Number prefix already taken'} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/treatment/new', + auth: {key: 'admin'}, + httpStatus: 401, + req: {name: 'heat aging', number_prefix: 'C', parameters: [{name: 'time', range: {min: 1}}]} + }); + }); + it('rejects requests from a write user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/treatment/new', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {name: 'heat aging', number_prefix: 'C', parameters: [{name: 'time', range: {min: 1}}]} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/treatment/new', + httpStatus: 401, + req: {name: 'heat aging', number_prefix: 'C', parameters: [{name: 'time', range: {min: 1}}]} }); }); }); @@ -345,9 +441,10 @@ describe('/template', () => { const json = require('../test/db.json'); should(res.body).have.lengthOf(json.collections.measurement_templates.length); should(res.body).matchEach(measurement => { - should(measurement).have.only.keys('_id', 'name', 'parameters'); + should(measurement).have.only.keys('_id', 'name', 'version', 'parameters'); should(measurement).have.property('_id').be.type('string'); should(measurement).have.property('name').be.type('string'); + should(measurement).have.property('version').be.type('number'); should(measurement.parameters).matchEach(number => { should(number).have.only.keys('name', 'range'); should(number).have.property('name').be.type('string'); @@ -374,28 +471,28 @@ describe('/template', () => { }); }); - describe('GET /template/measurement/{name}', () => { + describe('GET /template/measurement/id', () => { it('returns the right measurement template', done => { TestHelper.request(server, done, { method: 'get', - url: '/template/measurement/spectrum', + url: '/template/measurement/300000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 200, - res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {}}]} + res: {_id: '300000000000000000000001', name: 'spectrum', version: 1, parameters: [{name: 'dpt', range: {}}]} }); }); it('rejects an API key', done => { TestHelper.request(server, done, { method: 'get', - url: '/template/measurement/spectrum', + url: '/template/measurement/300000000000000000000001', auth: {key: 'janedoe'}, httpStatus: 401 }); }); - it('rejects an unknown name', done => { + it('rejects an unknown id', done => { TestHelper.request(server, done, { method: 'get', - url: '/template/measurement/xxx', + url: '/template/measurement/000000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 404 }); @@ -403,7 +500,7 @@ describe('/template', () => { it('rejects unauthorized requests', done => { TestHelper.request(server, done, { method: 'get', - url: '/template/measurement/spectrum', + url: '/template/measurement/300000000000000000000001', httpStatus: 401 }); }); @@ -413,38 +510,49 @@ describe('/template', () => { it('returns the right measurement template', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/measurement/spectrum', + url: '/template/measurement/300000000000000000000001', auth: {basic: 'admin'}, httpStatus: 200, req: {}, - res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {}}]} + res: {_id: '300000000000000000000001', name: 'spectrum', version: 1, parameters: [{name: 'dpt', range: {}}]} }); }); it('keeps unchanged properties', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/measurement/spectrum', + url: '/template/measurement/300000000000000000000001', auth: {basic: 'admin'}, httpStatus: 200, req: {name: 'spectrum', parameters: [{name: 'dpt', range: {}}]}, - res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {}}]} + res: {_id: '300000000000000000000001', name: 'spectrum', version: 1, parameters: [{name: 'dpt', range: {type: 'array'}}]} + }); + }); + it('keeps only one unchanged property', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/300000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'spectrum'}, + res: {_id: '300000000000000000000001', name: 'spectrum', version: 1, parameters: [{name: 'dpt', range: {type: 'array'}}]} }); }); it('changes the given properties', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/measurement/spectrum', + url: '/template/measurement/300000000000000000000001', auth: {basic: 'admin'}, httpStatus: 200, req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]}, }).end((err, res) => { if (err) return done(err); should(res.body).be.eql({_id: '300000000000000000000001', name: 'IR spectrum', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]}); - TemplateMeasurementModel.find({name: 'IR spectrum'}).lean().exec((err, data:any) => { + TemplateMeasurementModel.findById('300000000000000000000001').lean().exec((err, data:any) => { if (err) return done(err); should(data).have.lengthOf(1); - should(data[0]).have.only.keys('_id', 'name', 'parameters', '__v'); + should(data[0]).have.only.keys('_id', 'name', 'version', 'parameters', '__v'); should(data[0]).have.property('name', 'IR spectrum'); + should(data[0]).have.property('version', 2); should(data[0]).have.property('parameters').have.lengthOf(1); should(data[0].parameters[0]).have.property('name', 'data point table'); should(data[0].parameters[0]).have.property('range'); @@ -454,132 +562,102 @@ describe('/template', () => { }); }); }); + it('allows changing only one property', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/300000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'IR spectrum'}, + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '300000000000000000000001', name: 'IR spectrum', parameters: [{name: 'dpt', range: {type: 'array'}}]}); + TemplateMeasurementModel.findById('300000000000000000000001').lean().exec((err, data:any) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data).have.only.keys('_id', 'name', 'version', 'parameters', '__v'); + should(data).have.property('name', 'IR spectrum'); + should(data).have.property('version', 2); + should(data).have.property('parameters').have.lengthOf(1); + should(data.parameters[0]).have.property('name', 'dpt'); + should(data.parameters[0]).have.property('range'); + should(data.parameters[0].range).have.property('type', 'array'); + done(); + }); + }); + }); it('supports values ranges', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/measurement/spectrum', + url: '/template/measurement/300000000000000000000001', auth: {basic: 'admin'}, httpStatus: 200, req: {parameters: [{name: 'dpt', range: {values: [1, 2, 5]}}]}, - res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {values: [1, 2, 5]}}]} + res: {_id: '300000000000000000000001', name: 'spectrum', version: 2, parameters: [{name: 'dpt', range: {values: [1, 2, 5]}}]} }); }); it('supports min max ranges', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/measurement/spectrum', + url: '/template/measurement/300000000000000000000001', auth: {basic: 'admin'}, httpStatus: 200, req: {parameters: [{name: 'dpt', range: {min: 0, max: 1000}}]}, - res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {min: 0, max: 1000}}]} + res: {_id: '300000000000000000000001', name: 'spectrum', version: 2, parameters: [{name: 'dpt', range: {min: 0, max: 1000}}]} + }); + }); + it('supports min max ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/300000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'dpt', range: {type: 'array'}}]}, + res: {_id: '300000000000000000000001', name: 'spectrum', version: 2, parameters: [{name: 'dpt', range: {type: 'array'}}]} }); }); it('supports empty ranges', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/measurement/kf', + url: '/template/measurement/300000000000000000000002', auth: {basic: 'admin'}, httpStatus: 200, req: {parameters: [{name: 'weight %', range: {}}]}, - res: {_id: '300000000000000000000002', name: 'kf', parameters: [{name: 'weight %', range: {}}]} + res: {_id: '300000000000000000000002', name: 'kf', version: 2, parameters: [{name: 'weight %', range: {}}]} }); }); - it('adds a new template for an unknown name', done => { + it('rejects not specified parameters', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/measurement/vz', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {name: 'vz', parameters: [{name: 'vz', range: {min: 1}}]} - }).end(err => { - if (err) return done(err); - TemplateMeasurementModel.find({name: 'vz'}).lean().exec((err, data:any) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.only.keys('_id', 'name', 'parameters', '__v'); - should(data[0]).have.property('name', 'vz'); - should(data[0]).have.property('parameters').have.lengthOf(1); - should(data[0].parameters[0]).have.property('name', 'vz'); - should(data[0].parameters[0]).have.property('range'); - should(data[0].parameters[0].range).have.property('min', 1); - done(); - }); - }); - }); - it('rejects a missing name for a new name', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/measurement/spectrum2', + url: '/template/measurement/300000000000000000000001', auth: {basic: 'admin'}, httpStatus: 400, - req: {parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]}, - res: {status: 'Invalid body format', details: '"name" is required'} + req: {parameters: [{name: 'dpt'}], range: {xx: 33}}, + res: {} }); }); - it('rejects missing parameters for a new name', done => { + it('rejects an invalid id', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/measurement/spectrum2', + url: '/template/measurement/3000000000h0000000000001', auth: {basic: 'admin'}, - httpStatus: 400, - req: {name: 'IR spectrum'}, - res: {status: 'Invalid body format', details: '"parameters" is required'} + httpStatus: 404, + req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]}, }); }); - it('rejects a missing parameter name for a new name', done => { + it('rejects an unknown id', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/measurement/spectrum2', + url: '/template/measurement/000000000000000000000001', auth: {basic: 'admin'}, - httpStatus: 400, - req: {name: 'IR spectrum', parameters: [{range: {min: 0, max: 1000}}]}, - res: {status: 'Invalid body format', details: '"parameters[0].name" is required'} - }); - }); - it('rejects a missing parameter range for a new name', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/measurement/spectrum2', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {name: 'IR spectrum', parameters: [{name: 'data point table'}]}, - res: {status: 'Invalid body format', details: '"parameters[0].range" is required'} - }); - }); - it('rejects a an invalid parameter range property for a new name', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/measurement/spectrum2', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {xx: 0}}]}, - res: {status: 'Invalid body format', details: '"parameters[0].range.xx" is not allowed'} - }); - }); - it('rejects already existing names', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/measurement/spectrum', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {name: 'kf', parameters: [{name: 'dpt', range: {min: 1}}]}, - res: {status: 'Template name already taken'} - }); - }); - it('rejects wrong properties', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/measurement/spectrum', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {parameters: [{name: 'dpt'}], xx: 33}, - res: {status: 'Invalid body format', details: '"parameters[0].range" is required'} + httpStatus: 404, + req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]}, }); }); it('rejects an API key', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/measurement/spectrum', + url: '/template/measurement/300000000000000000000001', auth: {key: 'admin'}, httpStatus: 401, req: {} @@ -588,7 +666,7 @@ describe('/template', () => { it('rejects requests from a write user', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/measurement/spectrum', + url: '/template/measurement/300000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 403, req: {} @@ -597,11 +675,141 @@ describe('/template', () => { it('rejects unauthorized requests', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/measurement/spectrum', + url: '/template/measurement/300000000000000000000001', httpStatus: 401, req: {} }); }); }); + + describe('POST /template/measurement/new', () => { + it('returns the right measurement template', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/measurement/new', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'vz', parameters: [{name: 'vz', range: {min: 1}}]} + }).end((err, res) => { + if (err) return done(err); + should(res.body).have.only.keys('_id', 'name', 'version', 'parameters'); + should(res.body).have.property('name', 'vz'); + should(res.body).have.property('version', 1); + should(res.body).have.property('parameters').have.lengthOf(1); + should(res.body.parameters[0]).have.property('name', 'vz'); + should(res.body.parameters[0]).have.property('range'); + should(res.body.parameters[0].range).have.property('min', 1); + }); + }); + it('stores the template', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/measurement/new', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'vz', parameters: [{name: 'vz', range: {min: 1}}]} + }).end(err => { + if (err) return done(err); + TemplateMeasurementModel.find({name: 'vz'}).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'version', 'parameters', '__v'); + should(data[0]).have.property('name', 'vz'); + should(data[0]).have.property('vaersion', 1); + should(data[0]).have.property('parameters').have.lengthOf(1); + should(data[0].parameters[0]).have.property('name', 'vz'); + should(data[0].parameters[0]).have.property('range'); + should(data[0].parameters[0].range).have.property('min', 1); + done(); + }); + }); + }); + it('rejects a missing name', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/measurement/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]}, + res: {status: 'Invalid body format', details: '"name" is required'} + }); + }); + it('rejects missing parameters', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/measurement/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'IR spectrum'}, + res: {status: 'Invalid body format', details: '"parameters" is required'} + }); + }); + it('rejects a missing parameter name', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/measurement/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'IR spectrum', parameters: [{range: {min: 0, max: 1000}}]}, + res: {status: 'Invalid body format', details: '"parameters[0].name" is required'} + }); + }); + it('rejects a missing parameter range', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/measurement/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'IR spectrum', parameters: [{name: 'data point table'}]}, + res: {status: 'Invalid body format', details: '"parameters[0].range" is required'} + }); + }); + it('rejects a an invalid parameter range property', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/measurement/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {xx: 0}}]}, + res: {status: 'Invalid body format', details: '"parameters[0].range.xx" is not allowed'} + }); + }); + it('rejects wrong properties', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/measurement/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {}}], xx: 35}, + res: {} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/measurement/new', + auth: {key: 'admin'}, + httpStatus: 401, + req: {name: 'vz', parameters: [{name: 'vz', range: {min: 1}}]} + }); + }); + it('rejects requests from a write user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/measurement/new', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {name: 'vz', parameters: [{name: 'vz', range: {min: 1}}]} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/measurement/new', + httpStatus: 401, + req: {name: 'vz', parameters: [{name: 'vz', range: {min: 1}}]} + }); + }); + }); }); }); \ No newline at end of file diff --git a/src/routes/template.ts b/src/routes/template.ts index 55088f9..6c3212b 100644 --- a/src/routes/template.ts +++ b/src/routes/template.ts @@ -5,6 +5,7 @@ import TemplateValidate from './validate/template'; import TemplateTreatmentModel from '../models/treatment_template'; import TemplateMeasurementModel from '../models/measurement_template'; import res400 from './validate/res400'; +import IdValidate from './validate/id'; // TODO: remove f() for await @@ -13,21 +14,20 @@ const router = express.Router(); router.get('/template/:collection(measurements|treatments)', (req, res, next) => { if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; - (req.params.collection === 'treatments' ? TemplateTreatmentModel : TemplateMeasurementModel) - .find({}).lean().exec((err, data) => { + req.params.collection = req.params.collection.replace(/s$/g, ''); + model(req).find({}).lean().exec((err, data) => { if (err) next (err); - res.json(_.compact(data.map(e => TemplateValidate.output(e)))); // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => TemplateValidate.output(e, req.params.collection)))); // validate all and filter null values from validation errors }); }); -router.get('/template/:collection(measurement|treatment)/:name', (req, res, next) => { +router.get('/template/:collection(measurement|treatment)/' + IdValidate.parameter(), (req, res, next) => { if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; - (req.params.collection === 'treatment' ? TemplateTreatmentModel : TemplateMeasurementModel) - .findOne({name: req.params.name}).lean().exec((err, data) => { + model(req).findById(req.params.id).lean().exec((err, data) => { if (err) next (err); if (data) { - res.json(TemplateValidate.output(data)); + res.json(TemplateValidate.output(data, req.params.collection)); } else { res.status(404).json({status: 'Not found'}); @@ -35,19 +35,18 @@ router.get('/template/:collection(measurement|treatment)/:name', (req, res, next }); }); -router.put('/template/:collection(measurement|treatment)/:name', (req, res, next) => { +router.put('/template/:collection(measurement|treatment)/' + IdValidate.parameter(), (req, res, next) => { if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; - const collectionModel = req.params.collection === 'treatment' ? TemplateTreatmentModel : TemplateMeasurementModel; - collectionModel.findOne({name: req.params.name}).lean().exec((err, data) => { + model(req).findOne({name: req.params.name}).lean().exec((err, data) => { if (err) next (err); const templateState = data? 'change': 'new'; - const {error, value: template} = TemplateValidate.input(req.body, templateState); + const {error, value: template} = TemplateValidate.input(req.body, templateState, req.params.collection); if (error) return res400(error, res); if (template.hasOwnProperty('name') && template.name !== req.params.name) { - collectionModel.find({name: template.name}).lean().exec((err, data) => { + model(req).find({name: template.name}).lean().exec((err, data) => { if (err) next (err); if (data.length > 0) { res.status(400).json({status: 'Template name already taken'}); @@ -63,9 +62,9 @@ router.put('/template/:collection(measurement|treatment)/:name', (req, res, next } function f() { // to resolve async - collectionModel.findOneAndUpdate({name: req.params.name}, template, {new: true, upsert: true}).lean().exec((err, data) => { + model(req).findOneAndUpdate({name: req.params.name}, template, {new: true, upsert: true}).lean().exec((err, data) => { if (err) return next(err); - res.json(TemplateValidate.output(data)); + res.json(TemplateValidate.output(data, req.params.collection)); }); } }); @@ -87,4 +86,9 @@ router.put('/template/:collection(measurement|treatment)/:name', (req, res, next // }); -module.exports = router; \ No newline at end of file +module.exports = router; + + +function model (req) { + return req.params.collection === 'treatment' ? TemplateTreatmentModel : TemplateMeasurementModel; +} \ No newline at end of file diff --git a/src/routes/validate/template.ts b/src/routes/validate/template.ts index a279dce..440c515 100644 --- a/src/routes/validate/template.ts +++ b/src/routes/validate/template.ts @@ -6,6 +6,13 @@ export default class TemplateValidate { name: joi.string() .max(128), + version: joi.number() + .min(1), + + number_prefix: joi.string() + .min(1) + .max(16), + parameters: joi.array() .min(1) .items( @@ -29,31 +36,63 @@ export default class TemplateValidate { ) }; - static input (data, param) { // validate data, param: new(everything required)/change(available attributes are validated) + static input (data, param, template) { // validate data, param: new(everything required)/change(available attributes are validated) if (param === 'new') { - return joi.object({ - name: this.template.name.required(), - parameters: this.template.parameters.required() - }).validate(data); + if (template === 'treatment') { + return joi.object({ + name: this.template.name.required(), + number_prefix: this.template.number_prefix.required(), + parameters: this.template.parameters.required() + }).validate(data); + } + else { + return joi.object({ + name: this.template.name.required(), + parameters: this.template.parameters.required() + }).validate(data); + } } else if (param === 'change') { - return joi.object({ - name: this.template.name, - parameters: this.template.parameters - }).validate(data); + if (template === 'treatment') { + return joi.object({ + name: this.template.name, + number_prefix: this.template.number_prefix, + parameters: this.template.parameters + }).validate(data); + } + else { + return joi.object({ + name: this.template.name, + parameters: this.template.parameters + }).validate(data); + } } else { return{error: 'No parameter specified!', value: {}}; } } - static output (data) { // validate output from database for needed properties, strip everything else + static output (data, template) { // validate output from database for needed properties, strip everything else data = IdValidate.stringify(data); - const {value, error} = joi.object({ - _id: IdValidate.get(), - name: this.template.name, - parameters: this.template.parameters - }).validate(data, {stripUnknown: true}); + let joiObject; + if (template === 'treatment') { + joiObject = { + _id: IdValidate.get(), + name: this.template.name, + version: this.template.version, + number_prefix: this.template.number_prefix, + parameters: this.template.parameters + }; + } + else { + joiObject = { + _id: IdValidate.get(), + name: this.template.name, + version: this.template.version, + parameters: this.template.parameters + }; + } + const {value, error} = joi.object(joiObject).validate(data, {stripUnknown: true}); return error !== undefined? null : value; } } \ No newline at end of file