Archived
2

Merge pull request #10 in ~VLE2FE/dfop-api from minor-features to develop

* commit 'aef275322955774e0c34c69ce3c4e7722aadcb9e':
  added TODOs, improved password validation
  refactored user.ts
  /materials/new|deleted
  sample number generation
  number prefixes are now not allowed to contain numbers
  material numbers defined as string, colors without numbers and numbers with leading zeros can be added
  material numbers defined as string, colors without numbers can be added
  number generation for condition done
  changed allowed characters for username
This commit is contained in:
Veit Lukas (PEA4-Fe) 2020-05-26 09:08:01 +02:00
commit 2829752d0c
38 changed files with 807 additions and 385 deletions

13
.idea/libraries/dist.xml Normal file
View File

@ -0,0 +1,13 @@
<component name="libraryTable">
<library name="dist" type="javaScript">
<properties>
<sourceFilesUrls>
<item url="file://$PROJECT_DIR$/dist" />
</sourceFilesUrls>
</properties>
<CLASSES>
<root url="file://$PROJECT_DIR$/dist" />
</CLASSES>
<SOURCES />
</library>
</component>

View File

@ -4,7 +4,7 @@
get: get:
summary: condition by id summary: condition by id
description: 'Auth: all, levels: read, write, maintain, dev, admin' description: 'Auth: all, levels: read, write, maintain, dev, admin'
x-doc: status handling (accessible (only for maintain/admin))? # TODO x-doc: status handling (accessible (only for maintain/admin))? # TODO after decision
tags: tags:
- /condition - /condition
responses: responses:
@ -38,9 +38,6 @@
allOf: allOf:
- $ref: 'api.yaml#/components/schemas/_Id' - $ref: 'api.yaml#/components/schemas/_Id'
properties: properties:
number:
type: string
example: B1
parameters: parameters:
type: object type: object
responses: responses:

View File

@ -2,7 +2,30 @@
get: get:
summary: lists all materials summary: lists all materials
description: 'Auth: all, levels: read, write, maintain, dev, admin' description: 'Auth: all, levels: read, write, maintain, dev, admin'
x-doc: returns only materials with status 10 # TODO: methods /materials/new|deleted x-doc: returns only materials with status 10
tags:
- /material
responses:
200:
description: all material details
content:
application/json:
schema:
type: array
items:
$ref: 'api.yaml#/components/schemas/Material'
401:
$ref: 'api.yaml#/components/responses/401'
500:
$ref: 'api.yaml#/components/responses/500'
/materials/{group}:
parameters:
- $ref: 'api.yaml#/components/parameters/Group'
get:
summary: lists all new/deleted materials
description: 'Auth: basic, levels: maintain, admin'
x-doc: returns materials with status 0/-1
tags: tags:
- /material - /material
responses: responses:
@ -25,7 +48,7 @@
get: get:
summary: get material details summary: get material details
description: 'Auth: all, levels: read, write, maintain, dev, admin' description: 'Auth: all, levels: read, write, maintain, dev, admin'
x-doc: status handling (accessible (only for maintain/admin))? # TODO x-doc: status handling (accessible (only for maintain/admin))? # TODO after decision
tags: tags:
- /material - /material
responses: responses:

View File

@ -4,7 +4,7 @@
get: get:
summary: measurement values by id summary: measurement values by id
description: 'Auth: all, levels: read, write, maintain, dev, admin' description: 'Auth: all, levels: read, write, maintain, dev, admin'
x-doc: status handling (accessible (only for maintain/admin))? # TODO x-doc: status handling (accessible (only for maintain/admin))? # TODO after decision
tags: tags:
- /measurement - /measurement
responses: responses:

View File

@ -5,10 +5,20 @@ Id:
schema: schema:
type: string type: string
example: 5ea0450ed851c30a90e70894 example: 5ea0450ed851c30a90e70894
Name: Name:
name: name name: name
description: has to be URL encoded description: has to be URL encoded
in: path in: path
required: true required: true
schema: schema:
type: string type: string
Group:
name: group
description: 'possible values: new, deleted'
in: path
required: true
schema:
type: string
example: deleted

View File

@ -2,7 +2,7 @@
get: get:
summary: all samples in overview summary: all samples in overview
description: 'Auth: all, levels: read, write, maintain, dev, admin' description: 'Auth: all, levels: read, write, maintain, dev, admin'
x-doc: returns only samples with status 10 # TODO: methods /samples/new|deleted x-doc: returns only samples with status 10
tags: tags:
- /sample - /sample
responses: responses:
@ -18,13 +18,37 @@
$ref: 'api.yaml#/components/responses/401' $ref: 'api.yaml#/components/responses/401'
500: 500:
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
/samples{group}:
parameters:
- $ref: 'api.yaml#/components/parameters/Group'
get:
summary: all new/deleted samples in overview
description: 'Auth: basic, levels: maintain, admin'
x-doc: returns only samples with status 0/-1
tags:
- /sample
responses:
200:
description: samples overview
content:
application/json:
schema:
type: array
items:
$ref: 'api.yaml#/components/schemas/SampleRefs'
401:
$ref: 'api.yaml#/components/responses/401'
500:
$ref: 'api.yaml#/components/responses/500'
/sample/{id}: /sample/{id}:
parameters: parameters:
- $ref: 'api.yaml#/components/parameters/Id' - $ref: 'api.yaml#/components/parameters/Id'
get: get:
summary: TODO sample details summary: TODO sample details
description: 'Auth: all, levels: read, write, maintain, dev, admin' description: 'Auth: all, levels: read, write, maintain, dev, admin'
x-doc: status handling (accessible (only for maintain/admin))? # TODO x-doc: status handling (accessible (only for maintain/admin))? # TODO after decision
tags: tags:
- /sample - /sample
responses: responses:
@ -130,7 +154,7 @@
get: get:
summary: 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' description: 'Auth: all, levels: read, write, maintain, dev, admin'
x-doc: integrity has to be ensured # TODO: implement mechanism to regularly check note_fields x-doc: integrity has to be ensured
tags: tags:
- /sample - /sample
responses: responses:

View File

@ -16,6 +16,7 @@ SampleProperties:
properties: properties:
number: number:
type: string type: string
readOnly: true
example: Rng172 example: Rng172
type: type:
type: string type: string
@ -111,7 +112,7 @@ Material:
- $ref: 'api.yaml#/components/schemas/Color' - $ref: 'api.yaml#/components/schemas/Color'
properties: properties:
number: number:
type: number type: string
example: 5514263423 example: 5514263423
Condition: Condition:
@ -122,6 +123,7 @@ Condition:
$ref: 'api.yaml#/components/schemas/Id' $ref: 'api.yaml#/components/schemas/Id'
number: number:
type: string type: string
readOnly: true
example: B1 example: B1
parameters: parameters:
type: object type: object

View File

