diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 0000000..54163ef
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,11 @@
+
+
+
+
+ mongo
+ true
+ com.dbschema.MongoJdbcDriver
+ mongodb://localhost:27017
+
+
+
\ No newline at end of file
diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml
new file mode 100644
index 0000000..ad4eaf6
--- /dev/null
+++ b/.idea/dbnavigator.xml
@@ -0,0 +1,458 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/api/material.yaml b/api/material.yaml
index a3b80da..5e8bc13 100644
--- a/api/material.yaml
+++ b/api/material.yaml
@@ -79,6 +79,8 @@
responses:
200:
$ref: 'api.yaml#/components/responses/Ok'
+ 400:
+ $ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
diff --git a/api/sample.yaml b/api/sample.yaml
index 32bb6ed..e911d9c 100644
--- a/api/sample.yaml
+++ b/api/sample.yaml
@@ -41,8 +41,8 @@
500:
$ref: 'api.yaml#/components/responses/500'
put:
- summary: TODO change sample
- description: 'Auth: basic, levels: write, maintain, dev, admin'
+ summary: change sample
+ description: 'Auth: basic, levels: write, maintain, dev, admin, only maintain and admin are allowed to edit samples created by another user'
tags:
- /sample
security:
@@ -59,7 +59,7 @@
content:
application/json:
schema:
- $ref: 'api.yaml#/components/schemas/SampleDetail'
+ $ref: 'api.yaml#/components/schemas/SampleRefs'
400:
$ref: 'api.yaml#/components/responses/400'
401:
@@ -71,8 +71,8 @@
500:
$ref: 'api.yaml#/components/responses/500'
delete:
- summary: TODO delete sample
- description: 'Auth: basic, levels: write, maintain, dev, admin'
+ summary: delete sample
+ description: 'Auth: basic, levels: write, maintain, dev, admin, only maintain and admin are allowed to edit samples created by another user'
tags:
- /sample
security:
@@ -123,7 +123,7 @@
/sample/notes/fields:
get:
- summary: TODO list all existing field names for custom notes fields
+ summary: list all existing field names for custom notes fields
description: 'Auth: all, levels: read, write, maintain, dev, admin'
tags:
- /sample
diff --git a/package.json b/package.json
index d3f9e63..9a69ea2 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,8 @@
"tsc": "tsc",
"test": "mocha dist/**/**.spec.js",
"start": "tsc && node dist/index.js || exit 1",
- "dev": "nodemon -e ts,yaml --exec \"npm run start\""
+ "dev": "nodemon -e ts,yaml --exec \"npm run start\"",
+ "loadDev": "node dist/test/loadDev.js"
},
"keywords": [],
"author": "",
diff --git a/src/db.ts b/src/db.ts
index f188468..89c3183 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -42,19 +42,19 @@ export default class db {
});
mongoose.connection.on('error', console.error.bind(console, 'connection error:'));
mongoose.connection.on('disconnected', () => { // reset state on disconnect
- console.log('Database disconnected');
+ console.info('Database disconnected');
this.state.db = 0;
done();
});
process.on('SIGINT', () => { // close connection when app is terminated
mongoose.connection.close(() => {
- console.log('Mongoose default connection disconnected through app termination');
+ console.info('Mongoose default connection disconnected through app termination');
process.exit(0);
});
});
mongoose.connection.once('open', () => {
mongoose.set('useFindAndModify', false);
- console.log(process.env.NODE_ENV === 'test' ? '' : `Connected to ${connectionString}`);
+ console.info(process.env.NODE_ENV === 'test' ? '' : `Connected to ${connectionString}`);
this.state.db = mongoose.connection;
done();
});
@@ -90,13 +90,7 @@ export default class db {
let loadCounter = 0; // count number of loaded collections to know when to return done()
Object.keys(json.collections).forEach(collectionName => { // create each collection
- for(let i in json.collections[collectionName]) { // convert $oid fields to actual ObjectIds
- Object.keys(json.collections[collectionName][i]).forEach(key => {
- if (json.collections[collectionName][i][key] !== null && json.collections[collectionName][i][key].hasOwnProperty('$oid')) {
- json.collections[collectionName][i][key] = mongoose.Types.ObjectId(json.collections[collectionName][i][key].$oid);
- }
- })
- }
+ json.collections[collectionName] = this.oidResolve(json.collections[collectionName]);
this.state.db.createCollection(collectionName, (err, collection) => {
collection.insertMany(json.collections[collectionName], () => { // insert JSON data
if (++ loadCounter >= Object.keys(json.collections).length) { // all collections loaded
@@ -106,4 +100,16 @@ export default class db {
});
});
}
+
+ private static oidResolve (object: any) { // resolve $oid fields to actual ObjectIds recursively
+ Object.keys(object).forEach(key => {
+ if (object[key] !== null && object[key].hasOwnProperty('$oid')) {
+ object[key] = mongoose.Types.ObjectId(object[key].$oid);
+ }
+ else if (typeof object[key] === 'object' && object[key] !== null) {
+ object[key] = this.oidResolve(object[key]);
+ }
+ });
+ return object;
+ }
};
\ No newline at end of file
diff --git a/src/helpers/mail.ts b/src/helpers/mail.ts
index 949d243..792f35f 100644
--- a/src/helpers/mail.ts
+++ b/src/helpers/mail.ts
@@ -30,7 +30,7 @@ export default (mailAddress, subject, content, f) => { // callback, executed em
});
}
else if (process.env.NODE_ENV === 'test') {
- console.log('Sending mail to ' + mailAddress + ': -- ' + subject + ' -- ' + content);
+ console.info('Sending mail to ' + mailAddress + ': -- ' + subject + ' -- ' + content);
f();
}
else { // dev
diff --git a/src/index.ts b/src/index.ts
index 63ca19e..3a87996 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -8,7 +8,7 @@ import db from './db';
// tell if server is running in debug or production environment
-console.log(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT =====');
+console.info(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT =====');
// mongodb connection
@@ -75,7 +75,7 @@ app.use((err, req, res, ignore) => { // internal server error handling
// hook up server to port
const server = app.listen(port, () => {
- console.log(process.env.NODE_ENV === 'test' ? '' : `Listening on http://localhost:${port}`);
+ console.info(process.env.NODE_ENV === 'test' ? '' : `Listening on http://localhost:${port}`);
});
module.exports = server;
\ No newline at end of file
diff --git a/src/routes/material.spec.ts b/src/routes/material.spec.ts
index 7b84c08..dbc646b 100644
--- a/src/routes/material.spec.ts
+++ b/src/routes/material.spec.ts
@@ -1,6 +1,6 @@
import should from 'should/as-function';
import MaterialModel from '../models/material';
-import TestHelper from "../helpers/test";
+import TestHelper from "../test/helper";
describe('/material', () => {
@@ -171,14 +171,54 @@ describe('/material', () => {
res: {status: 'Material name already taken'}
});
});
- it('rejects wrong material properties', done => {
+ it('rejects a wrong mineral property', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/material/100000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 400,
- req: {mineral: 'x', glass_fiber: 'x', carbon_fiber: 'x', numbers: [{colorxx: 'black', number: 'xxx'}]},
- res: {status: 'Invalid body format'}
+ req: {mineral: 'x'},
+ res: {status: 'Invalid body format', details: '"mineral" must be a number'}
+ });
+ });
+ it('rejects a wrong glass_fiber property', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/material/100000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {glass_fiber: 'x'},
+ res: {status: 'Invalid body format', details: '"glass_fiber" must be a number'}
+ });
+ });
+ it('rejects a wrong carbon_fiber property', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/material/100000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {carbon_fiber: 'x'},
+ res: {status: 'Invalid body format', details: '"carbon_fiber" must be a number'}
+ });
+ });
+ it('rejects a wrong color name property', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/material/100000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {numbers: [{colorxx: 'black', number: 55}]},
+ res: {status: 'Invalid body format', details: '"numbers[0].color" is required'}
+ });
+ });
+ it('rejects a wrong color number property', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/material/100000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {numbers: [{color: 'black', number: 'xxx'}]},
+ res: {status: 'Invalid body format', details: '"numbers[0].number" must be a number'}
});
});
it('rejects an invalid id', done => {
@@ -231,20 +271,28 @@ describe('/material', () => {
it('deletes the material', done => {
TestHelper.request(server, done, {
method: 'delete',
- url: '/material/100000000000000000000001',
+ url: '/material/100000000000000000000002',
auth: {basic: 'janedoe'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
should(res.body).be.eql({status: 'OK'});
- MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => {
+ MaterialModel.findById('100000000000000000000002').lean().exec((err, data) => {
if (err) return done(err);
should(data).be.null();
done();
});
});
});
- it('rejects deleting a material referenced by samples');
+ it('rejects deleting a material referenced by samples', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/material/100000000000000000000004',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ res: {status: 'Material still in use'}
+ })
+ });
it('rejects an invalid id', done => {
TestHelper.request(server, done, {
method: 'delete',
@@ -347,24 +395,94 @@ describe('/material', () => {
res: {status: 'Material name already taken'}
});
});
- it('rejects wrong material properties', done => {
+ it('rejects a missing name', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/material/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
- req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 'x', glass_fiber: 'x', carbon_fiber: 'x', numbers: [{colorxx: 'black', number: 'xxx'}]},
- res: {status: 'Invalid body format'}
+ req: {supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]},
+ res: {status: 'Invalid body format', details: '"name" is required'}
});
});
- it('rejects incomplete material properties', done => {
+ it('rejects a missing supplier', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/material/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
- req: {name: 'Crastin CE 2510'},
- res: {status: 'Invalid body format'}
+ req: {name: 'Crastin CE 2510', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]},
+ res: {status: 'Invalid body format', details: '"supplier" is required'}
+ });
+ });
+ it('rejects a missing group', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/material/new',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {name: 'Crastin CE 2510', supplier: 'Du Pont', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]},
+ res: {status: 'Invalid body format', details: '"group" is required'}
+ });
+ });
+ it('rejects a missing mineral property', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/material/new',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]},
+ res: {status: 'Invalid body format', details: '"mineral" is required'}
+ });
+ });
+ it('rejects a missing glass_fiber property', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/material/new',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]},
+ res: {status: 'Invalid body format', details: '"glass_fiber" is required'}
+ });
+ });
+ it('rejects a missing carbon_fiber property', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/material/new',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, numbers: [{color: 'black', number: 5515798402}]},
+ res: {status: 'Invalid body format', details: '"carbon_fiber" is required'}
+ });
+ });
+ it('rejects a missing numbers array', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/material/new',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0},
+ res: {status: 'Invalid body format', details: '"numbers" is required'}
+ });
+ });
+ it('rejects a missing color name', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/material/new',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{number: 5515798402}]},
+ res: {status: 'Invalid body format', details: '"numbers[0].color" is required'}
+ });
+ });
+ it('rejects a missing color number', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/material/new',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black'}]},
+ res: {status: 'Invalid body format', details: '"numbers[0].number" is required'}
});
});
it('rejects an API key', done => {
diff --git a/src/routes/material.ts b/src/routes/material.ts
index 5628fa6..292f02f 100644
--- a/src/routes/material.ts
+++ b/src/routes/material.ts
@@ -2,7 +2,10 @@ import express from 'express';
import MaterialValidate from './validate/material';
import MaterialModel from '../models/material'
+import SampleModel from '../models/sample';
import IdValidate from './validate/id';
+import res400 from './validate/res400';
+import mongoose from 'mongoose';
const router = express.Router();
@@ -34,10 +37,7 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
const {error, value: material} = MaterialValidate.input(req.body, 'change');
- if (error) {
- res.status(400).json({status: 'Invalid body format'});
- return;
- }
+ if (error) return res400(error, res);
if (material.hasOwnProperty('name')) {
MaterialModel.find({name: material.name}).lean().exec((err, data) => {
@@ -71,14 +71,21 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => {
router.delete('/material/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
- MaterialModel.findByIdAndDelete(req.params.id).lean().exec((err, data) => {
+ // check if there are still samples referencing this material
+ SampleModel.find({'material_id': new mongoose.Types.ObjectId(req.params.id)}).lean().exec((err, data) => {
if (err) return next(err);
- if (data) {
- res.json({status: 'OK'})
- }
- else {
- res.status(404).json({status: 'Not found'});
+ if (data.length) {
+ return res.status(400).json({status: 'Material still in use'});
}
+ MaterialModel.findByIdAndDelete(req.params.id).lean().exec((err, data) => {
+ if (err) return next(err);
+ if (data) {
+ res.json({status: 'OK'})
+ }
+ else {
+ res.status(404).json({status: 'Not found'});
+ }
+ });
});
});
@@ -87,10 +94,7 @@ router.post('/material/new', (req, res, next) => {
// validate input
const {error, value: material} = MaterialValidate.input(req.body, 'new');
- if (error) {
- res.status(400).json({status: 'Invalid body format'});
- return;
- }
+ if (error) return res400(error, res);
MaterialModel.find({name: material.name}).lean().exec((err, data) => {
if (err) return next(err);
diff --git a/src/routes/root.spec.ts b/src/routes/root.spec.ts
index 25be1ba..f8a803f 100644
--- a/src/routes/root.spec.ts
+++ b/src/routes/root.spec.ts
@@ -1,4 +1,4 @@
-import TestHelper from "../helpers/test";
+import TestHelper from "../test/helper";
describe('/', () => {
diff --git a/src/routes/sample.spec.ts b/src/routes/sample.spec.ts
index 857556c..aa01a39 100644
--- a/src/routes/sample.spec.ts
+++ b/src/routes/sample.spec.ts
@@ -2,7 +2,7 @@ import should from 'should/as-function';
import SampleModel from '../models/sample';
import NoteModel from '../models/note';
import NoteFieldModel from '../models/note_field';
-import TestHelper from "../helpers/test";
+import TestHelper from "../test/helper";
describe('/sample', () => {
@@ -69,6 +69,387 @@ describe('/sample', () => {
});
});
+ describe('PUT /sample/{id}', () => {
+ it('returns the right sample', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200,
+ req: {},
+ res: {_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'}
+ });
+ });
+ it('keeps unchanged properties', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200,
+ req: {number: '1', type: 'granulate', color: 'black', batch: '', material_id: '100000000000000000000004', notes: {}},
+ res: {_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'}
+ });
+ });
+ it('changes the given properties', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200,
+ req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}
+ }).end(err => {
+ if (err) return done (err);
+ SampleModel.findById('400000000000000000000001').lean().exec((err, data: any) => {
+ if (err) return done (err);
+ should(data).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'validated', 'material_id', 'note_id', 'user_id', '__v');
+ should(data).have.property('_id');
+ should(data).have.property('number', '10');
+ should(data).have.property('color', 'signalviolet');
+ should(data).have.property('type', 'part');
+ should(data).have.property('batch', '114531');
+ should(data).have.property('validated').be.type('boolean');
+ should(data.material_id.toString()).be.eql('100000000000000000000002');
+ should(data.user_id.toString()).be.eql('000000000000000000000002');
+ should(data).have.property('note_id');
+ NoteModel.findById(data.note_id).lean().exec((err, data: any) => {
+ if (err) return done (err);
+ should(data).have.property('_id');
+ should(data).have.property('comment', 'Testcomment');
+ should(data).have.property('sample_references');
+ should(data.sample_references).have.lengthOf(1);
+ should(data.sample_references[0].id.toString()).be.eql('400000000000000000000003');
+ should(data.sample_references[0]).have.property('relation', 'part to this sample');
+ done();
+ });
+ })
+ });
+ });
+ it('adjusts the note_fields correctly', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000003',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {notes: {comment: 'Testcomment', sample_references: [], custom_fields: {field1: 'value 1'}}}
+ }).end(err => {
+ if (err) return done(err);
+ NoteFieldModel.findOne({name: 'not allowed for new applications'}).lean().exec((err, data) => {
+ console.log(data);
+ if (err) return done(err);
+ should(data).have.property('qty', 1);
+ NoteFieldModel.findOne({name: 'field1'}).lean().exec((err, data) => {
+ if (err) return done(err);
+ console.log(data);
+ should(data).have.property('qty', 1);
+ done();
+ });
+ });
+ });
+ });
+ it('deletes old note_fields', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000004',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {notes: {comment: 'Testcomment', sample_references: []}}
+ }).end(err => {
+ if (err) return done (err);
+ NoteFieldModel.findOne({name: 'another_field'}).lean().exec((err, data) => {
+ if (err) return done (err);
+ should(data).be.null();
+ done();
+ });
+ });
+ });
+ it('keeps untouched notes', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000002',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200,
+ req: {number: '111'}
+ }).end((err, res) => {
+ if (err) return done (err);
+ NoteModel.findById(res.body.note_id).lean().exec((err, data) => {
+ if (err) return done (err);
+ console.log(data);
+ should(data).not.be.null();
+ should(data).have.property('comment', 'Stoff gesperrt');
+ should(data).have.property('sample_references').have.lengthOf(0);
+ done();
+ });
+ });
+ });
+ it('deletes old notes', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000004',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {notes: {comment: 'Testcomment', sample_references: []}}
+ }).end(err => {
+ if (err) return done (err);
+ NoteModel.findById('500000000000000000000003').lean().exec((err, data) => {
+ if (err) return done (err);
+ should(data).be.null();
+ done();
+ });
+ });
+ });
+ it('rejects a color not defined for the material', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{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',
+ url: '/sample/400000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '000000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
+ res: {status: 'Material not available'}
+ });
+ });
+ it('rejects a sample number in use', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {number: '21', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{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: 'put',
+ url: '/sample/400000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '000000000000000000000003', relation: 'part to this sample'}]}},
+ res: {status: 'Sample reference not available'}
+ });
+ });
+ it('rejects an invalid material id', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
+ res: {status: 'Invalid body format', details: '"material_id" with value "10000000000h000000000001" fails to match the required pattern: /[0-9a-f]{24}/'}
+ });
+ });
+ it('rejects an invalid id', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/10000000000h000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 404,
+ req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
+ });
+ });
+ it('rejects an API key', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000001',
+ auth: {key: 'janedoe'},
+ httpStatus: 401,
+ req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
+ });
+ });
+ it('rejects changes for samples from another user for a write user', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000003',
+ auth: {basic: 'janedoe'},
+ httpStatus: 403,
+ req: {}
+ });
+ });
+ it('accepts changes for samples from another user for a maintain/admin user', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000001',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {},
+ res: {_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'}
+ });
+ });
+ it('rejects requests from a read user', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000001',
+ auth: {basic: 'user'},
+ httpStatus: 403,
+ req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
+ });
+ });
+ it('returns 404 for an unknown sample', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/000000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 404,
+ req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}
+ });
+ })
+ it('rejects unauthorized requests', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000001',
+ httpStatus: 401,
+ req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
+ });
+ });
+ });
+
+ describe('DELETE /sample/{id}', () => {
+ it('deletes the sample', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/sample/400000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200
+ }).end((err, res) => {
+ if (err) return done(err);
+ should(res.body).be.eql({status: 'OK'});
+ SampleModel.findById('400000000000000000000001').lean().exec((err, data) => {
+ if (err) return done(err);
+ should(data).be.null();
+ done();
+ });
+ });
+ });
+ it('deletes the notes of the sample', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/sample/400000000000000000000002',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200
+ }).end((err, res) => {
+ if (err) return done(err);
+ should(res.body).be.eql({status: 'OK'});
+ NoteModel.findById('500000000000000000000001').lean().exec((err, data) => {
+ if (err) return done(err);
+ should(data).be.null();
+ done();
+ });
+ });
+ });
+ it('adjusts the note_fields correctly', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/sample/400000000000000000000004',
+ auth: {basic: 'admin'},
+ httpStatus: 200
+ }).end((err, res) => {
+ if (err) return done(err);
+ should(res.body).be.eql({status: 'OK'});
+ NoteFieldModel.findOne({name: 'not allowed for new applications'}).lean().exec((err, data) => {
+ if (err) return done(err);
+ should(data).have.property('qty', 1);
+ NoteFieldModel.findOne({name: 'another_field'}).lean().exec((err, data) => {
+ if (err) return done(err);
+ should(data).be.null();
+ done();
+ });
+ });
+ });
+ });
+ it('resets references to this sample', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/sample/400000000000000000000003',
+ auth: {basic: 'admin'},
+ httpStatus: 200
+ }).end((err, res) => {
+ if (err) return done(err);
+ should(res.body).be.eql({status: 'OK'});
+ setTimeout(() => { // background action takes some time before we can check
+ NoteModel.findById('500000000000000000000003').lean().exec((err, data) => {
+ if (err) return done(err);
+ console.log(data);
+ should(data).have.property('sample_references').with.lengthOf(0);
+ done();
+ });
+ }, 100);
+
+ });
+ });
+ it('lets admin/maintain users delete samples of other users', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/sample/400000000000000000000001',
+ auth: {basic: 'admin'},
+ httpStatus: 200
+ }).end((err, res) => {
+ if (err) return done(err);
+ should(res.body).be.eql({status: 'OK'});
+ SampleModel.findById('400000000000000000000001').lean().exec((err, data) => {
+ if (err) return done(err);
+ should(data).be.null();
+ done();
+ });
+ });
+ });
+ it('rejects deleting samples of other users for write users', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/sample/400000000000000000000004',
+ auth: {basic: 'janedoe'},
+ httpStatus: 403
+ });
+ });
+ it('rejects an invalid id', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/sample/400000000000h00000000004',
+ auth: {basic: 'janedoe'},
+ httpStatus: 404
+ });
+ });
+ it('rejects requests from a read user', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/sample/400000000000000000000004',
+ auth: {basic: 'user'},
+ httpStatus: 403
+ });
+ });
+ it('returns 404 for an unknown id', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/sample/000000000000000000000004',
+ auth: {basic: 'janedoe'},
+ httpStatus: 404
+ });
+ });
+ it('rejects an API key', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/sample/400000000000000000000001',
+ auth: {key: 'janedoe'},
+ httpStatus: 401
+ });
+ });
+ it('rejects unauthorized requests', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/sample/400000000000000000000001',
+ httpStatus: 401
+ });
+ });
+ });
+
describe('POST /sample/new', () => {
it('returns the right sample', done => {
TestHelper.request(server, done, {
@@ -209,7 +590,7 @@ describe('/sample', () => {
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'Rng172', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
- res: {status: 'Invalid body format'}
+ res: {status: 'Invalid body format', details: '"color" is required'}
});
});
it('rejects a missing sample number', done => {
@@ -219,7 +600,7 @@ describe('/sample', () => {
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
- res: {status: 'Invalid body format'}
+ res: {status: 'Invalid body format', details: '"number" is required'}
});
});
it('rejects a missing type', done => {
@@ -229,7 +610,7 @@ describe('/sample', () => {
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'Rng172', color: 'black', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
- res: {status: 'Invalid body format'}
+ res: {status: 'Invalid body format', details: '"type" is required'}
});
});
it('rejects a missing batch', done => {
@@ -239,7 +620,7 @@ describe('/sample', () => {
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'Rng172', color: 'black', type: 'granulate', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
- res: {status: 'Invalid body format'}
+ res: {status: 'Invalid body format', details: '"batch" is required'}
});
});
it('rejects a missing material id', done => {
@@ -249,7 +630,7 @@ describe('/sample', () => {
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
- res: {status: 'Invalid body format'}
+ res: {status: 'Invalid body format', details: '"material_id" is required'}
});
});
it('rejects an invalid material id', done => {
@@ -259,7 +640,7 @@ describe('/sample', () => {
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
- res: {status: 'Invalid body format'}
+ res: {status: 'Invalid body format', details: '"material_id" with value "10000000000h000000000001" fails to match the required pattern: /[0-9a-f]{24}/'}
});
});
it('rejects an API key', done => {
diff --git a/src/routes/sample.ts b/src/routes/sample.ts
index bbebaba..fe12ed0 100644
--- a/src/routes/sample.ts
+++ b/src/routes/sample.ts
@@ -2,11 +2,12 @@ import express from 'express';
import SampleValidate from './validate/sample';
import NoteFieldValidate from './validate/note_field';
+import res400 from './validate/res400';
import SampleModel from '../models/sample'
import MaterialModel from '../models/material';
import NoteModel from '../models/note';
import NoteFieldModel from '../models/note_field';
-
+import IdValidate from './validate/id';
const router = express.Router();
@@ -20,66 +21,119 @@ router.get('/samples', (req, res, next) => {
})
});
-
-router.post('/sample/new', (req, res, next) => {
+router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
- const {error, value: sample} = SampleValidate.input(req.body, 'new');
- if (error) {
- return res.status(400).json({status: 'Invalid body format'});
- }
+ const {error, value: sample} = SampleValidate.input(req.body, 'change');
+ if (error) return res400(error, res);
- MaterialModel.findById(sample.material_id).lean().exec((err, data: any) => { // validate material_id
+ SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => { // check if id exists
if (err) return next(err);
- if (!data) { // could not find material_id
- return res.status(400).json({status: 'Material not available'});
+ if (!sampleData) {
+ return res.status(404).json({status: 'Not found'});
}
- if (!data.numbers.find(e => e.color === sample.color)) { // color for material not specified
- return res.status(400).json({status: 'Color not available for material'});
- }
- SampleModel.findOne({number: sample.number}).lean().exec((err, data) => { // validate sample number
- if (err) return next(err);
- if (data) { // found entry with sample number
- return res.status(400).json({status: 'Sample number already taken'});
- }
+ // only maintain and admin are allowed to edit other user's data
+ if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return;
- if (sample.notes.sample_references.length > 0) { // validate sample_references
- let referencesCount = sample.notes.sample_references.length;
- sample.notes.sample_references.forEach(reference => {
- SampleModel.findById(reference.id).lean().exec((err, data) => {
- if (err) return next(err);
- if (!data) {
- return res.status(400).json({status: 'Sample reference not available'});
- }
- referencesCount --;
- if (referencesCount <= 0) {
- f();
- }
+ if (sample.hasOwnProperty('number') && sample.number !== sampleData.number) {
+ if (!await numberCheck(sample, res, next)) return;
+ }
+ if (sample.hasOwnProperty('material_id')) {
+ if (!await materialCheck(sample, res, next)) return;
+ }
+ else if (sample.hasOwnProperty('color')) {
+ if (!await materialCheck(sample, res, next, sampleData.material_id)) return;
+ }
+
+ if (sample.hasOwnProperty('notes') && sampleData.note_id !== null) { // deal with old notes data
+ NoteModel.findById(sampleData.note_id).lean().exec((err, data: any) => {
+ if (err) return console.error(err);
+ if (data.hasOwnProperty('custom_fields')) { // update note_fields
+ customFieldsChange(Object.keys(data.custom_fields), -1);
+ }
+ NoteModel.findByIdAndDelete(sampleData.note_id).lean().exec(err => { // delete old notes
+ if (err) return console.error(err);
+ })
+ });
+ }
+ if (sample.hasOwnProperty('notes') && Object.keys(sample.notes).length > 0) { // save new notes
+ if (!await sampleRefCheck(sample, res, next)) return;
+ if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) { // new custom_fields
+ customFieldsChange(Object.keys(sample.notes.custom_fields), 1);
+ }
+ let data = await new NoteModel(sample.notes).save().catch(err => { return next(err)}); // save new notes
+ delete sample.notes;
+ sample.note_id = data._id;
+ }
+ SampleModel.findByIdAndUpdate(req.params.id, sample, {new: true}).lean().exec((err, data) => {
+ if (err) return next(err);
+ res.json(SampleValidate.output(data));
+ });
+
+ });
+});
+
+router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => {
+ if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
+
+ SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => { // check if id exists
+ if (err) return next(err);
+ if (!sampleData) {
+ return res.status(404).json({status: 'Not found'});
+ }
+ // only maintain and admin are allowed to edit other user's data
+ if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return;
+
+ SampleModel.findByIdAndDelete(req.params.id).lean().exec(err => { // delete sample
+ if (err) return next(err);
+ if (sampleData.note_id !== null) {
+ NoteModel.findByIdAndDelete(sampleData.note_id).lean().exec((err, data: any) => { // delete notes
+ if (err) return next(err);
+ console.log(data);
+ if (data.hasOwnProperty('custom_fields')) { // update note_fields
+ customFieldsChange(Object.keys(data.custom_fields), -1);
+ }
+ res.json({status: 'OK'});
+ NoteModel.updateMany({'sample_references.id': req.params.id}, {$unset: {'sample_references.$': null}}).lean().exec(err => { // remove sample_references
+ if (err) console.error(err);
+ NoteModel.collection.updateMany({sample_references: null}, {$pull: {sample_references: null}}, err => { // only works with native MongoDB driver somehow
+ if (err) console.error(err);
+ });
});
});
}
else {
- f();
- }
-
- if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) {
- customFieldsAdd(Object.keys(sample.notes.custom_fields));
- }
-
- function f() { // to resolve async
- new NoteModel(sample.notes).save((err, data) => {
- if (err) return next(err);
- delete sample.notes;
- sample.note_id = data._id;
- sample.user_id = req.authDetails.id;
- new SampleModel(sample).save((err, data) => {
- if (err) return next(err);
- res.json(SampleValidate.output(data.toObject()));
- });
- });
+ res.json({status: 'OK'});
}
});
- })
+ });
+});
+
+router.post('/sample/new', async (req, res, next) => {
+ if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
+
+ const {error, value: sample} = SampleValidate.input(req.body, 'new');
+ if (error) return res400(error, res);
+
+ if (!await numberCheck(sample, res, next)) return;
+ if (!await materialCheck(sample, res, next)) return;
+ if (!await sampleRefCheck(sample, res, next)) return;
+
+ if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) { // new custom_fields
+ customFieldsChange(Object.keys(sample.notes.custom_fields), 1);
+ }
+
+ new NoteModel(sample.notes).save((err, data) => {
+ if (err) return next(err);
+ delete sample.notes;
+ sample.note_id = data._id;
+ sample.user_id = req.authDetails.id;
+ console.log(sample);
+ new SampleModel(sample).save((err, data) => {
+ if (err) return next(err);
+ res.json(SampleValidate.output(data.toObject()));
+ });
+ });
});
router.get('/sample/notes/fields', (req, res, next) => {
@@ -95,15 +149,69 @@ router.get('/sample/notes/fields', (req, res, next) => {
module.exports = router;
-function customFieldsAdd (fields) {
+async function numberCheck (sample, res, next) { // validate number, returns false if invalid
+ const sampleData = await SampleModel.findOne({number: sample.number}).lean().exec().catch(err => { return next(err)});
+ 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;
+ }
+ if (!materialData) { // could not find material_id
+ res.status(400).json({status: 'Material not available'});
+ return false;
+ }
+ if (sample.hasOwnProperty('color') && !materialData.numbers.find(e => e.color === sample.color)) { // color for material not specified
+ res.status(400).json({status: 'Color not available for material'});
+ return false;
+ }
+ return true;
+}
+
+function sampleRefCheck (sample, res, next) { // validate sample_references, resolves false for invalid reference
+ return new Promise(resolve => {
+ if (sample.notes.sample_references.length > 0) { // there are sample_references
+ let referencesCount = sample.notes.sample_references.length;
+ sample.notes.sample_references.forEach(reference => {
+ SampleModel.findById(reference.id).lean().exec((err, data) => {
+ if (err) {next(err); resolve(false)}
+ if (!data) {
+ res.status(400).json({status: 'Sample reference not available'});
+ return resolve(false);
+ }
+ referencesCount --;
+ if (referencesCount <= 0) {
+ resolve(true);
+ }
+ });
+ });
+ }
+ else {
+ resolve(true);
+ }
+ });
+}
+
+function customFieldsChange (fields, amount) {
fields.forEach(field => {
- NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: 1}}).lean().exec((err, data) => { // check if field exists
+ NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: amount}}, {new: true}).lean().exec((err, data: any) => { // check if field exists
if (err) return console.error(err);
if (!data) { // new field
new NoteFieldModel({name: field, qty: 1}).save(err => {
if (err) return console.error(err);
})
}
+ else if (data.qty <= 0) {
+ NoteFieldModel.findOneAndDelete({name: field}).lean().exec(err => {
+ if (err) return console.error(err);
+ });
+ }
});
});
}
\ No newline at end of file
diff --git a/src/routes/template.spec.ts b/src/routes/template.spec.ts
index 68b3d4a..fa9361f 100644
--- a/src/routes/template.spec.ts
+++ b/src/routes/template.spec.ts
@@ -1,7 +1,7 @@
import should from 'should/as-function';
import TemplateTreatmentModel from '../models/treatment_template';
import TemplateMeasurementModel from '../models/measurement_template';
-import TestHelper from "../helpers/test";
+import TestHelper from "../test/helper";
describe('/template', () => {
@@ -182,14 +182,54 @@ describe('/template', () => {
});
});
});
- it('rejects an incomplete template for a new name', done => {
+ it('rejects a missing name for a new name', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/treatment/heat%20aging',
auth: {basic: 'admin'},
httpStatus: 400,
- req: {parameters: [{name: 'time'}]},
- res: {status: 'Invalid body format'}
+ req: {parameters: [{name: 'time', range: {min: 1}}]},
+ res: {status: 'Invalid body format', details: '"name" is required'}
+ });
+ });
+ it('rejects missing parameters for a new name', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/template/treatment/heat%20aging',
+ auth: {basic: 'admin'},
+ httpStatus: 400,
+ req: {name: 'heat aging'},
+ res: {status: 'Invalid body format', details: '"parameters" is required'}
+ });
+ });
+ it('rejects a missing parameter name for a new name', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/template/treatment/heat%20aging',
+ auth: {basic: 'admin'},
+ httpStatus: 400,
+ req: {name: 'heat aging', parameters: [{range: {min: 1}}]},
+ 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/treatment/heat%20aging',
+ 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 for a new name', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/template/treatment/heat%20aging',
+ auth: {basic: 'admin'},
+ httpStatus: 400,
+ req: {name: 'heat aging', parameters: [{name: 'time', range: {xx: 1}}]},
+ res: {status: 'Invalid body format', details: '"parameters[0].range.xx" is not allowed'}
});
});
it('rejects already existing names', done => {
@@ -209,7 +249,7 @@ describe('/template', () => {
auth: {basic: 'admin'},
httpStatus: 400,
req: {parameters: [{name: 'time'}], xx: 33},
- res: {status: 'Invalid body format'}
+ res: {status: 'Invalid body format', details: '"name" is required'}
});
});
it('rejects an API key', done => {
@@ -466,14 +506,54 @@ describe('/template', () => {
});
});
});
- it('rejects an incomplete template for a new name', done => {
+ it('rejects a missing name for a new name', done => {
TestHelper.request(server, done, {
method: 'put',
- url: '/template/measurement/vz',
+ url: '/template/measurement/spectrum2',
auth: {basic: 'admin'},
httpStatus: 400,
- req: {parameters: [{name: 'vz'}]},
- res: {status: 'Invalid body format'}
+ req: {parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]},
+ res: {status: 'Invalid body format', details: '"name" is required'}
+ });
+ });
+ it('rejects missing parameters for a new name', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/template/measurement/spectrum2',
+ auth: {basic: 'admin'},
+ httpStatus: 400,
+ req: {name: 'IR spectrum'},
+ res: {status: 'Invalid body format', details: '"parameters" is required'}
+ });
+ });
+ it('rejects a missing parameter name 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: [{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 => {
@@ -493,7 +573,7 @@ describe('/template', () => {
auth: {basic: 'admin'},
httpStatus: 400,
req: {parameters: [{name: 'dpt'}], xx: 33},
- res: {status: 'Invalid body format'}
+ res: {status: 'Invalid body format', details: '"parameters[0].range" is required'}
});
});
it('rejects an API key', done => {
diff --git a/src/routes/template.ts b/src/routes/template.ts
index 1e859cd..afd686e 100644
--- a/src/routes/template.ts
+++ b/src/routes/template.ts
@@ -3,6 +3,7 @@ import express from 'express';
import TemplateValidate from './validate/template';
import TemplateTreatmentModel from '../models/treatment_template';
import TemplateMeasurementModel from '../models/measurement_template';
+import res400 from './validate/res400';
const router = express.Router();
@@ -41,10 +42,7 @@ router.put('/template/:collection(measurement|treatment)/:name', (req, res, next
if (err) next (err);
const templateState = data? 'change': 'new';
const {error, value: template} = TemplateValidate.input(req.body, templateState);
- if (error) {
- res.status(400).json({status: 'Invalid body format'});
- return;
- }
+ if (error) return res400(error, res);
if (template.hasOwnProperty('name') && template.name !== req.params.name) {
collectionModel.find({name: template.name}).lean().exec((err, data) => {
diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts
index b103ef7..e294cb2 100644
--- a/src/routes/user.spec.ts
+++ b/src/routes/user.spec.ts
@@ -1,6 +1,6 @@
import should from 'should/as-function';
import UserModel from '../models/user';
-import TestHelper from "../helpers/test";
+import TestHelper from "../test/helper";
describe('/user', () => {
@@ -224,7 +224,7 @@ describe('/user', () => {
req: {level: 'read'}
}).end((err, res) => {
if (err) return done (err);
- should(res.body).be.eql({status: 'Invalid body format'});
+ should(res.body).be.eql({status: 'Invalid body format', details: '"level" is not allowed'});
UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => {
if (err) return done(err);
should(data).have.lengthOf(1);
@@ -267,7 +267,7 @@ describe('/user', () => {
auth: {basic: 'admin'},
httpStatus: 400,
req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', location: 44, device_name: 'Alpha II'},
- res: {status: 'Invalid body format'}
+ res: {status: 'Invalid body format', details: '"location" must be a string'}
});
});
it('rejects an invalid email address', done => {
@@ -277,7 +277,7 @@ describe('/user', () => {
auth: {basic: 'admin'},
httpStatus: 400,
req: {email: 'john.doe'},
- res: {status: 'Invalid body format'}
+ res: {status: 'Invalid body format', details: '"email" must be a valid email'}
});
});
it('rejects an invalid password', done => {
@@ -287,7 +287,7 @@ describe('/user', () => {
auth: {basic: 'admin'},
httpStatus: 400,
req: {pass: 'password'},
- res: {status: 'Invalid body format'}
+ res: {status: 'Invalid body format', details: '"pass" with value "password" fails to match the required pattern: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$/'}
});
});
it('rejects requests from non-admins for another user', done => {
@@ -515,7 +515,7 @@ describe('/user', () => {
auth: {basic: 'admin'},
httpStatus: 400,
req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 44, device_name: 'Alpha II'},
- res: {status: 'Invalid body format'}
+ res: {status: 'Invalid body format', details: '"location" must be a string'}
});
});
it('rejects an invalid user level', done => {
@@ -525,7 +525,7 @@ describe('/user', () => {
auth: {basic: 'admin'},
httpStatus: 400,
req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'xxx', location: 'Rng', device_name: 'Alpha II'},
- res: {status: 'Invalid body format'}
+ res: {status: 'Invalid body format', details: '"level" must be one of [read, write, maintain, dev, admin]'}
});
});
it('rejects an invalid email address', done => {
@@ -535,7 +535,7 @@ describe('/user', () => {
auth: {basic: 'admin'},
httpStatus: 400,
req: {email: 'john.doe', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'},
- res: {status: 'Invalid body format'}
+ res: {status: 'Invalid body format', details: '"email" must be a valid email'}
});
});
it('rejects an invalid password', done => {
@@ -545,7 +545,7 @@ describe('/user', () => {
auth: {basic: 'admin'},
httpStatus: 400,
req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'password', level: 'read', location: 'Rng', device_name: 'Alpha II'},
- res: {status: 'Invalid body format'}
+ res: {status: 'Invalid body format', details: '"pass" with value "password" fails to match the required pattern: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$/'}
});
});
it('rejects requests from non-admins', done => {
diff --git a/src/routes/user.ts b/src/routes/user.ts
index a0161f9..db78527 100644
--- a/src/routes/user.ts
+++ b/src/routes/user.ts
@@ -5,6 +5,7 @@ import bcrypt from 'bcryptjs';
import UserValidate from './validate/user';
import UserModel from '../models/user';
import mail from '../helpers/mail';
+import res400 from './validate/res400';
const router = express.Router();
@@ -46,10 +47,7 @@ router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi
username = req.params.username;
}
const {error, value: user} = UserValidate.input(req.body, 'change' + (req.authDetails.level === 'admin'? 'admin' : ''));
- if (error) {
- res.status(400).json({status: 'Invalid body format'});
- return;
- }
+ if (error) return res400(error, res);
if (user.hasOwnProperty('pass')) {
user.pass = bcrypt.hashSync(user.pass, 10);
@@ -122,10 +120,7 @@ router.post('/user/new', (req, res, next) => {
// validate input
const {error, value: user} = UserValidate.input(req.body, 'new');
- if (error) {
- res.status(400).json({status: 'Invalid body format'});
- return;
- }
+ if (error) return res400(error, res);
// check that user does not already exist
UserModel.find({name: user.name}).lean().exec( (err, data:any) => {
diff --git a/src/routes/validate/id.ts b/src/routes/validate/id.ts
index 5409993..a9bb70a 100644
--- a/src/routes/validate/id.ts
+++ b/src/routes/validate/id.ts
@@ -1,7 +1,7 @@
-import joi from '@hapi/joi';
+import Joi from '@hapi/joi';
export default class IdValidate {
- private static id = joi.string().pattern(new RegExp('[0-9a-f]{24}')).length(24);
+ private static id = Joi.string().pattern(new RegExp('[0-9a-f]{24}')).length(24);
static get () {
return this.id;
diff --git a/src/routes/validate/material.ts b/src/routes/validate/material.ts
index 54cd749..c8b6e91 100644
--- a/src/routes/validate/material.ts
+++ b/src/routes/validate/material.ts
@@ -31,9 +31,11 @@ export default class MaterialValidate { // validate input for material
numbers: joi.array()
.items(joi.object({
color: joi.string()
- .max(128),
+ .max(128)
+ .required(),
number: joi.number()
.min(0)
+ .required()
}))
};
@@ -46,7 +48,7 @@ export default class MaterialValidate { // validate input for material
mineral: this.material.mineral.required(),
glass_fiber: this.material.glass_fiber.required(),
carbon_fiber: this.material.carbon_fiber.required(),
- numbers: this.material.numbers
+ numbers: this.material.numbers.required()
}).validate(data);
}
else if (param === 'change') {
diff --git a/src/routes/validate/note_field.ts b/src/routes/validate/note_field.ts
index 4892f22..7d34d98 100644
--- a/src/routes/validate/note_field.ts
+++ b/src/routes/validate/note_field.ts
@@ -1,15 +1,15 @@
-import joi from '@hapi/joi';
+import Joi from '@hapi/joi';
export default class NoteFieldValidate {
private static note_field = {
- name: joi.string()
+ name: Joi.string()
.max(128),
- qty: joi.number()
+ qty: Joi.number()
};
static output (data) {
- const {value, error} = joi.object({
+ const {value, error} = Joi.object({
name: this.note_field.name,
qty: this.note_field.qty
}).validate(data, {stripUnknown: true});
diff --git a/src/routes/validate/res400.ts b/src/routes/validate/res400.ts
new file mode 100644
index 0000000..5e032f7
--- /dev/null
+++ b/src/routes/validate/res400.ts
@@ -0,0 +1,3 @@
+export default function res400 (error, res) {
+ res.status(400).json({status: 'Invalid body format', details: error.details[0].message});
+}
\ No newline at end of file
diff --git a/src/routes/validate/sample.ts b/src/routes/validate/sample.ts
index d94cede..aa28304 100644
--- a/src/routes/validate/sample.ts
+++ b/src/routes/validate/sample.ts
@@ -1,41 +1,41 @@
-import joi from '@hapi/joi';
+import Joi from '@hapi/joi';
import IdValidate from './id';
export default class SampleValidate {
private static sample = {
- number: joi.string()
+ number: Joi.string()
.max(128),
- color: joi.string()
+ color: Joi.string()
.max(128),
- type: joi.string()
+ type: Joi.string()
.max(128),
- batch: joi.string()
+ batch: Joi.string()
.max(128)
.allow(''),
- notes: joi.object({
- comment: joi.string()
+ notes: Joi.object({
+ comment: Joi.string()
.max(512),
- sample_references: joi.array()
- .items(joi.object({
+ sample_references: Joi.array()
+ .items(Joi.object({
id: IdValidate.get(),
- relation: joi.string()
+ relation: Joi.string()
.max(128)
})),
- custom_fields: joi.object()
- .pattern(/.*/, joi.alternatives()
+ custom_fields: Joi.object()
+ .pattern(/.*/, Joi.alternatives()
.try(
- joi.string().max(128),
- joi.number(),
- joi.boolean(),
- joi.date()
+ Joi.string().max(128),
+ Joi.number(),
+ Joi.boolean(),
+ Joi.date()
)
)
})
@@ -43,7 +43,7 @@ export default class SampleValidate {
static input (data, param) { // validate data, param: new(everything required)/change(available attributes are validated)
if (param === 'new') {
- return joi.object({
+ return Joi.object({
number: this.sample.number.required(),
color: this.sample.color.required(),
type: this.sample.type.required(),
@@ -53,7 +53,14 @@ export default class SampleValidate {
}).validate(data);
}
else if (param === 'change') {
- return{error: 'Not implemented!', value: {}};
+ return Joi.object({
+ number: this.sample.number,
+ color: this.sample.color,
+ type: this.sample.type,
+ batch: this.sample.batch,
+ material_id: IdValidate.get(),
+ notes: this.sample.notes,
+ }).validate(data);
}
else {
return{error: 'No parameter specified!', value: {}};
@@ -62,7 +69,7 @@ export default class SampleValidate {
static output (data) {
data = IdValidate.stringify(data);
- const {value, error} = joi.object({
+ const {value, error} = Joi.object({
_id: IdValidate.get(),
number: this.sample.number,
color: this.sample.color,
diff --git a/src/routes/validate/user.ts b/src/routes/validate/user.ts
index 150bf64..024d1a9 100644
--- a/src/routes/validate/user.ts
+++ b/src/routes/validate/user.ts
@@ -1,32 +1,32 @@
-import joi from '@hapi/joi';
+import Joi from '@hapi/joi';
import globals from '../../globals';
import IdValidate from './id';
export default class UserValidate { // validate input for user
private static user = {
- name: joi.string()
+ name: Joi.string()
.alphanum()
.lowercase()
.max(128),
- email: joi.string()
+ email: Joi.string()
.email({minDomainSegments: 2})
.lowercase()
.max(128),
- pass: joi.string()
+ pass: Joi.string()
.pattern(new RegExp('^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$'))
.max(128),
- level: joi.string()
+ level: Joi.string()
.valid(...globals.levels),
- location: joi.string()
+ location: Joi.string()
.alphanum()
.max(128),
- device_name: joi.string()
+ device_name: Joi.string()
.allow('')
.max(128),
};
@@ -35,7 +35,7 @@ export default class UserValidate { // validate input for user
static input (data, param) {
if (param === 'new') {
- return joi.object({
+ return Joi.object({
name: this.user.name.required(),
email: this.user.email.required(),
pass: this.user.pass.required(),
@@ -45,7 +45,7 @@ export default class UserValidate { // validate input for user
}).validate(data);
}
else if (param === 'change') {
- return joi.object({
+ return Joi.object({
name: this.user.name,
email: this.user.email,
pass: this.user.pass,
@@ -54,7 +54,7 @@ export default class UserValidate { // validate input for user
}).validate(data);
}
else if (param === 'changeadmin') {
- return joi.object({
+ return Joi.object({
name: this.user.name,
email: this.user.email,
pass: this.user.pass,
@@ -70,7 +70,7 @@ export default class UserValidate { // validate input for user
static output (data) { // validate output from database for needed properties, strip everything else
data = IdValidate.stringify(data);
- const {value, error} = joi.object({
+ const {value, error} = Joi.object({
_id: IdValidate.get(),
name: this.user.name,
email: this.user.email,
diff --git a/src/test/db.json b/src/test/db.json
index 2d8a7d0..24daaca 100644
--- a/src/test/db.json
+++ b/src/test/db.json
@@ -61,7 +61,7 @@
"_id": {"$oid":"500000000000000000000002"},
"comment": "",
"sample_references": [{
- "id": "400000000000000000000004",
+ "id": {"$oid":"400000000000000000000004"},
"relation": "granulate to sample"
}],
"custom_fields": {
@@ -73,11 +73,12 @@
"_id": {"$oid":"500000000000000000000003"},
"comment": "",
"sample_references": [{
- "id": "400000000000000000000003",
+ "id": {"$oid":"400000000000000000000003"},
"relation": "part to sample"
}],
"custom_fields": {
- "not allowed for new applications": true
+ "not allowed for new applications": true,
+ "another_field": "is there"
},
"__v": 0
}
@@ -88,6 +89,12 @@
"name": "not allowed for new applications",
"qty": 2,
"__v": 0
+ },
+ {
+ "_id": {"$oid":"600000000000000000000002"},
+ "name": "another_field",
+ "qty": 1,
+ "__v": 0
}
],
"materials": [
diff --git a/src/helpers/test.ts b/src/test/helper.ts
similarity index 98%
rename from src/helpers/test.ts
rename to src/test/helper.ts
index 6c2fa72..26cb5a5 100644
--- a/src/helpers/test.ts
+++ b/src/test/helper.ts
@@ -28,7 +28,7 @@ export default class TestHelper {
server = require('../index');
db.drop(err => { // reset database
if (err) return done(err);
- db.loadJson(require('../test/db.json'), done);
+ db.loadJson(require('./db.json'), done);
});
return server
}
diff --git a/src/test/loadDev.ts b/src/test/loadDev.ts
new file mode 100644
index 0000000..690044d
--- /dev/null
+++ b/src/test/loadDev.ts
@@ -0,0 +1,12 @@
+import db from '../db';
+
+db.connect('dev', () => {
+ console.info('dropping data...');
+ db.drop(() => { // reset database
+ console.info('loading data...');
+ db.loadJson(require('./db.json'), () => {
+ console.info('done');
+ process.exit(0);
+ });
+ });
+});