only allowed latest template version and allowed admin to set sample number
This commit is contained in:
parent
0fcb902499
commit
74080d0902
@ -170,7 +170,7 @@
|
||||
/sample/new:
|
||||
post:
|
||||
summary: add sample
|
||||
description: 'Auth: basic, levels: write, maintain, dev, admin'
|
||||
description: 'Auth: basic, levels: write, maintain, dev, admin. Number property is only for admin when adding existing samples'
|
||||
x-doc: 'Adds status: 0 automatically'
|
||||
tags:
|
||||
- /sample
|
||||
@ -181,7 +181,12 @@
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: 'api.yaml#/components/schemas/Sample'
|
||||
allOf:
|
||||
- $ref: 'api.yaml#/components/schemas/Sample'
|
||||
properties:
|
||||
number:
|
||||
type: string
|
||||
readOnly: false
|
||||
responses:
|
||||
200:
|
||||
description: samples details
|
||||
|
@ -69,6 +69,7 @@ Sample:
|
||||
relation:
|
||||
type: string
|
||||
example: part to this sample
|
||||
|
||||
SampleDetail:
|
||||
allOf:
|
||||
- $ref: 'api.yaml#/components/schemas/_Id'
|
||||
|
@ -7,14 +7,12 @@ import db from './db';
|
||||
|
||||
// TODO: changelog
|
||||
// TODO: check executing index.js/move everything needed into dist
|
||||
// TODO: One condition per sample
|
||||
// TODO: validation: VZ, Humidity: min/max value, DPT: filename
|
||||
// TODO: condition values not needed on initial add
|
||||
// TODO: add multiple samples at once
|
||||
// TODO: coverage
|
||||
// TODO: add multiple samples at once (only GUI)
|
||||
// TODO: think about the display of deleted/new samples and validation in data and UI
|
||||
// TODO: improve error coverage
|
||||
// TODO: guess properties from material name in UI
|
||||
// TODO: mongodb user
|
||||
|
||||
// tell if server is running in debug or production environment
|
||||
console.info(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT =====');
|
||||
|
@ -586,14 +586,24 @@ describe('/measurement', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('rejects no values', done => {
|
||||
TestHelper.request(server, done, {
|
||||
method: 'post',
|
||||
url: '/measurement/new',
|
||||
auth: {basic: 'janedoe'},
|
||||
httpStatus: 400,
|
||||
req: {sample_id: '400000000000000000000001', values: {}, measurement_template: '300000000000000000000002'},
|
||||
res: {status: 'At least one value is required'}
|
||||
});
|
||||
});
|
||||
it('rejects a value not in the value range', done => {
|
||||
TestHelper.request(server, done, {
|
||||
method: 'post',
|
||||
url: '/measurement/new',
|
||||
auth: {basic: 'janedoe'},
|
||||
httpStatus: 400,
|
||||
req: {sample_id: '400000000000000000000001', values: {val1: 4}, measurement_template: '300000000000000000000003'},
|
||||
res: {status: 'Invalid body format', details: '"val1" must be one of [1, 2, 3, null]'}
|
||||
req: {sample_id: '400000000000000000000001', values: {val2: 5}, measurement_template: '300000000000000000000004'},
|
||||
res: {status: 'Invalid body format', details: '"val2" must be one of [1, 2, 3, 4, null]'}
|
||||
});
|
||||
});
|
||||
it('rejects a value below minimum range', done => {
|
||||
@ -664,6 +674,16 @@ describe('/measurement', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('rejects an old version of a measurement template', done => {
|
||||
TestHelper.request(server, done, {
|
||||
method: 'post',
|
||||
url: '/measurement/new',
|
||||
auth: {basic: 'janedoe'},
|
||||
httpStatus: 400,
|
||||
req: {sample_id: '400000000000000000000001', values: {val1: 2}, measurement_template: '300000000000000000000003'},
|
||||
res: {status: 'Old template version not allowed'}
|
||||
});
|
||||
});
|
||||
it('rejects an API key', done => {
|
||||
TestHelper.request(server, done, {
|
||||
method: 'post',
|
||||
|
@ -130,6 +130,14 @@ async function templateCheck (measurement, param, res, next) { // validate meas
|
||||
|
||||
// fill not given values for new measurements
|
||||
if (param === 'new') {
|
||||
// get all template versions and check if given is latest
|
||||
const templateVersions = await MeasurementTemplateModel.find({first_id: templateData.first_id}).sort({version: -1}).lean().exec().catch(err => next(err)) as any;
|
||||
if (templateVersions instanceof Error) return false;
|
||||
if (measurement.measurement_template !== templateVersions[0]._id.toString()) { // template not latest
|
||||
res.status(400).json({status: 'Old template version not allowed'});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Object.keys(measurement.values).length === 0) {
|
||||
res.status(400).json({status: 'At least one value is required'});
|
||||
return false
|
||||
|
@ -454,6 +454,16 @@ describe('/sample', () => {
|
||||
res: {status: 'Color not available for material'}
|
||||
});
|
||||
});
|
||||
it('rejects an undefined color for the same material', done => {
|
||||
TestHelper.request(server, done, {
|
||||
method: 'put',
|
||||
url: '/sample/400000000000000000000001',
|
||||
auth: {basic: 'janedoe'},
|
||||
httpStatus: 400,
|
||||
req: {type: 'part', color: 'signalviolet', batch: '114531', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}},
|
||||
res: {status: 'Color not available for material'}
|
||||
});
|
||||
});
|
||||
it('rejects an unknown material id', done => {
|
||||
TestHelper.request(server, done, {
|
||||
method: 'put',
|
||||
@ -573,6 +583,26 @@ describe('/sample', () => {
|
||||
res: {_id: '400000000000000000000006', number: 'Rng36', type: 'granulate', color: 'black', batch: '', condition: {}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'}
|
||||
});
|
||||
});
|
||||
it('rejects an old version of a condition template', done => {
|
||||
TestHelper.request(server, done, {
|
||||
method: 'put',
|
||||
url: '/sample/400000000000000000000001',
|
||||
auth: {basic: 'janedoe'},
|
||||
httpStatus: 400,
|
||||
req: {condition: {p1: 36, condition_template: '200000000000000000000004'}},
|
||||
res: {status: 'Old template version not allowed'}
|
||||
});
|
||||
});
|
||||
it('allows keeping an old version of a condition template', done => {
|
||||
TestHelper.request(server, done, {
|
||||
method: 'put',
|
||||
url: '/sample/400000000000000000000004',
|
||||
auth: {basic: 'admin'},
|
||||
httpStatus: 200,
|
||||
req: {condition: {p1: 36, condition_template: '200000000000000000000004'}},
|
||||
res: {_id: '400000000000000000000004', number: '32', type: 'granulate', color: 'black', batch: '1653000308', condition: {p1: 36, condition_template: '200000000000000000000004'}, material_id: '100000000000000000000005', note_id: '500000000000000000000003', user_id: '000000000000000000000003'}
|
||||
});
|
||||
});
|
||||
it('rejects an changing back to an empty condition', done => {
|
||||
TestHelper.request(server, done, {
|
||||
method: 'put',
|
||||
@ -1108,7 +1138,7 @@ describe('/sample', () => {
|
||||
res: {status: 'Material not available'}
|
||||
});
|
||||
});
|
||||
it('rejects a sample number', done => {
|
||||
it('rejects a sample number for a write user', done => {
|
||||
TestHelper.request(server, done, {
|
||||
method: 'post',
|
||||
url: '/sample/new',
|
||||
@ -1118,6 +1148,38 @@ describe('/sample', () => {
|
||||
res: {status: 'Invalid body format', details: '"number" is not allowed'}
|
||||
});
|
||||
});
|
||||
it('allows a sample number for an admin user', done => {
|
||||
TestHelper.request(server, done, {
|
||||
method: 'post',
|
||||
url: '/sample/new',
|
||||
auth: {basic: 'admin'},
|
||||
httpStatus: 200,
|
||||
req: {number: 'Rng34', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}},
|
||||
}).end((err, res) => {
|
||||
if (err) return done (err);
|
||||
should(res.body).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'condition', 'material_id', 'note_id', 'user_id');
|
||||
should(res.body).have.property('_id').be.type('string');
|
||||
should(res.body).have.property('number', 'Rng34');
|
||||
should(res.body).have.property('color', 'black');
|
||||
should(res.body).have.property('type', 'granulate');
|
||||
should(res.body).have.property('batch', '1560237365');
|
||||
should(res.body).have.property('condition', {});
|
||||
should(res.body).have.property('material_id', '100000000000000000000001');
|
||||
should(res.body).have.property('note_id').be.type('string');
|
||||
should(res.body).have.property('user_id', '000000000000000000000003');
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('rejects an existing sample number for an admin user', done => {
|
||||
TestHelper.request(server, done, {
|
||||
method: 'post',
|
||||
url: '/sample/new',
|
||||
auth: {basic: 'admin'},
|
||||
httpStatus: 400,
|
||||
req: {number: 'Rng33', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}},
|
||||
res: {status: 'Sample number already taken'}
|
||||
});
|
||||
});
|
||||
it('rejects an invalid sample reference', done => {
|
||||
TestHelper.request(server, done, {
|
||||
method: 'post',
|
||||
@ -1208,6 +1270,16 @@ describe('/sample', () => {
|
||||
res: {status: 'Condition template not available'}
|
||||
});
|
||||
});
|
||||
it('rejects an old version of a condition template', done => {
|
||||
TestHelper.request(server, done, {
|
||||
method: 'post',
|
||||
url: '/sample/new',
|
||||
auth: {basic: 'janedoe'},
|
||||
httpStatus: 400,
|
||||
req: {color: 'black', type: 'granulate', batch: '1560237365', condition: {p1: 36, condition_template: '200000000000000000000004'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}},
|
||||
res: {status: 'Old template version not allowed'}
|
||||
});
|
||||
});
|
||||
it('rejects a missing color', done => {
|
||||
TestHelper.request(server, done, {
|
||||
method: 'post',
|
||||
|
@ -90,7 +90,7 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
|
||||
}
|
||||
|
||||
if (sample.hasOwnProperty('condition') && !(_.isEmpty(sample.condition) && _.isEmpty(sampleData.condition))) { // do not execute check if condition is and was empty
|
||||
if (!await conditionCheck(sample.condition, 'change', res, next)) return;
|
||||
if (!await conditionCheck(sample.condition, 'change', res, next, sampleData.condition.condition_template.toString() !== sample.condition.condition_template)) return;
|
||||
}
|
||||
|
||||
if (sample.hasOwnProperty('notes')) {
|
||||
@ -217,7 +217,7 @@ router.post('/sample/new', async (req, res, next) => {
|
||||
req.body.condition = {};
|
||||
}
|
||||
|
||||
const {error, value: sample} = SampleValidate.input(req.body, 'new');
|
||||
const {error, value: sample} = SampleValidate.input(req.body, 'new' + (req.authDetails.level === 'admin' ? '-admin' : ''));
|
||||
if (error) return res400(error, res);
|
||||
|
||||
if (!await materialCheck(sample, res, next)) return;
|
||||
@ -232,7 +232,12 @@ router.post('/sample/new', async (req, res, next) => {
|
||||
}
|
||||
|
||||
sample.status = globals.status.new; // set status to new
|
||||
sample.number = await numberGenerate(sample, req, res, next);
|
||||
if (sample.hasOwnProperty('number')) {
|
||||
if (!await numberCheck(sample, res, next)) return;
|
||||
}
|
||||
else {
|
||||
sample.number = await numberGenerate(sample, req, res, next);
|
||||
}
|
||||
if (!sample.number) return;
|
||||
|
||||
await new NoteModel(sample.notes).save((err, data) => { // save notes
|
||||
@ -272,6 +277,15 @@ async function numberGenerate (sample, req, res, next) { // generate number in
|
||||
return req.authDetails.location + (sampleData ? Number(sampleData.number.replace(/[^0-9]+/g, '')) + 1 : 1);
|
||||
}
|
||||
|
||||
async function numberCheck(sample, res, next) {
|
||||
const sampleData = await SampleModel.findOne({number: sample.number}).lean().exec().catch(err => {next(err); return false;});
|
||||
if (sampleData) { // found entry with sample number
|
||||
res.status(400).json({status: 'Sample number already taken'});
|
||||
return false
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function materialCheck (sample, res, next, id = sample.material_id) { // validate material_id and color, returns false if invalid
|
||||
const materialData = await MaterialModel.findById(id).lean().exec().catch(err => next(err)) as any;
|
||||
if (materialData instanceof Error) return false;
|
||||
@ -286,7 +300,7 @@ async function materialCheck (sample, res, next, id = sample.material_id) { //
|
||||
return true;
|
||||
}
|
||||
|
||||
async function conditionCheck (condition, param, res, next) { // validate treatment template, returns false if invalid, otherwise template data
|
||||
async function conditionCheck (condition, param, res, next, checkVersion = true) { // validate treatment template, returns false if invalid, otherwise template data
|
||||
if (!condition.condition_template || !IdValidate.valid(condition.condition_template)) { // template id not found
|
||||
res.status(400).json({status: 'Condition template not available'});
|
||||
return false;
|
||||
@ -298,6 +312,16 @@ async function conditionCheck (condition, param, res, next) { // validate treat
|
||||
return false;
|
||||
}
|
||||
|
||||
if (checkVersion) {
|
||||
// get all template versions and check if given is latest
|
||||
const conditionVersions = await ConditionTemplateModel.find({first_id: conditionData.first_id}).sort({version: -1}).lean().exec().catch(err => next(err)) as any;
|
||||
if (conditionVersions instanceof Error) return false;
|
||||
if (condition.condition_template !== conditionVersions[0]._id.toString()) { // template not latest
|
||||
res.status(400).json({status: 'Old template version not allowed'});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// validate parameters
|
||||
const {error, value: ignore} = ParametersValidate.input(_.omit(condition, 'condition_template'), conditionData.parameters, param);
|
||||
if (error) {res400(error, res); return false;}
|
||||
|
@ -4,7 +4,6 @@ import TemplateConditionModel from '../models/condition_template';
|
||||
import TemplateMeasurementModel from '../models/measurement_template';
|
||||
import TestHelper from "../test/helper";
|
||||
|
||||
// TODO: do not allow usage of old templates for new samples
|
||||
|
||||
describe('/template', () => {
|
||||
let server;
|
||||
@ -644,7 +643,7 @@ describe('/template', () => {
|
||||
req: {parameters: [{name: 'weight %', range: {}}]}
|
||||
}).end((err, res) => {
|
||||
if (err) return done(err);
|
||||
should(_.omit(res.body, '_id')).be.eql({name: 'kf', version: 3, parameters: [{name: 'weight %', range: {}}]});
|
||||
should(_.omit(res.body, '_id')).be.eql({name: 'kf', version: 2, parameters: [{name: 'weight %', range: {}}]});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -67,6 +67,17 @@ export default class SampleValidate {
|
||||
notes: this.sample.notes,
|
||||
}).validate(data);
|
||||
}
|
||||
else if (param === 'new-admin') {
|
||||
return Joi.object({
|
||||
number: this.sample.number.required(),
|
||||
color: this.sample.color.required(),
|
||||
type: this.sample.type.required(),
|
||||
batch: this.sample.batch.required(),
|
||||
condition: this.sample.condition.required(),
|
||||
material_id: IdValidate.get().required(),
|
||||
notes: this.sample.notes.required()
|
||||
}).validate(data);
|
||||
}
|
||||
else {
|
||||
return{error: 'No parameter specified!', value: {}};
|
||||
}
|
||||
|
@ -59,9 +59,8 @@
|
||||
"color": "black",
|
||||
"batch": "1653000308",
|
||||
"condition": {
|
||||
"material": "hot air",
|
||||
"weeks": 5,
|
||||
"condition_template": {"$oid":"200000000000000000000001"}
|
||||
"p1": 44,
|
||||
"condition_template": {"$oid":"200000000000000000000004"}
|
||||
},
|
||||
"material_id": {"$oid":"100000000000000000000005"},
|
||||
"note_id": {"$oid":"500000000000000000000003"},
|
||||
@ -447,24 +446,37 @@
|
||||
"__v": 0
|
||||
},
|
||||
{
|
||||
"_id": {"$oid":"200000000000000000000002"},
|
||||
"first_id": {"$oid":"200000000000000000000001"},
|
||||
"name": "heat treatment 2",
|
||||
"version": 2,
|
||||
"_id": {"$oid":"200000000000000000000003"},
|
||||
"first_id": {"$oid":"200000000000000000000003"},
|
||||
"name": "raw material",
|
||||
"version": 1,
|
||||
"parameters": [
|
||||
],
|
||||
"__v": 0
|
||||
},
|
||||
{
|
||||
"_id": {"$oid":"200000000000000000000004"},
|
||||
"first_id": {"$oid":"200000000000000000000004"},
|
||||
"name": "old condition",
|
||||
"version": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"name": "material",
|
||||
"name": "p1",
|
||||
"range": {}
|
||||
}
|
||||
],
|
||||
"__v": 0
|
||||
},
|
||||
{
|
||||
"_id": {"$oid":"200000000000000000000003"},
|
||||
"first_id": {"$oid":"200000000000000000000003"},
|
||||
"name": "raw material",
|
||||
"version": 1,
|
||||
"_id": {"$oid":"200000000000000000000005"},
|
||||
"first_id": {"$oid":"200000000000000000000004"},
|
||||
"name": "new condition",
|
||||
"version": 2,
|
||||
"parameters": [
|
||||
{
|
||||
"name": "p11",
|
||||
"range": {}
|
||||
}
|
||||
],
|
||||
"__v": 0
|
||||
}
|
||||
@ -487,9 +499,9 @@
|
||||
},
|
||||
{
|
||||
"_id": {"$oid":"300000000000000000000002"},
|
||||
"first_id": {"$oid":"300000000000000000000001"},
|
||||
"first_id": {"$oid":"300000000000000000000002"},
|
||||
"name": "kf",
|
||||
"version": 2,
|
||||
"version": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"name": "weight %",
|
||||
@ -522,6 +534,21 @@
|
||||
}
|
||||
],
|
||||
"__v": 0
|
||||
},
|
||||
{
|
||||
"_id": {"$oid":"300000000000000000000004"},
|
||||
"first_id": {"$oid":"300000000000000000000003"},
|
||||
"name": "mt 31",
|
||||
"version": 2,
|
||||
"parameters": [
|
||||
{
|
||||
"name": "val2",
|
||||
"range": {
|
||||
"values": [1,2,3,4]
|
||||
}
|
||||
}
|
||||
],
|
||||
"__v": 0
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
|
Reference in New Issue
Block a user