@ -4,7 +4,7 @@ import oasParser from '@apidevtools/swagger-parser';
// modifies the normal swagger-ui-express package // modifies the normal swagger-ui-express package
// usage: app.use('/api', api.serve(), api.setup()); // usage: app.use('/api-doc', api.serve(), api.setup());
// the paths property can be split using allOf // the paths property can be split using allOf
// further route documentation can be included in the x-doc property // further route documentation can be included in the x-doc property
@ -20,7 +20,7 @@ export default class api {
apiDoc = doc; apiDoc = doc;
apiDoc.paths = apiDoc.paths.allOf.reduce((s, e) => Object.assign(s, e)); // bundle routes apiDoc.paths = apiDoc.paths.allOf.reduce((s, e) => Object.assign(s, e)); // bundle routes
apiDoc = this.resolveXDoc(apiDoc); apiDoc = this.resolveXDoc(apiDoc);
oasParser.validate(apiDoc, (err, api) => { oasParser.validate(apiDoc, (err, api) => { // validate oas schema
if (err) { if (err) {
console.error(err); console.error(err);
} }
@ -35,8 +35,8 @@ export default class api {
private static resolveXDoc (doc) { // resolve x-doc properties recursively private static resolveXDoc (doc) { // resolve x-doc properties recursively
Object.keys(doc).forEach(key => { Object.keys(doc).forEach(key => {
if (doc[key] !== null && doc[key].hasOwnProperty('x-doc')) { if (doc[key] !== null && doc[key].hasOwnProperty('x-doc')) { // add x-doc to description, is styled via css
doc[key].description += this.addHtml(doc[key]['x-doc']); doc[key].description += '<details class="docs"><summary>docs</summary>' + doc[key]['x-doc'] + '</details>';
} }
else if (typeof doc[key] === 'object' && doc[key] !== null) { // go deeper into recursion else if (typeof doc[key] === 'object' && doc[key] !== null) { // go deeper into recursion
doc[key] = this.resolveXDoc(doc[key]); doc[key] = this.resolveXDoc(doc[key]);
@ -44,8 +44,4 @@ export default class api {
}); });
return doc; return doc;
} }
private static addHtml (text) { // add docs HTML
return '<details class="docs"><summary>docs</summary>' + text + '</details>';
}
} }

View File

@ -13,7 +13,7 @@ export default class db {
mode: null, mode: null,
}; };
static connect (mode = '', done: Function = () => {}) { // set mode to test for unit/integration tests, otherwise skip parameter. done is also only needed for testing static connect (mode = '', done: Function = () => {}) { // set mode to test for unit/integration tests, otherwise skip parameters. done is also only needed for testing
if (this.state.db) return done(); // db is already connected if (this.state.db) return done(); // db is already connected
// find right connection url // find right connection url
@ -84,9 +84,9 @@ export default class db {
} }
static loadJson (json, done: Function = () => {}) { // insert given JSON data into db, uses core mongodb methods static loadJson (json, done: Function = () => {}) { // insert given JSON data into db, uses core mongodb methods
if (!this.state.db || !json.hasOwnProperty('collections') || json.collections.length === 0) { if (!this.state.db || !json.hasOwnProperty('collections') || json.collections.length === 0) { // no db connection or nothing to load
return done(); return done();
} // no db connection or nothing to load }
let loadCounter = 0; // count number of loaded collections to know when to return done() let loadCounter = 0; // count number of loaded collections to know when to return done()
Object.keys(json.collections).forEach(collectionName => { // create each collection Object.keys(json.collections).forEach(collectionName => { // create each collection
@ -103,10 +103,10 @@ export default class db {
private static oidResolve (object: any) { // resolve $oid fields to actual ObjectIds recursively private static oidResolve (object: any) { // resolve $oid fields to actual ObjectIds recursively
Object.keys(object).forEach(key => { Object.keys(object).forEach(key => {
if (object[key] !== null && object[key].hasOwnProperty('$oid')) { if (object[key] !== null && object[key].hasOwnProperty('$oid')) { // found oid, replace
object[key] = mongoose.Types.ObjectId(object[key].$oid); object[key] = mongoose.Types.ObjectId(object[key].$oid);
} }
else if (typeof object[key] === 'object' && object[key] !== null) { else if (typeof object[key] === 'object' && object[key] !== null) { // deeper into recursion
object[key] = this.oidResolve(object[key]); object[key] = this.oidResolve(object[key]);
} }
}); });

View File

@ -9,7 +9,7 @@ import UserModel from '../models/user';
module.exports = async (req, res, next) => { module.exports = async (req, res, next) => {
let givenMethod = ''; // authorization method given by client, basic taken preferred let givenMethod = ''; // authorization method given by client, basic taken preferred
let user = {name: '', level: '', id: ''}; // user object let user = {name: '', level: '', id: '', location: ''}; // user object
// test authentications // test authentications
const userBasic = await basic(req, next); const userBasic = await basic(req, next);
@ -46,7 +46,8 @@ module.exports = async (req, res, next) => {
method: givenMethod, method: givenMethod,
username: user.name, username: user.name,
level: user.level, level: user.level,
id: user.id id: user.id,
location: user.location
}; };
next(); next();
@ -62,8 +63,8 @@ function basic (req, next): any { // checks basic auth and returns changed user
if (data.length === 1) { // one user found if (data.length === 1) { // one user found
bcrypt.compare(auth.pass, data[0].pass, (err, res) => { // check password bcrypt.compare(auth.pass, data[0].pass, (err, res) => { // check password
if (err) return next(err); if (err) return next(err);
if (res === true) { if (res === true) { // password correct
resolve({level: data[0].level, name: data[0].name, id: data[0]._id.toString()}); resolve({level: data[0].level, name: data[0].name, id: data[0]._id.toString(), location: data[0].location});
} }
else { else {
resolve(null); resolve(null);
@ -83,11 +84,11 @@ function basic (req, next): any { // checks basic auth and returns changed user
function key (req, next): any { // checks API key and returns changed user object function key (req, next): any { // checks API key and returns changed user object
return new Promise(resolve => { return new Promise(resolve => {
if (req.query.key !== undefined) { if (req.query.key !== undefined) { // key available
UserModel.find({key: req.query.key}).lean().exec( (err, data: any) => { // find user UserModel.find({key: req.query.key}).lean().exec( (err, data: any) => { // find user
if (err) return next(err); if (err) return next(err);
if (data.length === 1) { // one user found if (data.length === 1) { // one user found
resolve({level: data[0].level, name: data[0].name, id: data[0]._id.toString()}); resolve({level: data[0].level, name: data[0].name, id: data[0]._id.toString(), location: data[0].location});
} }
else { else {
resolve(null); resolve(null);

View File

@ -1,6 +1,6 @@
import axios from 'axios'; import axios from 'axios';
// sends an email // sends an email using the BIC service
export default (mailAddress, subject, content, f) => { // callback, executed empty or with error export default (mailAddress, subject, content, f) => { // callback, executed empty or with error
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {

View File

@ -5,6 +5,12 @@ import mongoSanitize from 'mongo-sanitize';
import api from './api'; import api from './api';
import db from './db'; 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
// tell if server is running in debug or production environment // tell if server is running in debug or production environment
console.info(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 =====');
@ -43,19 +49,19 @@ app.use((req, res, next) => { // no database connection error
app.use(require('./helpers/authorize')); // handle authentication app.use(require('./helpers/authorize')); // handle authentication
// require routes // require routes
app.use('/', require('./routes/root')); app.use('/api', require('./routes/root'));
app.use('/', require('./routes/sample')); app.use('/api', require('./routes/sample'));
app.use('/', require('./routes/material')); app.use('/api', require('./routes/material'));
app.use('/', require('./routes/template')); app.use('/api', require('./routes/template'));
app.use('/', require('./routes/user')); app.use('/api', require('./routes/user'));
app.use('/', require('./routes/condition')); app.use('/api', require('./routes/condition'));
app.use('/', require('./routes/measurement')); app.use('/api', require('./routes/measurement'));
// static files // static files
app.use('/static', express.static('static')); app.use('/static', express.static('static'));
// Swagger UI // Swagger UI
app.use('/api', api.serve(), api.setup()); app.use('/api-doc', api.serve(), api.setup());
app.use((req, res) => { // 404 error handling app.use((req, res) => { // 404 error handling
res.status(404).json({status: 'Not found'}); res.status(404).json({status: 'Not found'});

View File

@ -9,9 +9,9 @@ const MaterialSchema = new mongoose.Schema({
carbon_fiber: String, carbon_fiber: String,
numbers: [{ numbers: [{
color: String, color: String,
number: Number number: String
}], }],
status: Number status: Number
}); }, {minimize: false});
export default mongoose.model('material', MaterialSchema); export default mongoose.model('material', MaterialSchema);

View File

@ -2,6 +2,10 @@ import should from 'should/as-function';
import ConditionModel from '../models/condition'; import ConditionModel from '../models/condition';
import TestHelper from "../test/helper"; import TestHelper from "../test/helper";
// TODO: adding conditions allowed only for m/a
// TODO: deleted data only visible for m/a
// TODO: restore deleted
// TODO: remove number_prefix
describe('/condition', () => { describe('/condition', () => {
let server; let server;
@ -16,7 +20,7 @@ describe('/condition', () => {
url: '/condition/700000000000000000000001', url: '/condition/700000000000000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 200, httpStatus: 200,
res: {_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'B1', parameters: {material: 'copper', weeks: 3}, treatment_template: '200000000000000000000001'} res: {_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'A1', parameters: {material: 'copper', weeks: 3}, treatment_template: '200000000000000000000001'}
}); });
}); });
it('returns the right condition for an API key', done => { it('returns the right condition for an API key', done => {
@ -25,7 +29,7 @@ describe('/condition', () => {
url: '/condition/700000000000000000000001', url: '/condition/700000000000000000000001',
auth: {key: 'janedoe'}, auth: {key: 'janedoe'},
httpStatus: 200, httpStatus: 200,
res: {_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'B1', parameters: {material: 'copper', weeks: 3}, treatment_template: '200000000000000000000001'} res: {_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'A1', parameters: {material: 'copper', weeks: 3}, treatment_template: '200000000000000000000001'}
}); });
}); });
it('rejects an invalid id', done => { it('rejects an invalid id', done => {
@ -61,7 +65,7 @@ describe('/condition', () => {
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 200, httpStatus: 200,
req: {}, req: {},
res: {_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'B1', treatment_template: '200000000000000000000001', parameters: {material: 'copper', weeks: 3}} res: {_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'A1', treatment_template: '200000000000000000000001', parameters: {material: 'copper', weeks: 3}}
}); });
}); });
it('keeps unchanged properties', done => { it('keeps unchanged properties', done => {
@ -73,7 +77,7 @@ describe('/condition', () => {
req: {parameters: {material: 'copper', weeks: 3}} req: {parameters: {material: 'copper', weeks: 3}}
}).end((err, res) => { }).end((err, res) => {
if (err) return done(err); if (err) return done(err);
should(res.body).be.eql({_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'B1', treatment_template: '200000000000000000000001', parameters: {material: 'copper', weeks: 3}}); should(res.body).be.eql({_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'A1', treatment_template: '200000000000000000000001', parameters: {material: 'copper', weeks: 3}});
ConditionModel.findById('700000000000000000000001').lean().exec((err, data) => { ConditionModel.findById('700000000000000000000001').lean().exec((err, data) => {
if (err) return done(err); if (err) return done(err);
should(data).have.property('status', 10); should(data).have.property('status', 10);
@ -90,7 +94,7 @@ describe('/condition', () => {
req: {parameters: {material: 'copper'}} req: {parameters: {material: 'copper'}}
}).end((err, res) => { }).end((err, res) => {
if (err) return done(err); if (err) return done(err);
should(res.body).be.eql({_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'B1', treatment_template: '200000000000000000000001', parameters: {material: 'copper', weeks: 3}}); should(res.body).be.eql({_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'A1', treatment_template: '200000000000000000000001', parameters: {material: 'copper', weeks: 3}});
ConditionModel.findById('700000000000000000000001').lean().exec((err, data) => { ConditionModel.findById('700000000000000000000001').lean().exec((err, data) => {
if (err) return done(err); if (err) return done(err);
should(data).have.property('status', 10); should(data).have.property('status', 10);
@ -107,12 +111,12 @@ describe('/condition', () => {
req: {parameters: {material: 'hot air', weeks: 10}} req: {parameters: {material: 'hot air', weeks: 10}}
}).end((err, res) => { }).end((err, res) => {
if (err) return done(err); if (err) return done(err);
should(res.body).be.eql({_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'B1', treatment_template: '200000000000000000000001', parameters: {material: 'hot air', weeks: 10}}); should(res.body).be.eql({_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'A1', treatment_template: '200000000000000000000001', parameters: {material: 'hot air', weeks: 10}});
ConditionModel.findById('700000000000000000000001').lean().exec((err, data: any) => { ConditionModel.findById('700000000000000000000001').lean().exec((err, data: any) => {
if (err) return done(err); if (err) return done(err);
should(data).have.only.keys('_id', 'sample_id', 'number', 'parameters', 'treatment_template', 'status', '__v'); should(data).have.only.keys('_id', 'sample_id', 'number', 'parameters', 'treatment_template', 'status', '__v');
should(data.sample_id.toString()).be.eql('400000000000000000000001'); should(data.sample_id.toString()).be.eql('400000000000000000000001');
should(data).have.property('number', 'B1'); should(data).have.property('number', 'A1');
should(data.treatment_template.toString()).be.eql('200000000000000000000001'); should(data.treatment_template.toString()).be.eql('200000000000000000000001');
should(data).have.property('status', 0); should(data).have.property('status', 0);
should(data).have.property('parameters'); should(data).have.property('parameters');
@ -129,7 +133,17 @@ describe('/condition', () => {
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 200, httpStatus: 200,
req: {parameters: {weeks: 8}}, req: {parameters: {weeks: 8}},
res: {_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'B1', treatment_template: '200000000000000000000001', parameters: {material: 'copper', weeks: 8}} res: {_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'A1', treatment_template: '200000000000000000000001', parameters: {material: 'copper', weeks: 8}}
});
});
it('rejects changing the condition number', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/condition/700000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'C2'},
res: {status: 'Invalid body format', details: '"number" is not allowed'}
}); });
}); });
it('rejects not specified parameters', done => { it('rejects not specified parameters', done => {
@ -198,7 +212,7 @@ describe('/condition', () => {
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 200, httpStatus: 200,
req: {parameters: {material: 'hot air', weeks: 10}}, req: {parameters: {material: 'hot air', weeks: 10}},
res: {_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'B1', treatment_template: '200000000000000000000001', parameters: {material: 'hot air', weeks: 10}} res: {_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'A1', treatment_template: '200000000000000000000001', parameters: {material: 'hot air', weeks: 10}}
}); });
}); });
it('rejects an API key', done => { it('rejects an API key', done => {
@ -227,34 +241,41 @@ describe('/condition', () => {
req: {parameters: {material: 'hot air', weeks: 10}} req: {parameters: {material: 'hot air', weeks: 10}}
}); });
}); });
}); // TODO: how to deal with template changes? Template versioning? });
// TODO: rewrite delete methods -> set status for every database collection
describe('DELETE /condition/{id}', () => { describe('DELETE /condition/{id}', () => {
it('sets the status to deleted', done => { it('sets the status to deleted', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'delete', method: 'delete',
url: '/condition/700000000000000000000002', url: '/condition/700000000000000000000004',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 200 httpStatus: 200
}).end((err, res) => { }).end((err, res) => {
if (err) return done(err); if (err) return done(err);
should(res.body).be.eql({status: 'OK'}); should(res.body).be.eql({status: 'OK'});
ConditionModel.findById('700000000000000000000002').lean().exec((err, data: any) => { ConditionModel.findById('700000000000000000000004').lean().exec((err, data: any) => {
if (err) return done(err); if (err) return done(err);
should(data).have.only.keys('_id', 'sample_id', 'number', 'parameters', 'treatment_template', 'status', '__v'); should(data).have.only.keys('_id', 'sample_id', 'number', 'parameters', 'treatment_template', 'status', '__v');
should(data.sample_id.toString()).be.eql('400000000000000000000002'); should(data.sample_id.toString()).be.eql('400000000000000000000001');
should(data).have.property('number', 'B1'); should(data).have.property('number', 'A2');
should(data.treatment_template.toString()).be.eql('200000000000000000000001'); should(data.treatment_template.toString()).be.eql('200000000000000000000001');
should(data).have.property('status', -1); should(data).have.property('status', -1);
should(data).have.property('parameters'); should(data).have.property('parameters');
should(data.parameters).have.property('material', 'copper'); should(data.parameters).have.property('material', 'hot air');
should(data.parameters).have.property('weeks', 3); should(data.parameters).have.property('weeks', 5);
done(); done();
}); });
}); });
}); });
it('rejects a deleting a condition referenced by measurements'); // TODO it('rejects deleting a condition referenced by measurements'/*, done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/condition/700000000000000000000002',
auth: {basic: 'janedoe'},
httpStatus: 200,
res: {status: 'Condition still in use'}
});
}*/); // TODO after decision
it('rejects an invalid id', done => { it('rejects an invalid id', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'delete', method: 'delete',
@ -266,7 +287,7 @@ describe('/condition', () => {
it('rejects an API key', done => { it('rejects an API key', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'delete', method: 'delete',
url: '/condition/700000000000000000000002', url: '/condition/700000000000000000000004',
auth: {key: 'janedoe'}, auth: {key: 'janedoe'},
httpStatus: 401 httpStatus: 401
}); });
@ -274,7 +295,7 @@ describe('/condition', () => {
it('rejects requests from a read user', done => { it('rejects requests from a read user', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'delete', method: 'delete',
url: '/condition/700000000000000000000002', url: '/condition/700000000000000000000004',
auth: {basic: 'user'}, auth: {basic: 'user'},
httpStatus: 403 httpStatus: 403
}); });
@ -290,7 +311,7 @@ describe('/condition', () => {
it('accepts an maintain/admin user deleting a condition belonging to a sample of another user', done => { it('accepts an maintain/admin user deleting a condition belonging to a sample of another user', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'delete', method: 'delete',
url: '/condition/700000000000000000000002', url: '/condition/700000000000000000000004',
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 200 httpStatus: 200
}).end((err, res) => { }).end((err, res) => {
@ -302,7 +323,7 @@ describe('/condition', () => {
it('returns 404 for an unknown id', done => { it('returns 404 for an unknown id', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'delete', method: 'delete',
url: '/condition/00000000000w000000000002', url: '/condition/000000000000000000000002',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 404 httpStatus: 404
}); });
@ -310,26 +331,26 @@ describe('/condition', () => {
it('rejects unauthorized requests', done => { it('rejects unauthorized requests', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'delete', method: 'delete',
url: '/condition/700000000000000000000002', url: '/condition/700000000000000000000004',
httpStatus: 401 httpStatus: 401
}); });
}); });
}); });
describe('POST /condition/new', () => { // TODO: sample number generation describe('POST /condition/new', () => {
it('returns the right condition', done => { it('returns the right condition', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'post', method: 'post',
url: '/condition/new', url: '/condition/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 200, httpStatus: 200,
req: {sample_id: '400000000000000000000002', number: 'B2', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'} req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'}
}).end((err, res) => { }).end((err, res) => {
if (err) return done(err); if (err) return done(err);
should(res.body).have.only.keys('_id', 'sample_id', 'number', 'parameters', 'treatment_template'); should(res.body).have.only.keys('_id', 'sample_id', 'number', 'parameters', 'treatment_template');
should(res.body).have.property('_id').be.type('string'); should(res.body).have.property('_id').be.type('string');
should(res.body).have.property('sample_id', '400000000000000000000002'); should(res.body).have.property('sample_id', '400000000000000000000002');
should(res.body).have.property('number', 'B2'); should(res.body).have.property('number', 'A2');
should(res.body).have.property('treatment_template', '200000000000000000000001'); should(res.body).have.property('treatment_template', '200000000000000000000001');
should(res.body).have.property('parameters'); should(res.body).have.property('parameters');
should(res.body.parameters).have.property('material', 'hot air'); should(res.body.parameters).have.property('material', 'hot air');
@ -343,14 +364,37 @@ describe('/condition', () => {
url: '/condition/new', url: '/condition/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 200, httpStatus: 200,
req: {sample_id: '400000000000000000000002', number: 'B2', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'} req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'}
}).end((err, res) => { }).end((err, res) => {
if (err) return done(err); if (err) return done(err);
ConditionModel.findById(res.body._id).lean().exec((err, data: any) => { ConditionModel.findById(res.body._id).lean().exec((err, data: any) => {
if (err) return done(err); if (err) return done(err);
should(data).have.only.keys('_id', 'sample_id', 'number', 'parameters', 'treatment_template', 'status', '__v'); should(data).have.only.keys('_id', 'sample_id', 'number', 'parameters', 'treatment_template', 'status', '__v');
should(data.sample_id.toString()).be.eql('400000000000000000000002'); should(data.sample_id.toString()).be.eql('400000000000000000000002');
should(data).have.property('number', 'B2'); should(data).have.property('number', 'A2');
should(data.treatment_template.toString()).be.eql('200000000000000000000001');
should(data).have.property('status', 0);
should(data).have.property('parameters');
should(data.parameters).have.property('material', 'hot air');
should(data.parameters).have.property('weeks', 10);
done();
});
});
});
it('stores the first condition as 1', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/condition/new',
auth: {basic: 'admin'},
httpStatus: 200,
req: {sample_id: '400000000000000000000003', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'}
}).end((err, res) => {
if (err) return done(err);
ConditionModel.findById(res.body._id).lean().exec((err, data: any) => {
if (err) return done(err);
should(data).have.only.keys('_id', 'sample_id', 'number', 'parameters', 'treatment_template', 'status', '__v');
should(data.sample_id.toString()).be.eql('400000000000000000000003');
should(data).have.property('number', 'A1');
should(data.treatment_template.toString()).be.eql('200000000000000000000001'); should(data.treatment_template.toString()).be.eql('200000000000000000000001');
should(data).have.property('status', 0); should(data).have.property('status', 0);
should(data).have.property('parameters'); should(data).have.property('parameters');
@ -366,7 +410,7 @@ describe('/condition', () => {
url: '/condition/new', url: '/condition/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {sample_id: '4000000000h0000000000002', number: 'B2', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'}, req: {sample_id: '4000000000h0000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'},
res: {status: 'Invalid body format', details: '"sample_id" with value "4000000000h0000000000002" fails to match the required pattern: /[0-9a-f]{24}/'} res: {status: 'Invalid body format', details: '"sample_id" with value "4000000000h0000000000002" fails to match the required pattern: /[0-9a-f]{24}/'}
}); });
}); });
@ -376,7 +420,7 @@ describe('/condition', () => {
url: '/condition/new', url: '/condition/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {sample_id: '000000000000000000000002', number: 'B2', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'}, req: {sample_id: '000000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'},
res: {status: 'Sample id not available'} res: {status: 'Sample id not available'}
}); });
}); });
@ -386,7 +430,7 @@ describe('/condition', () => {
url: '/condition/new', url: '/condition/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {sample_id: '400000000000000000000002', number: 'B2', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000h00000000001'}, req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000h00000000001'},
res: {status: 'Invalid body format', details: '"treatment_template" with value "200000000000h00000000001" fails to match the required pattern: /[0-9a-f]{24}/'} res: {status: 'Invalid body format', details: '"treatment_template" with value "200000000000h00000000001" fails to match the required pattern: /[0-9a-f]{24}/'}
}); });
}); });
@ -396,18 +440,18 @@ describe('/condition', () => {
url: '/condition/new', url: '/condition/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {sample_id: '400000000000000000000002', number: 'B2', parameters: {material: 'hot air', weeks: 10}, treatment_template: '000000000000000000000001'}, req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '000000000000000000000001'},
res: {status: 'Treatment template not available'} res: {status: 'Treatment template not available'}
}); });
}); });
it('rejects a condition number already in use for this sample', done => { it('rejects setting a condition number', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'post', method: 'post',
url: '/condition/new', url: '/condition/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {sample_id: '400000000000000000000001', number: 'B1', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'}, req: {sample_id: '400000000000000000000001', number: 'A7', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'},
res: {status: 'Condition number already taken'} res: {status: 'Invalid body format', details: '"number" is not allowed'}
}); });
}); });
it('rejects not specified parameters', done => { it('rejects not specified parameters', done => {
@ -416,7 +460,7 @@ describe('/condition', () => {
url: '/condition/new', url: '/condition/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {sample_id: '400000000000000000000002', number: 'B2', parameters: {material: 'hot air', weeks: 10, xx: 12}, treatment_template: '200000000000000000000001'}, req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10, xx: 12}, treatment_template: '200000000000000000000001'},
res: {status: 'Invalid body format', details: '"xx" is not allowed'} res: {status: 'Invalid body format', details: '"xx" is not allowed'}
}); });
}); });
@ -426,7 +470,7 @@ describe('/condition', () => {
url: '/condition/new', url: '/condition/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {sample_id: '400000000000000000000002', number: 'B2', parameters: {material: 'hot air'}, treatment_template: '200000000000000000000001'}, req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air'}, treatment_template: '200000000000000000000001'},
res: {status: 'Invalid body format', details: '"weeks" is required'} res: {status: 'Invalid body format', details: '"weeks" is required'}
}); });
}); });
@ -436,7 +480,7 @@ describe('/condition', () => {
url: '/condition/new', url: '/condition/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {sample_id: '400000000000000000000002', number: 'B2', parameters: {material: 'xxx', weeks: 10}, treatment_template: '200000000000000000000001'}, req: {sample_id: '400000000000000000000002', parameters: {material: 'xxx', weeks: 10}, treatment_template: '200000000000000000000001'},
res: {status: 'Invalid body format', details: '"material" must be one of [copper, hot air]'} res: {status: 'Invalid body format', details: '"material" must be one of [copper, hot air]'}
}); });
}); });
@ -446,7 +490,7 @@ describe('/condition', () => {
url: '/condition/new', url: '/condition/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {sample_id: '400000000000000000000002', number: 'B2', parameters: {material: 'hot air', weeks: -10}, treatment_template: '200000000000000000000001'}, req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: -10}, treatment_template: '200000000000000000000001'},
res: {status: 'Invalid body format', details: '"weeks" must be larger than or equal to 1'} res: {status: 'Invalid body format', details: '"weeks" must be larger than or equal to 1'}
}); });
}); });
@ -456,7 +500,7 @@ describe('/condition', () => {
url: '/condition/new', url: '/condition/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {sample_id: '400000000000000000000002', number: 'B2', parameters: {material: 'hot air', weeks: 11}, treatment_template: '200000000000000000000001'}, req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 11}, treatment_template: '200000000000000000000001'},
res: {status: 'Invalid body format', details: '"weeks" must be less than or equal to 10'} res: {status: 'Invalid body format', details: '"weeks" must be less than or equal to 10'}
}); });
}); });
@ -466,7 +510,7 @@ describe('/condition', () => {
url: '/condition/new', url: '/condition/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {number: 'B2', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'}, req: {parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'},
res: {status: 'Invalid body format', details: '"sample_id" is required'} res: {status: 'Invalid body format', details: '"sample_id" is required'}
}); });
}); });
@ -476,27 +520,17 @@ describe('/condition', () => {
url: '/condition/new', url: '/condition/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {sample_id: '400000000000000000000002', number: 'B2', parameters: {material: 'hot air', weeks: 10}}, req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}},
res: {status: 'Invalid body format', details: '"treatment_template" is required'} res: {status: 'Invalid body format', details: '"treatment_template" is required'}
}); });
}); });
it('rejects a missing number', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/condition/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'},
res: {status: 'Invalid body format', details: '"number" is required'}
});
});
it('rejects adding a condition to the sample of an other user for a write user', done => { it('rejects adding a condition to the sample of an other user for a write user', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'post', method: 'post',
url: '/condition/new', url: '/condition/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 403, httpStatus: 403,
req: {sample_id: '400000000000000000000003', number: 'B2', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'} req: {sample_id: '400000000000000000000003', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'}
}); });
}); });
it('accepts adding a condition to the sample of an other user for a maintain/admin user', done => { it('accepts adding a condition to the sample of an other user for a maintain/admin user', done => {
@ -505,13 +539,13 @@ describe('/condition', () => {
url: '/condition/new', url: '/condition/new',
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 200, httpStatus: 200,
req: {sample_id: '400000000000000000000002', number: 'B2', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'} req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'}
}).end((err, res) => { }).end((err, res) => {
if (err) return done(err); if (err) return done(err);
should(res.body).have.only.keys('_id', 'sample_id', 'number', 'parameters', 'treatment_template'); should(res.body).have.only.keys('_id', 'sample_id', 'number', 'parameters', 'treatment_template');
should(res.body).have.property('_id').be.type('string'); should(res.body).have.property('_id').be.type('string');
should(res.body).have.property('sample_id', '400000000000000000000002'); should(res.body).have.property('sample_id', '400000000000000000000002');
should(res.body).have.property('number', 'B2'); should(res.body).have.property('number', 'A2');
should(res.body).have.property('treatment_template', '200000000000000000000001'); should(res.body).have.property('treatment_template', '200000000000000000000001');
should(res.body).have.property('parameters'); should(res.body).have.property('parameters');
should(res.body.parameters).have.property('material', 'hot air'); should(res.body.parameters).have.property('material', 'hot air');
@ -525,7 +559,7 @@ describe('/condition', () => {
url: '/condition/new', url: '/condition/new',
auth: {key: 'janedoe'}, auth: {key: 'janedoe'},
httpStatus: 401, httpStatus: 401,
req: {sample_id: '400000000000000000000002', number: 'B2', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'} req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'}
}); });
}); });
it('rejects requests from a read user', done => { it('rejects requests from a read user', done => {
@ -534,7 +568,7 @@ describe('/condition', () => {
url: '/condition/new', url: '/condition/new',
auth: {basic: 'user'}, auth: {basic: 'user'},
httpStatus: 403, httpStatus: 403,
req: {sample_id: '400000000000000000000002', number: 'B2', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'} req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'}
}); });
}); });
it('rejects unauthorized requests', done => { it('rejects unauthorized requests', done => {
@ -542,7 +576,7 @@ describe('/condition', () => {
method: 'post', method: 'post',
url: '/condition/new', url: '/condition/new',
httpStatus: 401, httpStatus: 401,
req: {sample_id: '400000000000000000000002', number: 'B2', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'} req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'}
}); });
}); });
}); });

View File

@ -1,5 +1,4 @@
import express from 'express'; import express from 'express';
import mongoose from 'mongoose';
import _ from 'lodash'; import _ from 'lodash';
import ConditionValidate from './validate/condition'; import ConditionValidate from './validate/condition';
@ -38,13 +37,14 @@ router.put('/condition/' + IdValidate.parameter(), async (req, res, next) => {
if (!data) { if (!data) {
res.status(404).json({status: 'Not found'}); res.status(404).json({status: 'Not found'});
} }
// add properties needed for sampleIdCheck // add properties needed for sampleIdCheck
condition.treatment_template = data.treatment_template; condition.treatment_template = data.treatment_template;
condition.sample_id = data.sample_id; condition.sample_id = data.sample_id;
if (!await sampleIdCheck(condition, req, res, next)) return; if (!await sampleIdCheck(condition, req, res, next)) return;
if (condition.parameters) { if (condition.parameters) {
condition.parameters = _.assign({}, data.parameters, condition.parameters); condition.parameters = _.assign({}, data.parameters, condition.parameters);
if (!_.isEqual(condition.parameters, data.parameters)) { if (!_.isEqual(condition.parameters, data.parameters)) { // parameters did not change
condition.status = 0; condition.status = 0;
} }
} }
@ -79,10 +79,12 @@ router.post('/condition/new', async (req, res, next) => {
if (error) return res400(error, res); if (error) return res400(error, res);
if (!await sampleIdCheck(condition, req, res, next)) return; if (!await sampleIdCheck(condition, req, res, next)) return;
if (!await numberCheck(condition, res, next)) return; const treatmentData = await treatmentCheck(condition, 'new', res, next)
if (!await treatmentCheck(condition, 'new', res, next)) return; if (!treatmentData) return;
condition.status = 0; condition.number = await numberGenerate(condition, treatmentData, next);
if (!condition.number) return;
condition.status = 0; // set status to new
await new ConditionModel(condition).save((err, data) => { await new ConditionModel(condition).save((err, data) => {
if (err) return next(err); if (err) return next(err);
res.json(ConditionValidate.output(data.toObject())); res.json(ConditionValidate.output(data.toObject()));
@ -104,24 +106,28 @@ async function sampleIdCheck (condition, req, res, next) { // validate sample_i
return true; return true;
} }
async function numberCheck (condition, res, next) { // validate number, returns false if invalid async function numberGenerate (condition, treatmentData, next) { // generate number, returns false on error
const data = await ConditionModel.find({sample_id: new mongoose.Types.ObjectId(condition.sample_id), number: condition.number}).lean().exec().catch(err => {next(err); return false;}) as any; const conditionData = await ConditionModel // find condition with highest number belonging to the same sample
if (data.length) { .find({sample_id: condition.sample_id, number: new RegExp('^' + treatmentData.number_prefix + '[0-9]+$', 'm')})
res.status(400).json({status: 'Condition number already taken'}); .sort({number: -1})
return false; .limit(1)
} .lean()
return true; .exec()
.catch(err => next(err)) as any;
if (conditionData instanceof Error) return false;
return treatmentData.number_prefix + (conditionData.length > 0 ? Number(conditionData[0].number.replace(/[^0-9]+/g, '')) + 1 : 1); // return new number
} }
async function treatmentCheck (condition, param, res, next) { async function treatmentCheck (condition, param, res, next) { // validate treatment template, returns false if invalid, otherwise template data
const treatmentData = await TreatmentTemplateModel.findById(condition.treatment_template).lean().exec().catch(err => {next(err); return false;}) as any; const treatmentData = await TreatmentTemplateModel.findById(condition.treatment_template).lean().exec().catch(err => next(err)) as any;
if (treatmentData instanceof Error) return false;
if (!treatmentData) { // template not found if (!treatmentData) { // template not found
res.status(400).json({status: 'Treatment template not available'}); res.status(400).json({status: 'Treatment template not available'});
return false return false;
} }
// validate parameters // validate parameters
const {error, value: ignore} = ParametersValidate.input(condition.parameters, treatmentData.parameters, param); const {error, value: ignore} = ParametersValidate.input(condition.parameters, treatmentData.parameters, param);
if (error) {res400(error, res); return false;} if (error) {res400(error, res); return false;}
return true; return treatmentData;
} }

View File

@ -1,9 +1,10 @@
import should from 'should/as-function'; import should from 'should/as-function';
import _ from 'lodash';
import MaterialModel from '../models/material'; import MaterialModel from '../models/material';
import TestHelper from "../test/helper"; import TestHelper from "../test/helper";
// TODO: numbers with color only (no number) // TODO: color name must be unique to get color number
// TODO: deal with numbers with leading zeros // TODO: separate supplier/ material name into own collections
describe('/material', () => { describe('/material', () => {
let server; let server;
@ -21,7 +22,6 @@ describe('/material', () => {
}).end((err, res) => { }).end((err, res) => {
if (err) return done(err); if (err) return done(err);
const json = require('../test/db.json'); const json = require('../test/db.json');
console.log(res.body);
should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === 10).length); should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === 10).length);
should(res.body).matchEach(material => { should(res.body).matchEach(material => {
should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers');
@ -35,7 +35,7 @@ describe('/material', () => {
should(material.numbers).matchEach(number => { should(material.numbers).matchEach(number => {
should(number).have.only.keys('color', 'number'); should(number).have.only.keys('color', 'number');
should(number).have.property('color').be.type('string'); should(number).have.property('color').be.type('string');
should(number).have.property('number').be.type('number'); should(number).have.property('number').be.type('string');
}); });
}); });
done(); done();
@ -63,7 +63,7 @@ describe('/material', () => {
should(material.numbers).matchEach(number => { should(material.numbers).matchEach(number => {
should(number).have.only.keys('color', 'number'); should(number).have.only.keys('color', 'number');
should(number).have.property('color').be.type('string'); should(number).have.property('color').be.type('string');
should(number).have.property('number').be.type('number'); should(number).have.property('number').be.type('string');
}); });
}); });
done(); done();
@ -78,6 +78,101 @@ describe('/material', () => {
}); });
}); });
describe('GET /materials/{group}', () => {
it('returns all new materials', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/materials/new',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
const json = require('../test/db.json');
let asyncCounter = res.body.length;
should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === 0).length);
should(res.body).matchEach(material => {
should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers');
should(material).have.property('_id').be.type('string');
should(material).have.property('name').be.type('string');
should(material).have.property('supplier').be.type('string');
should(material).have.property('group').be.type('string');
should(material).have.property('mineral').be.type('number');
should(material).have.property('glass_fiber').be.type('number');
should(material).have.property('carbon_fiber').be.type('number');
should(material.numbers).matchEach(number => {
should(number).have.only.keys('color', 'number');
should(number).have.property('color').be.type('string');
should(number).have.property('number').be.type('string');
});
MaterialModel.findById(material._id).lean().exec((err, data) => {
should(data).have.property('status', 0);
if (--asyncCounter === 0) {
done();
}
});
});
});
});
it('returns all deleted materials', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/materials/deleted',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
const json = require('../test/db.json');
let asyncCounter = res.body.length;
should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === -1).length);
should(res.body).matchEach(material => {
should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers');
should(material).have.property('_id').be.type('string');
should(material).have.property('name').be.type('string');
should(material).have.property('supplier').be.type('string');
should(material).have.property('group').be.type('string');
should(material).have.property('mineral').be.type('number');
should(material).have.property('glass_fiber').be.type('number');
should(material).have.property('carbon_fiber').be.type('number');
should(material.numbers).matchEach(number => {
should(number).have.only.keys('color', 'number');
should(number).have.property('color').be.type('string');
should(number).have.property('number').be.type('string');
});
MaterialModel.findById(material._id).lean().exec((err, data) => {
should(data).have.property('status', -1);
if (--asyncCounter === 0) {
done();
}
});
});
done();
});
});
it('rejects requests from a write user', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/materials/new',
auth: {basic: 'janedoe'},
httpStatus: 403
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/materials/deleted',
auth: {key: 'admin'},
httpStatus: 401
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/materials/new',
httpStatus: 401
});
});
});
describe('GET /material/{id}', () => { describe('GET /material/{id}', () => {
it('returns the right material', done => { it('returns the right material', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
@ -85,7 +180,7 @@ describe('/material', () => {
url: '/material/100000000000000000000001', url: '/material/100000000000000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 200, httpStatus: 200,
res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}, {color: 'natural', number: 5514263422}]} res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: '5514263423'}, {color: 'natural', number: '5514263422'}]}
}); });
}); });
it('returns the right material for an API key', done => { it('returns the right material for an API key', done => {
@ -97,6 +192,15 @@ describe('/material', () => {
res: {_id: '100000000000000000000003', name: 'PA GF 50 black (2706)', supplier: 'Akro-Plastic', group: 'PA66+PA6I/6T', mineral: 0, glass_fiber: 0, carbon_fiber: 0, numbers: []} res: {_id: '100000000000000000000003', name: 'PA GF 50 black (2706)', supplier: 'Akro-Plastic', group: 'PA66+PA6I/6T', mineral: 0, glass_fiber: 0, carbon_fiber: 0, numbers: []}
}); });
}); });
it('returns a material with a color without number', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/material/100000000000000000000007',
auth: {basic: 'janedoe'},
httpStatus: 200,
res: {_id: '100000000000000000000007', name: 'Ultramid A4H', supplier: 'BASF', group: 'PA66', mineral: 0, glass_fiber: 0, carbon_fiber: 0, numbers: [{color: 'black', number: ''}]}
});
});
it('rejects an invalid id', done => { it('rejects an invalid id', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'get', method: 'get',
@ -130,7 +234,7 @@ describe('/material', () => {
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 200, httpStatus: 200,
req: {}, req: {},
res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}, {color: 'natural', number: 5514263422}]} res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: '5514263423'}, {color: 'natural', number: '5514263422'}]}
}); });
}); });
it('keeps unchanged properties', done => { it('keeps unchanged properties', done => {
@ -139,10 +243,10 @@ describe('/material', () => {
url: '/material/100000000000000000000001', url: '/material/100000000000000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 200, httpStatus: 200,
req: {name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}, {color: 'natural', number: 5514263422}]} req: {name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: '5514263423'}, {color: 'natural', number: '5514263422'}]}
}).end((err, res) => { }).end((err, res) => {
if (err) return done(err); if (err) return done(err);
should(res.body).be.eql({_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}, {color: 'natural', number: 5514263422}]}); should(res.body).be.eql({_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: '5514263423'}, {color: 'natural', number: '5514263422'}]});
MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => {
if (err) return done(err); if (err) return done(err);
should(data).have.property('status', 10); should(data).have.property('status', 10);
@ -159,7 +263,7 @@ describe('/material', () => {
req: {name: 'Stanyl TW 200 F8'} req: {name: 'Stanyl TW 200 F8'}
}).end((err, res) => { }).end((err, res) => {
if (err) return done(err); if (err) return done(err);
should(res.body).be.eql({_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}, {color: 'natural', number: 5514263422}]}); should(res.body).be.eql({_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: '5514263423'}, {color: 'natural', number: '5514263422'}]});
MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => {
if (err) return done(err); if (err) return done(err);
should(data).have.property('status', 10); should(data).have.property('status', 10);
@ -173,20 +277,30 @@ describe('/material', () => {
url: '/material/100000000000000000000001', url: '/material/100000000000000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 200, httpStatus: 200,
req: {name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}]} req: {name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: '5514212901'}, {color: 'signalviolet', number: '5514612901'}]}
, ,
}).end((err, res) => { }).end((err, res) => {
if (err) return done(err); if (err) return done(err);
should(res.body).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}]}); should(res.body).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: '5514212901'}, {color: 'signalviolet', number: '5514612901'}]});
MaterialModel.findById('100000000000000000000001').lean().exec((err, data:any) => { MaterialModel.findById('100000000000000000000001').lean().exec((err, data:any) => {
if (err) return done(err); if (err) return done(err);
data._id = data._id.toString(); data._id = data._id.toString();
data.numbers = data.numbers.map(e => {return {color: e.color, number: e.number}}); data.numbers = data.numbers.map(e => {return {color: e.color, number: e.number}});
should(data).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: '0', glass_fiber: '35', carbon_fiber: '0', numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}], status: 0, __v: 0}); should(data).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: '0', glass_fiber: '35', carbon_fiber: '0', numbers: [{color: 'black', number: '5514212901'}, {color: 'signalviolet', number: '5514612901'}], status: 0, __v: 0});
done(); done();
}); });
}); });
}); });
it('accepts a color without number', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/material/100000000000000000000007',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {numbers: [{color: 'black', number: ''}, {color: 'natural', number: ''}]},
res: {_id: '100000000000000000000007', name: 'Ultramid A4H', supplier: 'BASF', group: 'PA66', mineral: 0, glass_fiber: 0, carbon_fiber: 0, numbers: [{color: 'black', number: ''}, {color: 'natural', number: ''}]}
});
})
it('rejects already existing material names', done => { it('rejects already existing material names', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'put', method: 'put',
@ -233,20 +347,10 @@ describe('/material', () => {
url: '/material/100000000000000000000001', url: '/material/100000000000000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {numbers: [{colorxx: 'black', number: 55}]}, req: {numbers: [{colorxx: 'black', number: '55'}]},
res: {status: 'Invalid body format', details: '"numbers[0].color" is required'} 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 => { it('rejects an invalid id', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'put', method: 'put',
@ -307,7 +411,7 @@ describe('/material', () => {
if (err) return done(err); if (err) return done(err);
data._id = data._id.toString(); data._id = data._id.toString();
data.numbers = data.numbers.map(e => {return {color: e.color, number: e.number}}); data.numbers = data.numbers.map(e => {return {color: e.color, number: e.number}});
should(data).be.eql({_id: '100000000000000000000002', name: 'Ultramid T KR 4355 G7', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}], status: -1, __v: 0} should(data).be.eql({_id: '100000000000000000000002', name: 'Ultramid T KR 4355 G7', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: '5514212901'}, {color: 'signalviolet', number: '5514612901'}], status: -1, __v: 0}
); );
done(); done();
}); });
@ -370,7 +474,7 @@ describe('/material', () => {
url: '/material/new', url: '/material/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 200, httpStatus: 200,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]} req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: '05515798402'}]}
}).end((err, res) => { }).end((err, res) => {
if (err) return done (err); if (err) return done (err);
should(res.body).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); should(res.body).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers');
@ -384,7 +488,7 @@ describe('/material', () => {
should(res.body.numbers).matchEach(number => { should(res.body.numbers).matchEach(number => {
should(number).have.only.keys('color', 'number'); should(number).have.only.keys('color', 'number');
should(number).have.property('color', 'black'); should(number).have.property('color', 'black');
should(number).have.property('number', 5515798402); should(number).have.property('number', '05515798402');
}); });
done(); done();
}); });
@ -415,13 +519,52 @@ describe('/material', () => {
}); });
}); });
}); });
it('accepts a color without number', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/material/new',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: ''}]}
}).end((err, res) => {
if (err) return done (err);
should(res.body).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers');
should(res.body).have.property('_id').be.type('string');
should(res.body).have.property('name', 'Crastin CE 2510');
should(res.body).have.property('supplier', 'Du Pont');
should(res.body).have.property('group', 'PBT');
should(res.body).have.property('mineral', 0);
should(res.body).have.property('glass_fiber', 30);
should(res.body).have.property('carbon_fiber', 0);
should(res.body.numbers).matchEach(number => {
should(number).have.only.keys('color', 'number');
should(number).have.property('color', 'black');
should(number).have.property('number', '');
});
MaterialModel.find({name: 'Crastin CE 2510'}).lean().exec((err, data: any) => {
if (err) return done (err);
should(data).have.lengthOf(1);
should(data[0]).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers', 'status', '__v');
should(data[0]).have.property('_id');
should(data[0]).have.property('name', 'Crastin CE 2510');
should(data[0]).have.property('supplier', 'Du Pont');
should(data[0]).have.property('group', 'PBT');
should(data[0]).have.property('mineral', '0');
should(data[0]).have.property('glass_fiber', '30');
should(data[0]).have.property('carbon_fiber', '0');
should(data[0]).have.property('status', 0);
should(_.omit(data[0].numbers[0], '_id')).be.eql({color: 'black', number: ''});
done();
});
});
});
it('rejects already existing material names', done => { it('rejects already existing material names', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'post', method: 'post',
url: '/material/new', url: '/material/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}]}, req: {name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: '5514263423'}]},
res: {status: 'Material name already taken'} res: {status: 'Material name already taken'}
}); });
}); });
@ -431,7 +574,7 @@ describe('/material', () => {
url: '/material/new', url: '/material/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]}, 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'} res: {status: 'Invalid body format', details: '"name" is required'}
}); });
}); });
@ -441,7 +584,7 @@ describe('/material', () => {
url: '/material/new', url: '/material/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {name: 'Crastin CE 2510', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]}, 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'} res: {status: 'Invalid body format', details: '"supplier" is required'}
}); });
}); });
@ -451,7 +594,7 @@ describe('/material', () => {
url: '/material/new', url: '/material/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]}, 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'} res: {status: 'Invalid body format', details: '"group" is required'}
}); });
}); });
@ -461,7 +604,7 @@ describe('/material', () => {
url: '/material/new', url: '/material/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]}, 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'} res: {status: 'Invalid body format', details: '"mineral" is required'}
}); });
}); });
@ -471,7 +614,7 @@ describe('/material', () => {
url: '/material/new', url: '/material/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]}, 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'} res: {status: 'Invalid body format', details: '"glass_fiber" is required'}
}); });
}); });
@ -481,7 +624,7 @@ describe('/material', () => {
url: '/material/new', url: '/material/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, numbers: [{color: 'black', number: 5515798402}]}, 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'} res: {status: 'Invalid body format', details: '"carbon_fiber" is required'}
}); });
}); });
@ -501,7 +644,7 @@ describe('/material', () => {
url: '/material/new', url: '/material/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{number: 5515798402}]}, 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'} res: {status: 'Invalid body format', details: '"numbers[0].color" is required'}
}); });
}); });

View File

@ -21,6 +21,22 @@ router.get('/materials', (req, res, next) => {
}); });
}); });
router.get('/materials/:group(new|deleted)', (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
let status;
switch (req.params.group) {
case 'new': status = 0;
break;
case 'deleted': status = -1;
break;
}
MaterialModel.find({status: status}).lean().exec((err, data) => {
if (err) return next(err);
res.json(_.compact(data.map(e => MaterialValidate.output(e)))); // validate all and filter null values from validation errors
});
});
router.get('/material/' + IdValidate.parameter(), (req, res, next) => { router.get('/material/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
@ -51,7 +67,7 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => {
// check for changes // check for changes
if (!_.isEqual(_.pick(IdValidate.stringify(materialData), _.keys(material)), material)) { if (!_.isEqual(_.pick(IdValidate.stringify(materialData), _.keys(material)), material)) {
material.status = 0; material.status = 0; // set status to new
} }
await MaterialModel.findByIdAndUpdate(req.params.id, material, {new: true}).lean().exec((err, data) => { await MaterialModel.findByIdAndUpdate(req.params.id, material, {new: true}).lean().exec((err, data) => {
@ -85,13 +101,12 @@ router.delete('/material/' + IdValidate.parameter(), (req, res, next) => {
router.post('/material/new', async (req, res, next) => { router.post('/material/new', async (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
// validate input
const {error, value: material} = MaterialValidate.input(req.body, 'new'); const {error, value: material} = MaterialValidate.input(req.body, 'new');
if (error) return res400(error, res); if (error) return res400(error, res);
if (!await nameCheck(material, res, next)) return; if (!await nameCheck(material, res, next)) return;
material.status = 0; material.status = 0; // set status to new
await new MaterialModel(material).save((err, data) => { await new MaterialModel(material).save((err, data) => {
if (err) return next(err); if (err) return next(err);
res.json(MaterialValidate.output(data.toObject())); res.json(MaterialValidate.output(data.toObject()));
@ -103,7 +118,7 @@ module.exports = router;
async function nameCheck (material, res, next) { // check if name was already taken async function nameCheck (material, res, next) { // check if name was already taken
const materialData = await MaterialModel.findOne({name: material.name}).lean().exec().catch(err => {next(err); return false;}) as any; const materialData = await MaterialModel.findOne({name: material.name}).lean().exec().catch(err => next(err)) as any;
if (materialData instanceof Error) return false; if (materialData instanceof Error) return false;
if (materialData) { // could not find material_id if (materialData) { // could not find material_id
res.status(400).json({status: 'Material name already taken'}); res.status(400).json({status: 'Material name already taken'});

View File

@ -2,6 +2,9 @@ import should from 'should/as-function';
import MeasurementModel from '../models/measurement'; import MeasurementModel from '../models/measurement';
import TestHelper from "../test/helper"; import TestHelper from "../test/helper";
// TODO: allow empty values
describe('/measurement', () => { describe('/measurement', () => {
let server; let server;
before(done => TestHelper.before(done)); before(done => TestHelper.before(done));

View File

@ -36,16 +36,20 @@ router.put('/measurement/' + IdValidate.parameter(), async (req, res, next) => {
if (!data) { if (!data) {
res.status(404).json({status: 'Not found'}); res.status(404).json({status: 'Not found'});
} }
// add properties needed for conditionIdCheck // add properties needed for conditionIdCheck
measurement.measurement_template = data.measurement_template; measurement.measurement_template = data.measurement_template;
measurement.condition_id = data.condition_id; measurement.condition_id = data.condition_id;
if (!await conditionIdCheck(measurement, req, res, next)) return; if (!await conditionIdCheck(measurement, req, res, next)) return;
// check for changes
if (measurement.values) { if (measurement.values) {
measurement.values = _.assign({}, data.values, measurement.values); measurement.values = _.assign({}, data.values, measurement.values);
if (!_.isEqual(measurement.values, data.values)) { if (!_.isEqual(measurement.values, data.values)) {
measurement.status = 0; measurement.status = 0; // set status to new
} }
} }
if (!await templateCheck(measurement, 'change', res, next)) return; if (!await templateCheck(measurement, 'change', res, next)) return;
await MeasurementModel.findByIdAndUpdate(req.params.id, measurement, {new: true}).lean().exec((err, data) => { await MeasurementModel.findByIdAndUpdate(req.params.id, measurement, {new: true}).lean().exec((err, data) => {
if (err) return next(err); if (err) return next(err);
@ -99,7 +103,7 @@ async function conditionIdCheck (measurement, req, res, next) { // validate con
return true; return true;
} }
async function templateCheck (measurement, param, res, next) { // validate measurement_template and values async function templateCheck (measurement, param, res, next) { // validate measurement_template and values, param for new/change
const templateData = await MeasurementTemplateModel.findById(measurement.measurement_template).lean().exec().catch(err => {next(err); return false;}) as any; const templateData = await MeasurementTemplateModel.findById(measurement.measurement_template).lean().exec().catch(err => {next(err); return false;}) as any;
if (!templateData) { // template not found if (!templateData) { // template not found
res.status(400).json({status: 'Measurement template not available'}); res.status(400).json({status: 'Measurement template not available'});
@ -108,7 +112,6 @@ async function templateCheck (measurement, param, res, next) { // validate meas
// validate values // validate values
const {error, value: ignore} = ParametersValidate.input(measurement.values, templateData.parameters, param); const {error, value: ignore} = ParametersValidate.input(measurement.values, templateData.parameters, param);
console.log(error);
if (error) {res400(error, res); return false;} if (error) {res400(error, res); return false;}
return true; return true;
} }

View File

@ -3,8 +3,10 @@ import SampleModel from '../models/sample';
import NoteModel from '../models/note'; import NoteModel from '../models/note';
import NoteFieldModel from '../models/note_field'; import NoteFieldModel from '../models/note_field';
import TestHelper from "../test/helper"; import TestHelper from "../test/helper";
// TODO: generate sample number
// TODO: think again which parameters are required at POST // TODO: generate output for ML in format DPT -> data, implement filtering, field selection
// TODO: write script for data import
// TODO: delete everything (measurements, condition) with sample
describe('/sample', () => { describe('/sample', () => {
let server; let server;
@ -23,16 +25,16 @@ describe('/sample', () => {
if (err) return done(err); if (err) return done(err);
const json = require('../test/db.json'); const json = require('../test/db.json');
should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status === 10).length); should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status === 10).length);
should(res.body).matchEach(material => { should(res.body).matchEach(sample => {
should(material).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id'); should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id');
should(material).have.property('_id').be.type('string'); should(sample).have.property('_id').be.type('string');
should(material).have.property('number').be.type('string'); should(sample).have.property('number').be.type('string');
should(material).have.property('type').be.type('string'); should(sample).have.property('type').be.type('string');
should(material).have.property('color').be.type('string'); should(sample).have.property('color').be.type('string');
should(material).have.property('batch').be.type('string'); should(sample).have.property('batch').be.type('string');
should(material).have.property('material_id').be.type('string'); should(sample).have.property('material_id').be.type('string');
should(material).have.property('note_id'); should(sample).have.property('note_id');
should(material).have.property('user_id').be.type('string'); should(sample).have.property('user_id').be.type('string');
}); });
done(); done();
}); });
@ -70,6 +72,94 @@ describe('/sample', () => {
}); });
}); });
describe('GET /samples/{group}', () => {
it('returns all new samples', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/samples/new',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
const json = require('../test/db.json');
let asyncCounter = res.body.length;
should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status === 0).length);
should(res.body).matchEach(sample => {
should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id');
should(sample).have.property('_id').be.type('string');
should(sample).have.property('number').be.type('string');
should(sample).have.property('type').be.type('string');
should(sample).have.property('color').be.type('string');
should(sample).have.property('batch').be.type('string');
should(sample).have.property('material_id').be.type('string');
should(sample).have.property('note_id');
should(sample).have.property('user_id').be.type('string');
SampleModel.findById(sample._id).lean().exec((err, data) => {
should(data).have.property('status', 0);
if (--asyncCounter === 0) {
done();
}
});
});
done();
});
});
it('returns all deleted samples', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/samples/deleted',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
const json = require('../test/db.json');
let asyncCounter = res.body.length;
should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status === -1).length);
should(res.body).matchEach(sample => {
should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id');
should(sample).have.property('_id').be.type('string');
should(sample).have.property('number').be.type('string');
should(sample).have.property('type').be.type('string');
should(sample).have.property('color').be.type('string');
should(sample).have.property('batch').be.type('string');
should(sample).have.property('material_id').be.type('string');
should(sample).have.property('note_id');
should(sample).have.property('user_id').be.type('string');
SampleModel.findById(sample._id).lean().exec((err, data) => {
should(data).have.property('status', -1);
if (--asyncCounter === 0) {
done();
}
});
});
done();
});
});
it('rejects requests from a write user', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/samples/new',
auth: {basic: 'janedoe'},
httpStatus: 403
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/samples/new',
auth: {key: 'admin'},
httpStatus: 401
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/samples/new',
httpStatus: 401
});
});
});
describe('PUT /sample/{id}', () => { describe('PUT /sample/{id}', () => {
it('returns the right sample', done => { it('returns the right sample', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
@ -87,7 +177,7 @@ describe('/sample', () => {
url: '/sample/400000000000000000000001', url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 200, httpStatus: 200,
req: {number: '1', type: 'granulate', color: 'black', batch: '', material_id: '100000000000000000000004', notes: {}} req: {type: 'granulate', color: 'black', batch: '', material_id: '100000000000000000000004', notes: {}}
}).end((err, res) => { }).end((err, res) => {
if (err) return done(err); if (err) return done(err);
should(res.body).be.eql({_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'}); should(res.body).be.eql({_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'});
@ -156,14 +246,14 @@ describe('/sample', () => {
url: '/sample/400000000000000000000001', url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 200, 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'}]}} req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}
}).end(err => { }).end(err => {
if (err) return done (err); if (err) return done (err);
SampleModel.findById('400000000000000000000001').lean().exec((err, data: any) => { SampleModel.findById('400000000000000000000001').lean().exec((err, data: any) => {
if (err) return done (err); if (err) return done (err);
should(data).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'material_id', 'note_id', 'user_id', 'status', '__v'); should(data).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'material_id', 'note_id', 'user_id', 'status', '__v');
should(data).have.property('_id'); should(data).have.property('_id');
should(data).have.property('number', '10'); should(data).have.property('number', '1');
should(data).have.property('color', 'signalviolet'); should(data).have.property('color', 'signalviolet');
should(data).have.property('type', 'part'); should(data).have.property('type', 'part');
should(data).have.property('batch', '114531'); should(data).have.property('batch', '114531');
@ -194,12 +284,10 @@ describe('/sample', () => {
}).end(err => { }).end(err => {
if (err) return done(err); if (err) return done(err);
NoteFieldModel.findOne({name: 'not allowed for new applications'}).lean().exec((err, data) => { NoteFieldModel.findOne({name: 'not allowed for new applications'}).lean().exec((err, data) => {
console.log(data);
if (err) return done(err); if (err) return done(err);
should(data).have.property('qty', 1); should(data).have.property('qty', 1);
NoteFieldModel.findOne({name: 'field1'}).lean().exec((err, data) => { NoteFieldModel.findOne({name: 'field1'}).lean().exec((err, data) => {
if (err) return done(err); if (err) return done(err);
console.log(data);
should(data).have.property('qty', 1); should(data).have.property('qty', 1);
done(); done();
}); });
@ -228,12 +316,11 @@ describe('/sample', () => {
url: '/sample/400000000000000000000002', url: '/sample/400000000000000000000002',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 200, httpStatus: 200,
req: {number: '111'} req: {type: 'part'}
}).end((err, res) => { }).end((err, res) => {
if (err) return done (err); if (err) return done (err);
NoteModel.findById(res.body.note_id).lean().exec((err, data) => { NoteModel.findById(res.body.note_id).lean().exec((err, data) => {
if (err) return done (err); if (err) return done (err);
console.log(data);
should(data).not.be.null(); should(data).not.be.null();
should(data).have.property('comment', 'Stoff gesperrt'); should(data).have.property('comment', 'Stoff gesperrt');
should(data).have.property('sample_references').have.lengthOf(0); should(data).have.property('sample_references').have.lengthOf(0);
@ -263,7 +350,7 @@ describe('/sample', () => {
url: '/sample/400000000000000000000001', url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, 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'}]}}, req: {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'} res: {status: 'Color not available for material'}
}); });
}); });
@ -273,18 +360,18 @@ describe('/sample', () => {
url: '/sample/400000000000000000000001', url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, 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'}]}}, req: {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'} res: {status: 'Material not available'}
}); });
}); });
it('rejects a sample number in use', done => { it('rejects a sample number', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'put', method: 'put',
url: '/sample/400000000000000000000001', url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, 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'}]}}, req: {number: 25, 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'} res: {status: 'Invalid body format', details: '"number" is not allowed'}
}); });
}); });
it('rejects an invalid sample reference', done => { it('rejects an invalid sample reference', done => {
@ -293,7 +380,7 @@ describe('/sample', () => {
url: '/sample/400000000000000000000001', url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, 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'}]}}, req: {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'} res: {status: 'Sample reference not available'}
}); });
}); });
@ -303,7 +390,7 @@ describe('/sample', () => {
url: '/sample/400000000000000000000001', url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, 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'}]}}, req: {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}/'} res: {status: 'Invalid body format', details: '"material_id" with value "10000000000h000000000001" fails to match the required pattern: /[0-9a-f]{24}/'}
}); });
}); });
@ -313,7 +400,7 @@ describe('/sample', () => {
url: '/sample/10000000000h000000000001', url: '/sample/10000000000h000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 404, 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'}]}}, req: {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 => { it('rejects an API key', done => {
@ -322,7 +409,7 @@ describe('/sample', () => {
url: '/sample/400000000000000000000001', url: '/sample/400000000000000000000001',
auth: {key: 'janedoe'}, auth: {key: 'janedoe'},
httpStatus: 401, 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'}]}}, req: {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 => { it('rejects changes for samples from another user for a write user', done => {
@ -350,7 +437,7 @@ describe('/sample', () => {
url: '/sample/400000000000000000000001', url: '/sample/400000000000000000000001',
auth: {basic: 'user'}, auth: {basic: 'user'},
httpStatus: 403, 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'}]}}, req: {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 => { it('returns 404 for an unknown sample', done => {
@ -359,7 +446,7 @@ describe('/sample', () => {
url: '/sample/000000000000000000000001', url: '/sample/000000000000000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 404, 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'}]}} req: {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 => { it('rejects unauthorized requests', done => {
@ -367,7 +454,7 @@ describe('/sample', () => {
method: 'put', method: 'put',
url: '/sample/400000000000000000000001', url: '/sample/400000000000000000000001',
httpStatus: 401, 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'}]}}, req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
}); });
}); });
}); });
@ -448,7 +535,6 @@ describe('/sample', () => {
setTimeout(() => { // background action takes some time before we can check setTimeout(() => { // background action takes some time before we can check
NoteModel.findById('500000000000000000000003').lean().exec((err, data: any) => { NoteModel.findById('500000000000000000000003').lean().exec((err, data: any) => {
if (err) return done(err); if (err) return done(err);
console.log(data);
should(data).have.property('sample_references').with.lengthOf(1); should(data).have.property('sample_references').with.lengthOf(1);
should(data.sample_references[0].id.toString()).be.eql('400000000000000000000003'); should(data.sample_references[0].id.toString()).be.eql('400000000000000000000003');
should(data.sample_references[0]).have.property('relation', 'part to sample'); should(data.sample_references[0]).have.property('relation', 'part to sample');
@ -490,6 +576,7 @@ describe('/sample', () => {
httpStatus: 404 httpStatus: 404
}); });
}); });
it('rejects deleting a sample referenced by conditions'); // TODO after decision
it('rejects requests from a read user', done => { it('rejects requests from a read user', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'delete', method: 'delete',
@ -530,12 +617,12 @@ describe('/sample', () => {
url: '/sample/new', url: '/sample/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 200, httpStatus: 200,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}
}).end((err, res) => { }).end((err, res) => {
if (err) return done (err); if (err) return done (err);
should(res.body).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'material_id', 'note_id', 'user_id'); should(res.body).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'material_id', 'note_id', 'user_id');
should(res.body).have.property('_id').be.type('string'); should(res.body).have.property('_id').be.type('string');
should(res.body).have.property('number', 'Rng172'); should(res.body).have.property('number', 'Rng34');
should(res.body).have.property('color', 'black'); should(res.body).have.property('color', 'black');
should(res.body).have.property('type', 'granulate'); should(res.body).have.property('type', 'granulate');
should(res.body).have.property('batch', '1560237365'); should(res.body).have.property('batch', '1560237365');
@ -551,15 +638,15 @@ describe('/sample', () => {
url: '/sample/new', url: '/sample/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 200, httpStatus: 200,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}
}).end(err => { }).end(err => {
if (err) return done (err); if (err) return done (err);
SampleModel.find({number: 'Rng172'}).lean().exec((err, data: any) => { SampleModel.find({number: 'Rng34'}).lean().exec((err, data: any) => {
if (err) return done (err); if (err) return done (err);
should(data).have.lengthOf(1); should(data).have.lengthOf(1);
should(data[0]).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'material_id', 'note_id', 'user_id', 'status', '__v'); should(data[0]).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'material_id', 'note_id', 'user_id', 'status', '__v');
should(data[0]).have.property('_id'); should(data[0]).have.property('_id');
should(data[0]).have.property('number', 'Rng172'); should(data[0]).have.property('number', 'Rng34');
should(data[0]).have.property('color', 'black'); should(data[0]).have.property('color', 'black');
should(data[0]).have.property('type', 'granulate'); should(data[0]).have.property('type', 'granulate');
should(data[0]).have.property('batch', '1560237365'); should(data[0]).have.property('batch', '1560237365');
@ -586,7 +673,7 @@ describe('/sample', () => {
url: '/sample/new', url: '/sample/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 200, httpStatus: 200,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [], custom_fields: {field1: 'a', field2: 'b', 'not allowed for new applications': true}}} req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [], custom_fields: {field1: 'a', field2: 'b', 'not allowed for new applications': true}}}
}).end((err, res) => { }).end((err, res) => {
if (err) return done (err); if (err) return done (err);
NoteModel.findById(res.body.note_id).lean().exec((err, data: any) => { NoteModel.findById(res.body.note_id).lean().exec((err, data: any) => {
@ -617,13 +704,34 @@ describe('/sample', () => {
}); });
}); });
}); });
it('stores a new sample location as 1', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'johnnydoe'},
httpStatus: 200,
req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{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', 'material_id', 'note_id', 'user_id');
should(res.body).have.property('_id').be.type('string');
should(res.body).have.property('number', 'Fe1');
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('material_id', '100000000000000000000001');
should(res.body).have.property('note_id').be.type('string');
should(res.body).have.property('user_id', '000000000000000000000004');
done();
});
});
it('rejects a color not defined for the material', done => { it('rejects a color not defined for the material', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'post', method: 'post',
url: '/sample/new', url: '/sample/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {number: 'Rng172', color: 'green', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, req: {color: 'green', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Color not available for material'} res: {status: 'Color not available for material'}
}); });
}); });
@ -633,18 +741,18 @@ describe('/sample', () => {
url: '/sample/new', url: '/sample/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '000000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '000000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Material not available'} res: {status: 'Material not available'}
}); });
}); });
it('rejects a sample number in use', done => { it('rejects a sample number', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'post', method: 'post',
url: '/sample/new', url: '/sample/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {number: '1', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, req: {number: 'Rng34', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Sample number already taken'} res: {status: 'Invalid body format', details: '"number" is not allowed'}
}); });
}); });
it('rejects an invalid sample reference', done => { it('rejects an invalid sample reference', done => {
@ -653,7 +761,7 @@ describe('/sample', () => {
url: '/sample/new', url: '/sample/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '000000000000000000000003', relation: 'part to this sample'}]}}, req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '000000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Sample reference not available'} res: {status: 'Sample reference not available'}
}); });
}); });
@ -663,27 +771,17 @@ describe('/sample', () => {
url: '/sample/new', url: '/sample/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {number: 'Rng172', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, req: {type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Invalid body format', details: '"color" is required'} res: {status: 'Invalid body format', details: '"color" is required'}
}); });
}); });
it('rejects a missing sample number', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
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', details: '"number" is required'}
});
});
it('rejects a missing type', done => { it('rejects a missing type', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'post', method: 'post',
url: '/sample/new', url: '/sample/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {number: 'Rng172', color: 'black', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, req: {color: 'black', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Invalid body format', details: '"type" is required'} res: {status: 'Invalid body format', details: '"type" is required'}
}); });
}); });
@ -693,7 +791,7 @@ describe('/sample', () => {
url: '/sample/new', url: '/sample/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {number: 'Rng172', color: 'black', type: 'granulate', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, req: {color: 'black', type: 'granulate', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Invalid body format', details: '"batch" is required'} res: {status: 'Invalid body format', details: '"batch" is required'}
}); });
}); });
@ -703,7 +801,7 @@ describe('/sample', () => {
url: '/sample/new', url: '/sample/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, req: {color: 'black', type: 'granulate', batch: '1560237365', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Invalid body format', details: '"material_id" is required'} res: {status: 'Invalid body format', details: '"material_id" is required'}
}); });
}); });
@ -713,7 +811,7 @@ describe('/sample', () => {
url: '/sample/new', url: '/sample/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, 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'}]}}, req: {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', details: '"material_id" with value "10000000000h000000000001" fails to match the required pattern: /[0-9a-f]{24}/'} res: {status: 'Invalid body format', details: '"material_id" with value "10000000000h000000000001" fails to match the required pattern: /[0-9a-f]{24}/'}
}); });
}); });
@ -723,7 +821,7 @@ describe('/sample', () => {
url: '/sample/new', url: '/sample/new',
auth: {key: 'janedoe'}, auth: {key: 'janedoe'},
httpStatus: 401, httpStatus: 401,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}
}); });
}); });
it('rejects requests from a read user', done => { it('rejects requests from a read user', done => {
@ -732,7 +830,7 @@ describe('/sample', () => {
url: '/sample/new', url: '/sample/new',
auth: {basic: 'user'}, auth: {basic: 'user'},
httpStatus: 403, httpStatus: 403,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}
}); });
}); });
it('rejects unauthorized requests', done => { it('rejects unauthorized requests', done => {
@ -740,7 +838,7 @@ describe('/sample', () => {
method: 'post', method: 'post',
url: '/sample/new', url: '/sample/new',
httpStatus: 401, httpStatus: 401,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}
}); });
}); });
}); });

View File

@ -22,6 +22,22 @@ router.get('/samples', (req, res, next) => {
}) })
}); });
router.get('/samples/:group(new|deleted)', (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
let status;
switch (req.params.group) {
case 'new': status = 0;
break;
case 'deleted': status = -1;
break;
}
SampleModel.find({status: status}).lean().exec((err, data) => {
if (err) return next(err);
res.json(_.compact(data.map(e => SampleValidate.output(e)))); // validate all and filter null values from validation errors
})
});
router.put('/sample/' + IdValidate.parameter(), (req, res, next) => { router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
@ -33,12 +49,10 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
if (!sampleData) { if (!sampleData) {
return res.status(404).json({status: 'Not found'}); return res.status(404).json({status: 'Not found'});
} }
// only maintain and admin are allowed to edit other user's data // 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 (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return;
if (sample.hasOwnProperty('number') && sample.number !== sampleData.number) {
if (!await numberCheck(sample, res, next)) return;
}
if (sample.hasOwnProperty('material_id')) { if (sample.hasOwnProperty('material_id')) {
if (!await materialCheck(sample, res, next)) return; if (!await materialCheck(sample, res, next)) return;
} }
@ -51,12 +65,12 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
if (sampleData.note_id !== null) { // old notes data exists if (sampleData.note_id !== null) { // old notes data exists
const data = await NoteModel.findById(sampleData.note_id).lean().exec().catch(err => {next(err);}) as any; const data = await NoteModel.findById(sampleData.note_id).lean().exec().catch(err => {next(err);}) as any;
if (data instanceof Error) return; if (data instanceof Error) return;
newNotes = !_.isEqual(_.pick(IdValidate.stringify(data), _.keys(sample.notes)), sample.notes); newNotes = !_.isEqual(_.pick(IdValidate.stringify(data), _.keys(sample.notes)), sample.notes); // check if notes were changed
if (newNotes) { if (newNotes) {
if (data.hasOwnProperty('custom_fields')) { // update note_fields if (data.hasOwnProperty('custom_fields')) { // update note_fields
customFieldsChange(Object.keys(data.custom_fields), -1); customFieldsChange(Object.keys(data.custom_fields), -1);
} }
NoteModel.findByIdAndDelete(sampleData.note_id).lean().exec(err => { // delete old notes await NoteModel.findByIdAndDelete(sampleData.note_id).lean().exec(err => { // delete old notes
if (err) return console.error(err); if (err) return console.error(err);
}); });
} }
@ -77,7 +91,8 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
if (!_.isEqual(_.pick(IdValidate.stringify(sampleData), _.keys(sample)), _.omit(sample, ['notes']))) { if (!_.isEqual(_.pick(IdValidate.stringify(sampleData), _.keys(sample)), _.omit(sample, ['notes']))) {
sample.status = 0; sample.status = 0;
} }
SampleModel.findByIdAndUpdate(req.params.id, sample, {new: true}).lean().exec((err, data) => {
await SampleModel.findByIdAndUpdate(req.params.id, sample, {new: true}).lean().exec((err, data) => {
if (err) return next(err); if (err) return next(err);
res.json(SampleValidate.output(data)); res.json(SampleValidate.output(data));
}); });
@ -93,12 +108,13 @@ router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => {
if (!sampleData) { if (!sampleData) {
return res.status(404).json({status: 'Not found'}); return res.status(404).json({status: 'Not found'});
} }
// only maintain and admin are allowed to edit other user's data // 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 (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return;
SampleModel.findByIdAndUpdate(req.params.id, {status: -1}).lean().exec(err => { // set sample status await SampleModel.findByIdAndUpdate(req.params.id, {status: -1}).lean().exec(err => { // set sample status
if (err) return next(err); if (err) return next(err);
if (sampleData.note_id !== null) { if (sampleData.note_id !== null) { // handle notes
NoteModel.findById(sampleData.note_id).lean().exec((err, data: any) => { // find notes to update note_fields NoteModel.findById(sampleData.note_id).lean().exec((err, data: any) => { // find notes to update note_fields
if (err) return next(err); if (err) return next(err);
if (data.hasOwnProperty('custom_fields')) { // update note_fields if (data.hasOwnProperty('custom_fields')) { // update note_fields
@ -120,7 +136,6 @@ router.post('/sample/new', async (req, res, next) => {
const {error, value: sample} = SampleValidate.input(req.body, 'new'); const {error, value: sample} = SampleValidate.input(req.body, 'new');
if (error) return res400(error, res); if (error) return res400(error, res);
if (!await numberCheck(sample, res, next)) return;
if (!await materialCheck(sample, res, next)) return; if (!await materialCheck(sample, res, next)) return;
if (!await sampleRefCheck(sample, res, next)) return; if (!await sampleRefCheck(sample, res, next)) return;
@ -128,13 +143,15 @@ router.post('/sample/new', async (req, res, next) => {
customFieldsChange(Object.keys(sample.notes.custom_fields), 1); customFieldsChange(Object.keys(sample.notes.custom_fields), 1);
} }
sample.status = 0; sample.status = 0; // set status to new
new NoteModel(sample.notes).save((err, data) => { sample.number = await numberGenerate(sample, req, res, next);
if (!sample.number) return;
await new NoteModel(sample.notes).save((err, data) => { // save notes
if (err) return next(err); if (err) return next(err);
delete sample.notes; delete sample.notes;
sample.note_id = data._id; sample.note_id = data._id;
sample.user_id = req.authDetails.id; sample.user_id = req.authDetails.id;
console.log(sample);
new SampleModel(sample).save((err, data) => { new SampleModel(sample).save((err, data) => {
if (err) return next(err); if (err) return next(err);
res.json(SampleValidate.output(data.toObject())); res.json(SampleValidate.output(data.toObject()));
@ -155,17 +172,18 @@ router.get('/sample/notes/fields', (req, res, next) => {
module.exports = router; module.exports = router;
async function numberCheck (sample, res, next) { // validate number, returns false if invalid async function numberGenerate (sample, req, res, next) { // generate number, returns false on error
const sampleData = await SampleModel.findOne({number: sample.number}).lean().exec().catch(err => {next(err); return false;}); const sampleData = await SampleModel
if (sampleData) { // found entry with sample number .find({number: new RegExp('^' + req.authDetails.location + '[0-9]+$', 'm')})
res.status(400).json({status: 'Sample number already taken'}); .lean()
return false .exec()
} .catch(err => next(err));
return true; if (sampleData instanceof Error) return false;
return req.authDetails.location + (sampleData.length > 0 ? Number(sampleData[0].number.replace(/[^0-9]+/g, '')) + 1 : 1);
} }
async function materialCheck (sample, res, next, id = sample.material_id) { // validate material_id and color, returns false if invalid 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); return false;}) as any; const materialData = await MaterialModel.findById(id).lean().exec().catch(err => next(err)) as any;
if (materialData instanceof Error) return false; if (materialData instanceof Error) return false;
if (!materialData) { // could not find material_id if (!materialData) { // could not find material_id
res.status(400).json({status: 'Material not available'}); res.status(400).json({status: 'Material not available'});
@ -181,7 +199,8 @@ async function materialCheck (sample, res, next, id = sample.material_id) { //
function sampleRefCheck (sample, res, next) { // validate sample_references, resolves false for invalid reference function sampleRefCheck (sample, res, next) { // validate sample_references, resolves false for invalid reference
return new Promise(resolve => { return new Promise(resolve => {
if (sample.notes.sample_references.length > 0) { // there are sample_references if (sample.notes.sample_references.length > 0) { // there are sample_references
let referencesCount = sample.notes.sample_references.length; let referencesCount = sample.notes.sample_references.length; // count to keep track of running async operations
sample.notes.sample_references.forEach(reference => { sample.notes.sample_references.forEach(reference => {
SampleModel.findById(reference.id).lean().exec((err, data) => { SampleModel.findById(reference.id).lean().exec((err, data) => {
if (err) {next(err); resolve(false)} if (err) {next(err); resolve(false)}
@ -190,7 +209,7 @@ function sampleRefCheck (sample, res, next) { // validate sample_references, re
return resolve(false); return resolve(false);
} }
referencesCount --; referencesCount --;
if (referencesCount <= 0) { if (referencesCount <= 0) { // all async requests done
resolve(true); resolve(true);
} }
}); });
@ -202,7 +221,7 @@ function sampleRefCheck (sample, res, next) { // validate sample_references, re
}); });
} }
function customFieldsChange (fields, amount) { function customFieldsChange (fields, amount) { // update custom_fields and respective quantities
fields.forEach(field => { fields.forEach(field => {
NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: amount}}, {new: true}).lean().exec((err, data: any) => { // 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 (err) return console.error(err);

View File

@ -4,7 +4,7 @@ import TemplateTreatmentModel from '../models/treatment_template';
import TemplateMeasurementModel from '../models/measurement_template'; import TemplateMeasurementModel from '../models/measurement_template';
import TestHelper from "../test/helper"; import TestHelper from "../test/helper";
// TODO: do not allow usage of old templates for new samples
describe('/template', () => { describe('/template', () => {
let server; let server;
@ -201,7 +201,6 @@ describe('/template', () => {
httpStatus: 200, httpStatus: 200,
req: {parameters: [{name: 'time', range: {type: 'array'}}]} req: {parameters: [{name: 'time', range: {type: 'array'}}]}
}).end((err, res) => { }).end((err, res) => {
console.log(res.body);
if (err) return done(err); if (err) return done(err);
should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, number_prefix: 'A', parameters: [{name: 'time', range: {type: 'array'}}]}); should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, number_prefix: 'A', parameters: [{name: 'time', range: {type: 'array'}}]});
done(); done();
@ -371,14 +370,14 @@ describe('/template', () => {
res: {status: 'Invalid body format', details: '"parameters[0].name" is required'} res: {status: 'Invalid body format', details: '"parameters[0].name" is required'}
}); });
}); });
it('rejects a missing number prefix', done => { it('rejects a number prefix containing numbers', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'post', method: 'post',
url: '/template/treatment/new', url: '/template/treatment/new',
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 400, httpStatus: 400,
req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]}, req: {name: 'heat aging', number_prefix: 'AB5', parameters: [{name: 'time', range: {min: 1}}]},
res: {status: 'Invalid body format', details: '"number_prefix" is required'} res: {status: 'Invalid body format', details: '"number_prefix" with value "AB5" fails to match the required pattern: /^[a-zA-Z]+$/'}
}); });
}); });
it('rejects a missing parameter range', done => { it('rejects a missing parameter range', done => {

View File

@ -7,14 +7,14 @@ import TemplateMeasurementModel from '../models/measurement_template';
import res400 from './validate/res400'; import res400 from './validate/res400';
import IdValidate from './validate/id'; import IdValidate from './validate/id';
// TODO: remove f() for await
const router = express.Router(); const router = express.Router();
router.get('/template/:collection(measurements|treatments)', (req, res, next) => { router.get('/template/:collection(measurements|treatments)', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return;
req.params.collection = req.params.collection.replace(/s$/g, ''); req.params.collection = req.params.collection.replace(/s$/g, ''); // remove trailing s
model(req).find({}).lean().exec((err, data) => { model(req).find({}).lean().exec((err, data) => {
if (err) next (err); if (err) next (err);
res.json(_.compact(data.map(e => TemplateValidate.output(e, req.params.collection)))); // 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
@ -52,8 +52,8 @@ router.put('/template/:collection(measurement|treatment)/' + IdValidate.paramete
} }
if (!_.isEqual(_.pick(templateData, _.keys(template)), template)) { // data was changed if (!_.isEqual(_.pick(templateData, _.keys(template)), template)) { // data was changed
template.version = templateData.version + 1; template.version = templateData.version + 1; // increase version
await new (model(req))(_.assign({}, _.omit(templateData, ['_id', '__v']), template)).save((err, data) => { await new (model(req))(_.assign({}, _.omit(templateData, ['_id', '__v']), template)).save((err, data) => { // save new template, fill with old properties
if (err) next (err); if (err) next (err);
res.json(TemplateValidate.output(data.toObject(), req.params.collection)); res.json(TemplateValidate.output(data.toObject(), req.params.collection));
}); });
@ -73,7 +73,7 @@ router.post('/template/:collection(measurement|treatment)/new', async (req, res,
if (!await numberPrefixCheck(template, req, res, next)) return; if (!await numberPrefixCheck(template, req, res, next)) return;
} }
template.version = 1; template.version = 1; // set template version
await new (model(req))(template).save((err, data) => { await new (model(req))(template).save((err, data) => {
if (err) next (err); if (err) next (err);
res.json(TemplateValidate.output(data.toObject(), req.params.collection)); res.json(TemplateValidate.output(data.toObject(), req.params.collection));
@ -84,7 +84,7 @@ router.post('/template/:collection(measurement|treatment)/new', async (req, res,
module.exports = router; module.exports = router;
async function numberPrefixCheck (template, req, res, next) { async function numberPrefixCheck (template, req, res, next) { // check if number_prefix is available
const data = await model(req).findOne({number_prefix: template.number_prefix}).lean().exec().catch(err => {next(err); return false;}) as any; const data = await model(req).findOne({number_prefix: template.number_prefix}).lean().exec().catch(err => {next(err); return false;}) as any;
if (data) { if (data) {
res.status(400).json({status: 'Number prefix already taken'}); res.status(400).json({status: 'Number prefix already taken'});
@ -93,6 +93,6 @@ async function numberPrefixCheck (template, req, res, next) {
return true; return true;
} }
function model (req) { function model (req) { // return right template model
return req.params.collection === 'treatment' ? TemplateTreatmentModel : TemplateMeasurementModel; return req.params.collection === 'treatment' ? TemplateTreatmentModel : TemplateMeasurementModel;
} }

View File

@ -2,6 +2,7 @@ import should from 'should/as-function';
import UserModel from '../models/user'; import UserModel from '../models/user';
import TestHelper from "../test/helper"; import TestHelper from "../test/helper";
// TODO: reject usernames containing admin, etc.
describe('/user', () => { describe('/user', () => {
let server; let server;

View File

@ -20,14 +20,10 @@ router.get('/users', (req, res) => {
}); });
router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new. See https://forbeslindesay.github.io/express-route-tester/ for the generated regex router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new. See https://forbeslindesay.github.io/express-route-tester/ for the generated regex
req.params.username = req.params[0];
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return;
let username = req.authDetails.username;
if (req.params.username !== undefined) {
if (!req.auth(res, ['admin'], 'basic')) return;
username = req.params.username;
}
const username = getUsername(req, res);
if (!username) return;
UserModel.findOne({name: username}).lean().exec( (err, data:any) => { UserModel.findOne({name: username}).lean().exec( (err, data:any) => {
if (err) return next(err); if (err) return next(err);
if (data) { if (data) {
@ -39,14 +35,13 @@ router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi
}); });
}); });
router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new router.put('/user:username([/](?!key|new).?*|/?)', async (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new
req.params.username = req.params[0];
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return;
let username = req.authDetails.username;
if (req.params.username !== undefined) { const username = getUsername(req, res);
if (!req.auth(res, ['admin'], 'basic')) return; if (!username) return;
username = req.params.username; console.log(username);
}
const {error, value: user} = UserValidate.input(req.body, 'change' + (req.authDetails.level === 'admin'? 'admin' : '')); const {error, value: user} = UserValidate.input(req.body, 'change' + (req.authDetails.level === 'admin'? 'admin' : ''));
if (error) return res400(error, res); if (error) return res400(error, res);
@ -56,45 +51,25 @@ router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi
// check that user does not already exist if new name was specified // check that user does not already exist if new name was specified
if (user.hasOwnProperty('name') && user.name !== username) { if (user.hasOwnProperty('name') && user.name !== username) {
UserModel.find({name: user.name}).lean().exec( (err, data:any) => { if (!await usernameCheck(user.name, res, next)) return;
if (err) return next(err); }
if (data.length > 0 || UserValidate.isSpecialName(user.name)) {
res.status(400).json({status: 'Username already taken'});
return;
}
UserModel.findOneAndUpdate({name: username}, user, {new: true}).lean().exec( (err, data:any) => { await UserModel.findOneAndUpdate({name: username}, user, {new: true}).lean().exec( (err, data:any) => {
if (err) return next(err); if (err) return next(err);
if (data) { if (data) {
res.json(UserValidate.output(data)); res.json(UserValidate.output(data));
} }
else { else {
res.status(404).json({status: 'Not found'}); res.status(404).json({status: 'Not found'});
} }
}); });
});
}
else {
UserModel.findOneAndUpdate({name: username}, user, {new: true}).lean().exec( (err, data:any) => {
if (err) return next(err);
if (data) {
res.json(UserValidate.output(data)); // validate all and filter null values from validation errors
}
else {
res.status(404).json({status: 'Not found'});
}
});
}
}); });
router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new. See https://forbeslindesay.github.io/express-route-tester/ for the generated regex router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new. See https://forbeslindesay.github.io/express-route-tester/ for the generated regex
req.params.username = req.params[0];
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return;
let username = req.authDetails.username;
if (req.params.username !== undefined) { const username = getUsername(req, res);
if (!req.auth(res, ['admin'], 'basic')) return; if (!username) return;
username = req.params.username;
}
UserModel.findOneAndDelete({name: username}).lean().exec( (err, data:any) => { UserModel.findOneAndDelete({name: username}).lean().exec( (err, data:any) => {
if (err) return next(err); if (err) return next(err);
@ -116,7 +91,7 @@ router.get('/user/key', (req, res, next) => {
}); });
}); });
router.post('/user/new', (req, res, next) => { router.post('/user/new', async (req, res, next) => {
if (!req.auth(res, ['admin'], 'basic')) return; if (!req.auth(res, ['admin'], 'basic')) return;
// validate input // validate input
@ -124,20 +99,14 @@ router.post('/user/new', (req, res, next) => {
if (error) return res400(error, res); if (error) return res400(error, res);
// check that user does not already exist // check that user does not already exist
UserModel.find({name: user.name}).lean().exec( (err, data:any) => { if (!await usernameCheck(user.name, res, next)) return;
if (err) return next(err);
if (data.length > 0 || UserValidate.isSpecialName(user.name)) {
res.status(400).json({status: 'Username already taken'});
return;
}
user.key = mongoose.Types.ObjectId(); // use object id as unique API key user.key = mongoose.Types.ObjectId(); // use object id as unique API key
bcrypt.hash(user.pass, 10, (err, hash) => { // password hashing bcrypt.hash(user.pass, 10, (err, hash) => { // password hashing
user.pass = hash; user.pass = hash;
new UserModel(user).save((err, data) => { // store user new UserModel(user).save((err, data) => { // store user
if (err) return next(err); if (err) return next(err);
res.json(UserValidate.output(data.toObject())); res.json(UserValidate.output(data.toObject()));
});
}); });
}); });
}); });
@ -147,11 +116,14 @@ router.post('/user/passreset', (req, res, next) => {
UserModel.find({name: req.body.name, email: req.body.email}).lean().exec( (err, data: any) => { UserModel.find({name: req.body.name, email: req.body.email}).lean().exec( (err, data: any) => {
if (err) return next(err); if (err) return next(err);
if (data.length === 1) { // it exists if (data.length === 1) { // it exists
const newPass = Math.random().toString(36).substring(2); const newPass = Math.random().toString(36).substring(2); // generate temporary password
bcrypt.hash(newPass, 10, (err, hash) => { // password hashing bcrypt.hash(newPass, 10, (err, hash) => { // password hashing
if (err) return next(err); if (err) return next(err);
UserModel.findByIdAndUpdate(data[0]._id, {pass: hash}, err => { // write new password UserModel.findByIdAndUpdate(data[0]._id, {pass: hash}, err => { // write new password
if (err) return next(err); if (err) return next(err);
// send email
mail(data[0].email, 'Your new password for the DFOP database', 'Hi, <br><br> You requested to reset your password.<br>Your new password is:<br><br>' + newPass + '<br><br>If you did not request a password reset, talk to the sysadmin quickly!<br><br>Have a nice day.<br><br>The DFOP team', err => { mail(data[0].email, 'Your new password for the DFOP database', 'Hi, <br><br> You requested to reset your password.<br>Your new password is:<br><br>' + newPass + '<br><br>If you did not request a password reset, talk to the sysadmin quickly!<br><br>Have a nice day.<br><br>The DFOP team', err => {
if (err) return next(err); if (err) return next(err);
res.json({status: 'OK'}); res.json({status: 'OK'});
@ -166,4 +138,27 @@ router.post('/user/passreset', (req, res, next) => {
}); });
module.exports = router; module.exports = router;
function getUsername (req, res) { // returns username or false if action is not allowed
req.params.username = req.params[0]; // because of path regex
if (req.params.username !== undefined) { // different username than request user
if (!req.auth(res, ['admin'], 'basic')) return false;
return req.params.username;
}
else {
return req.authDetails.username;
}
}
async function usernameCheck (name, res, next) { // check if username is already taken
const userData = await UserModel.findOne({name: name}).lean().exec().catch(err => next(err)) as any;
if (userData instanceof Error) return false;
console.log(userData);
console.log(UserValidate.isSpecialName(name));
if (userData || UserValidate.isSpecialName(name)) {
res.status(400).json({status: 'Username already taken'});
return false;
}
return true;
}

View File

@ -18,18 +18,16 @@ export default class ConditionValidate {
) )
} }
static input (data, param) { static input (data, param) { // validate input, set param to 'new' to make all attributes required
if (param === 'new') { if (param === 'new') {
return Joi.object({ return Joi.object({
sample_id: IdValidate.get().required(), sample_id: IdValidate.get().required(),
number: this.condition.number.required(),
parameters: this.condition.parameters.required(), parameters: this.condition.parameters.required(),
treatment_template: IdValidate.get().required() treatment_template: IdValidate.get().required()
}).validate(data); }).validate(data);
} }
else if (param === 'change') { else if (param === 'change') {
return Joi.object({ return Joi.object({
number: this.condition.number,
parameters: this.condition.parameters parameters: this.condition.parameters
}).validate(data); }).validate(data);
} }
@ -38,7 +36,7 @@ export default class ConditionValidate {
} }
} }
static output (data) { static output (data) { // validate output and strip unwanted properties, returns null if not valid
data = IdValidate.stringify(data); data = IdValidate.stringify(data);
const {value, error} = Joi.object({ const {value, error} = Joi.object({
_id: IdValidate.get(), _id: IdValidate.get(),

View File

@ -3,11 +3,11 @@ import Joi from '@hapi/joi';
export default class IdValidate { 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 () { static get () { // return joi validation
return this.id; return this.id;
} }
static valid (id) { static valid (id) { // validate id
return this.id.validate(id).error === undefined; return this.id.validate(id).error === undefined;
} }
@ -15,11 +15,14 @@ export default class IdValidate {
return ':id([0-9a-f]{24})'; return ':id([0-9a-f]{24})';
} }
static stringify (data) { static stringify (data) { // convert all ObjectID objects to plain strings
Object.keys(data).forEach(key => { Object.keys(data).forEach(key => {
if (data[key] !== null && data[key].hasOwnProperty('_bsontype') && data[key]._bsontype === 'ObjectID') { if (data[key] !== null && data[key].hasOwnProperty('_bsontype') && data[key]._bsontype === 'ObjectID') { // stringify id
data[key] = data[key].toString(); data[key] = data[key].toString();
} }
else if (typeof data[key] === 'object' && data[key] !== null) { // deeper into recursion
data[key] = this.stringify(data[key]);
}
}); });
return data; return data;
} }

View File

@ -33,13 +33,14 @@ export default class MaterialValidate { // validate input for material
color: joi.string() color: joi.string()
.max(128) .max(128)
.required(), .required(),
number: joi.number() number: joi.string()
.min(0) .max(128)
.allow('')
.required() .required()
})) }))
}; };
static input (data, param) { // validate data, param: new(everything required)/change(available attributes are validated) static input (data, param) { // validate input, set param to 'new' to make all attributes required
if (param === 'new') { if (param === 'new') {
return joi.object({ return joi.object({
name: this.material.name.required(), name: this.material.name.required(),
@ -67,7 +68,7 @@ export default class MaterialValidate { // validate input for material
} }
} }
static output (data) { // validate output from database for needed properties, strip everything else static output (data) { // validate output and strip unwanted properties, returns null if not valid
data = IdValidate.stringify(data); data = IdValidate.stringify(data);
const {value, error} = joi.object({ const {value, error} = joi.object({
_id: IdValidate.get(), _id: IdValidate.get(),

View File

@ -15,7 +15,7 @@ export default class MeasurementValidate {
) )
}; };
static input (data, param) { static input (data, param) { // validate input, set param to 'new' to make all attributes required
if (param === 'new') { if (param === 'new') {
return Joi.object({ return Joi.object({
condition_id: IdValidate.get().required(), condition_id: IdValidate.get().required(),
@ -33,7 +33,7 @@ export default class MeasurementValidate {
} }
} }
static output (data) { static output (data) { // validate output and strip unwanted properties, returns null if not valid
data = IdValidate.stringify(data); data = IdValidate.stringify(data);
const {value, error} = Joi.object({ const {value, error} = Joi.object({
_id: IdValidate.get(), _id: IdValidate.get(),

View File

@ -8,7 +8,7 @@ export default class NoteFieldValidate {
qty: Joi.number() qty: Joi.number()
}; };
static output (data) { static output (data) { // validate output and strip unwanted properties, returns null if not valid
const {value, error} = Joi.object({ const {value, error} = Joi.object({
name: this.note_field.name, name: this.note_field.name,
qty: this.note_field.qty qty: this.note_field.qty

View File

@ -4,7 +4,7 @@ export default class ParametersValidate {
static input (data, parameters, param) { // data to validate, parameters from template, param: 'new', 'change' static input (data, parameters, param) { // data to validate, parameters from template, param: 'new', 'change'
let joiObject = {}; let joiObject = {};
parameters.forEach(parameter => { parameters.forEach(parameter => {
if (parameter.range.hasOwnProperty('values')) { if (parameter.range.hasOwnProperty('values')) { // append right validation method according to parameter
joiObject[parameter.name] = Joi.alternatives() joiObject[parameter.name] = Joi.alternatives()
.try(Joi.string().max(128), Joi.number(), Joi.boolean()) .try(Joi.string().max(128), Joi.number(), Joi.boolean())
.valid(...parameter.range.values); .valid(...parameter.range.values);

View File

@ -1,3 +1,5 @@
// respond with 400 and include error details from the joi validation
export default function res400 (error, res) { export default function res400 (error, res) {
res.status(400).json({status: 'Invalid body format', details: error.details[0].message}); res.status(400).json({status: 'Invalid body format', details: error.details[0].message});
} }

View File

@ -41,10 +41,9 @@ export default class SampleValidate {
}) })
}; };
static input (data, param) { // validate data, param: new(everything required)/change(available attributes are validated) static input (data, param) { // validate input, set param to 'new' to make all attributes required
if (param === 'new') { if (param === 'new') {
return Joi.object({ return Joi.object({
number: this.sample.number.required(),
color: this.sample.color.required(), color: this.sample.color.required(),
type: this.sample.type.required(), type: this.sample.type.required(),
batch: this.sample.batch.required(), batch: this.sample.batch.required(),
@ -54,7 +53,6 @@ export default class SampleValidate {
} }
else if (param === 'change') { else if (param === 'change') {
return Joi.object({ return Joi.object({
number: this.sample.number,
color: this.sample.color, color: this.sample.color,
type: this.sample.type, type: this.sample.type,
batch: this.sample.batch, batch: this.sample.batch,
@ -67,7 +65,7 @@ export default class SampleValidate {
} }
} }
static output (data) { static output (data) { // validate output and strip unwanted properties, returns null if not valid
data = IdValidate.stringify(data); data = IdValidate.stringify(data);
const {value, error} = Joi.object({ const {value, error} = Joi.object({
_id: IdValidate.get(), _id: IdValidate.get(),

View File

@ -1,35 +1,36 @@
import joi from '@hapi/joi'; import Joi from '@hapi/joi';
import IdValidate from './id'; import IdValidate from './id';
export default class TemplateValidate { export default class TemplateValidate {
private static template = { private static template = {
name: joi.string() name: Joi.string()
.max(128), .max(128),
version: joi.number() version: Joi.number()
.min(1), .min(1),
number_prefix: joi.string() number_prefix: Joi.string()
.pattern(/^[a-zA-Z]+$/)
.min(1) .min(1)
.max(16), .max(16),
parameters: joi.array() parameters: Joi.array()
.min(1) .min(1)
.items( .items(
joi.object({ Joi.object({
name: joi.string() name: Joi.string()
.max(128) .max(128)
.required(), .required(),
range: joi.object({ range: Joi.object({
values: joi.array() values: Joi.array()
.min(1), .min(1),
min: joi.number(), min: Joi.number(),
max: joi.number(), max: Joi.number(),
type: joi.string() type: Joi.string()
.valid('array') .valid('array')
}) })
.oxor('values', 'min') .oxor('values', 'min')
@ -42,17 +43,17 @@ export default class TemplateValidate {
) )
}; };
static input (data, param, template) { // validate data, param: new(everything required)/change(available attributes are validated) static input (data, param, template) { // validate input, set param to 'new' to make all attributes required
if (param === 'new') { if (param === 'new') {
if (template === 'treatment') { if (template === 'treatment') {
return joi.object({ return Joi.object({
name: this.template.name.required(), name: this.template.name.required(),
number_prefix: this.template.number_prefix.required(), number_prefix: this.template.number_prefix.required(),
parameters: this.template.parameters.required() parameters: this.template.parameters.required()
}).validate(data); }).validate(data);
} }
else { else {
return joi.object({ return Joi.object({
name: this.template.name.required(), name: this.template.name.required(),
parameters: this.template.parameters.required() parameters: this.template.parameters.required()
}).validate(data); }).validate(data);
@ -60,14 +61,14 @@ export default class TemplateValidate {
} }
else if (param === 'change') { else if (param === 'change') {
if (template === 'treatment') { if (template === 'treatment') {
return joi.object({ return Joi.object({
name: this.template.name, name: this.template.name,
number_prefix: this.template.number_prefix, number_prefix: this.template.number_prefix,
parameters: this.template.parameters parameters: this.template.parameters
}).validate(data); }).validate(data);
} }
else { else {
return joi.object({ return Joi.object({
name: this.template.name, name: this.template.name,
parameters: this.template.parameters parameters: this.template.parameters
}).validate(data); }).validate(data);
@ -78,10 +79,10 @@ export default class TemplateValidate {
} }
} }
static output (data, template) { // validate output from database for needed properties, strip everything else static output (data, template) { // validate output and strip unwanted properties, returns null if not valid
data = IdValidate.stringify(data); data = IdValidate.stringify(data);
let joiObject; let joiObject;
if (template === 'treatment') { if (template === 'treatment') { // differentiate between measurement and treatment (has number_prefix) template
joiObject = { joiObject = {
_id: IdValidate.get(), _id: IdValidate.get(),
name: this.template.name, name: this.template.name,
@ -98,7 +99,7 @@ export default class TemplateValidate {
parameters: this.template.parameters parameters: this.template.parameters
}; };
} }
const {value, error} = joi.object(joiObject).validate(data, {stripUnknown: true}); const {value, error} = Joi.object(joiObject).validate(data, {stripUnknown: true});
return error !== undefined? null : value; return error !== undefined? null : value;
} }
} }

View File

@ -5,9 +5,9 @@ import IdValidate from './id';
export default class UserValidate { // validate input for user export default class UserValidate { // validate input for user
private static user = { private static user = {
name: Joi.string() // TODO: check allowed characters name: Joi.string()
.alphanum()
.lowercase() .lowercase()
.pattern(new RegExp('^[a-z0-9-_.]+$'))
.max(128), .max(128),
email: Joi.string() email: Joi.string()
@ -16,7 +16,7 @@ export default class UserValidate { // validate input for user
.max(128), .max(128),
pass: Joi.string() pass: Joi.string()
.pattern(new RegExp('^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$')) .pattern(/^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&'()*+,-.\/:;<=>?@[\]^_`{|}~])(?=\S+$)[a-zA-Z0-9!"#%&'()*+,\-.\/:;<=>?@[\]^_`{|}~]{8,}$/)
.max(128), .max(128),
level: Joi.string() level: Joi.string()
@ -33,7 +33,7 @@ export default class UserValidate { // validate input for user
private static specialUsernames = ['admin', 'user', 'key', 'new', 'passreset']; // names a user cannot take private static specialUsernames = ['admin', 'user', 'key', 'new', 'passreset']; // names a user cannot take
static input (data, param) { static input (data, param) { // validate input, set param to 'new' to make all attributes required
if (param === 'new') { if (param === 'new') {
return Joi.object({ return Joi.object({
name: this.user.name.required(), name: this.user.name.required(),
@ -68,7 +68,7 @@ export default class UserValidate { // validate input for user
} }
} }
static output (data) { // validate output from database for needed properties, strip everything else static output (data) { // validate output and strip unwanted properties, returns null if not valid
data = IdValidate.stringify(data); data = IdValidate.stringify(data);
const {value, error} = Joi.object({ const {value, error} = Joi.object({
_id: IdValidate.get(), _id: IdValidate.get(),

View File

@ -51,7 +51,7 @@
}, },
{ {
"_id": {"$oid":"400000000000000000000005"}, "_id": {"$oid":"400000000000000000000005"},
"number": "33", "number": "Rng33",
"type": "granulate", "type": "granulate",
"color": "black", "color": "black",
"batch": "1653000308", "batch": "1653000308",
@ -121,11 +121,11 @@
"numbers": [ "numbers": [
{ {
"color": "black", "color": "black",
"number": 5514263423 "number": "5514263423"
}, },
{ {
"color": "natural", "color": "natural",
"number": 5514263422 "number": "5514263422"
} }
], ],
"status": 10, "status": 10,
@ -142,11 +142,11 @@
"numbers": [ "numbers": [
{ {
"color": "black", "color": "black",
"number": 5514212901 "number": "5514212901"
}, },
{ {
"color": "signalviolet", "color": "signalviolet",
"number": 5514612901 "number": "5514612901"
} }
], ],
"status": 10, "status": 10,
@ -176,7 +176,7 @@
"numbers": [ "numbers": [
{ {
"color": "black", "color": "black",
"number": 5513933405 "number": "5513933405"
} }
], ],
"status": 10, "status": 10,
@ -193,7 +193,7 @@
"numbers": [ "numbers": [
{ {
"color": "black", "color": "black",
"number": 5514262406 "number": "5514262406"
} }
], ],
"status": 10, "status": 10,
@ -210,18 +210,35 @@
"numbers": [ "numbers": [
{ {
"color": "natural", "color": "natural",
"number": 10000000 "number": "10000000"
} }
], ],
"status": -1, "status": -1,
"__v": 0 "__v": 0
},
{
"_id": {"$oid":"100000000000000000000007"},
"name": "Ultramid A4H",
"supplier": "BASF",
"group": "PA66",
"mineral": 0,
"glass_fiber": 0,
"carbon_fiber": 0,
"numbers": [
{
"color": "black",
"number": ""
}
],
"status": 0,
"__v": 0
} }
], ],
"conditions": [ "conditions": [
{ {
"_id": {"$oid":"700000000000000000000001"}, "_id": {"$oid":"700000000000000000000001"},
"sample_id": {"$oid":"400000000000000000000001"}, "sample_id": {"$oid":"400000000000000000000001"},
"number": "B1", "number": "A1",
"parameters": { "parameters": {
"material": "copper", "material": "copper",
"weeks": 3 "weeks": 3
@ -233,7 +250,7 @@
{ {
"_id": {"$oid":"700000000000000000000002"}, "_id": {"$oid":"700000000000000000000002"},
"sample_id": {"$oid":"400000000000000000000002"}, "sample_id": {"$oid":"400000000000000000000002"},
"number": "B1", "number": "A1",
"parameters": { "parameters": {
"material": "copper", "material": "copper",
"weeks": 3 "weeks": 3
@ -245,7 +262,7 @@
{ {
"_id": {"$oid":"700000000000000000000003"}, "_id": {"$oid":"700000000000000000000003"},
"sample_id": {"$oid":"400000000000000000000004"}, "sample_id": {"$oid":"400000000000000000000004"},
"number": "B1", "number": "A1",
"parameters": { "parameters": {
"material": "copper", "material": "copper",
"weeks": 3 "weeks": 3
@ -257,7 +274,7 @@
{ {
"_id": {"$oid":"700000000000000000000004"}, "_id": {"$oid":"700000000000000000000004"},
"sample_id": {"$oid":"400000000000000000000001"}, "sample_id": {"$oid":"400000000000000000000001"},
"number": "B3", "number": "A2",
"parameters": { "parameters": {
"material": "hot air", "material": "hot air",
"weeks": 5 "weeks": 5
@ -429,6 +446,17 @@
"device_name": "", "device_name": "",
"key": "000000000000000000001003", "key": "000000000000000000001003",
"__v": "0" "__v": "0"
},
{
"_id": {"$oid":"000000000000000000000004"},
"email": "johnny.doe@bosch.com",
"name": "johnnydoe",
"pass": "$2a$10$di26XKF63OG0V00PL1kSK.ceCcTxDExBMOg.jkHiCnXcY7cN7DlPi",
"level": "write",
"location": "Fe",
"device_name": "Alpha I",
"key": "000000000000000000001004",
"__v": 0
} }
] ]
} }

View File

@ -4,12 +4,13 @@ import db from "../db";
export default class TestHelper { export default class TestHelper {
public static auth = { public static auth = { // test user credentials
admin: {pass: 'Abc123!#', key: '000000000000000000001003'}, admin: {pass: 'Abc123!#', key: '000000000000000000001003'},
janedoe: {pass: 'Xyz890*)', key: '000000000000000000001002'}, janedoe: {pass: 'Xyz890*)', key: '000000000000000000001002'},
user: {pass: 'Xyz890*)', key: '000000000000000000001001'} user: {pass: 'Xyz890*)', key: '000000000000000000001001'},
johnnydoe: {pass: 'Xyz890*)', key: '000000000000000000001004'}
} }
public static res = { public static res = { // default responses
400: {status: 'Bad request'}, 400: {status: 'Bad request'},
401: {status: 'Unauthorized'}, 401: {status: 'Unauthorized'},
403: {status: 'Forbidden'}, 403: {status: 'Forbidden'},
@ -39,10 +40,10 @@ export default class TestHelper {
static request (server, done, options) { // options in form: {method, url, auth: {key/basic: 'name' or 'key'/{name, pass}}, httpStatus, req, res} static request (server, done, options) { // options in form: {method, url, auth: {key/basic: 'name' or 'key'/{name, pass}}, httpStatus, req, res}
let st = supertest(server); let st = supertest(server);
if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('key')) { if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('key')) { // resolve API key
options.url += '?key=' + (this.auth.hasOwnProperty(options.auth.key)? this.auth[options.auth.key].key : options.auth.key); options.url += '?key=' + (this.auth.hasOwnProperty(options.auth.key)? this.auth[options.auth.key].key : options.auth.key);
} }
switch (options.method) { switch (options.method) { // http method
case 'get': case 'get':
st = st.get(options.url) st = st.get(options.url)
break; break;
@ -56,10 +57,10 @@ export default class TestHelper {
st = st.delete(options.url) st = st.delete(options.url)
break; break;
} }
if (options.hasOwnProperty('req')) { if (options.hasOwnProperty('req')) { // request body
st = st.send(options.req); st = st.send(options.req);
} }
if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('basic')) { if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('basic')) { // resolve basic auth
if (this.auth.hasOwnProperty(options.auth.basic)) { if (this.auth.hasOwnProperty(options.auth.basic)) {
st = st.auth(options.auth.basic, this.auth[options.auth.basic].pass) st = st.auth(options.auth.basic, this.auth[options.auth.basic].pass)
} }
@ -69,21 +70,21 @@ export default class TestHelper {
} }
st = st.expect('Content-type', /json/) st = st.expect('Content-type', /json/)
.expect(options.httpStatus); .expect(options.httpStatus);
if (options.hasOwnProperty('res')) { if (options.hasOwnProperty('res')) { // evaluate result
return st.end((err, res) => { return st.end((err, res) => {
if (err) return done (err); if (err) return done (err);
should(res.body).be.eql(options.res); should(res.body).be.eql(options.res);
done(); done();
}); });
} }
else if (this.res.hasOwnProperty(options.httpStatus) && options.default !== false) { else if (this.res.hasOwnProperty(options.httpStatus) && options.default !== false) { // evaluate default results
return st.end((err, res) => { return st.end((err, res) => {
if (err) return done (err); if (err) return done (err);
should(res.body).be.eql(this.res[options.httpStatus]); should(res.body).be.eql(this.res[options.httpStatus]);
done(); done();
}); });
} }
else { else { // return object to do .end() manually
return st; return st;
} }
} }

View File

@ -1,5 +1,7 @@
import db from '../db'; import db from '../db';
// script to load test db into dev db for a clean start
db.connect('dev', () => { db.connect('dev', () => {
console.info('dropping data...'); console.info('dropping data...');
db.drop(() => { // reset database db.drop(() => { // reset database