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:
summary: condition by id
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:
- /condition
responses:
@ -38,9 +38,6 @@
allOf:
- $ref: 'api.yaml#/components/schemas/_Id'
properties:
number:
type: string
example: B1
parameters:
type: object
responses:

View File

@ -2,7 +2,30 @@
get:
summary: lists all materials
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:
- /material
responses:
@ -25,7 +48,7 @@
get:
summary: get material details
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:
- /material
responses:

View File

@ -4,7 +4,7 @@
get:
summary: measurement values by id
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:
- /measurement
responses:

View File

@ -5,10 +5,20 @@ Id:
schema:
type: string
example: 5ea0450ed851c30a90e70894
Name:
name: name
description: has to be URL encoded
in: path
required: true
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:
summary: all samples in overview
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:
- /sample
responses:
@ -18,13 +18,37 @@
$ref: 'api.yaml#/components/responses/401'
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}:
parameters:
- $ref: 'api.yaml#/components/parameters/Id'
get:
summary: TODO sample details
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:
- /sample
responses:
@ -130,7 +154,7 @@
get:
summary: list all existing field names for custom notes fields
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:
- /sample
responses:

View File

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

View File

@ -4,7 +4,7 @@ import oasParser from '@apidevtools/swagger-parser';
// 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
// further route documentation can be included in the x-doc property
@ -20,7 +20,7 @@ export default class api {
apiDoc = doc;
apiDoc.paths = apiDoc.paths.allOf.reduce((s, e) => Object.assign(s, e)); // bundle routes
apiDoc = this.resolveXDoc(apiDoc);
oasParser.validate(apiDoc, (err, api) => {
oasParser.validate(apiDoc, (err, api) => { // validate oas schema
if (err) {
console.error(err);
}
@ -35,8 +35,8 @@ export default class api {
private static resolveXDoc (doc) { // resolve x-doc properties recursively
Object.keys(doc).forEach(key => {
if (doc[key] !== null && doc[key].hasOwnProperty('x-doc')) {
doc[key].description += this.addHtml(doc[key]['x-doc']);
if (doc[key] !== null && doc[key].hasOwnProperty('x-doc')) { // add x-doc to description, is styled via css
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
doc[key] = this.resolveXDoc(doc[key]);
@ -44,8 +44,4 @@ export default class api {
});
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,
};
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
// 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
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();
} // no db connection or nothing to load
}
let loadCounter = 0; // count number of loaded collections to know when to return done()
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
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);
}
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]);
}
});

View File

@ -9,7 +9,7 @@ import UserModel from '../models/user';
module.exports = async (req, res, next) => {
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
const userBasic = await basic(req, next);
@ -46,7 +46,8 @@ module.exports = async (req, res, next) => {
method: givenMethod,
username: user.name,
level: user.level,
id: user.id
id: user.id,
location: user.location
};
next();
@ -62,8 +63,8 @@ function basic (req, next): any { // checks basic auth and returns changed user
if (data.length === 1) { // one user found
bcrypt.compare(auth.pass, data[0].pass, (err, res) => { // check password
if (err) return next(err);
if (res === true) {
resolve({level: data[0].level, name: data[0].name, id: data[0]._id.toString()});
if (res === true) { // password correct
resolve({level: data[0].level, name: data[0].name, id: data[0]._id.toString(), location: data[0].location});
}
else {
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
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
if (err) return next(err);
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 {
resolve(null);

View File

@ -1,6 +1,6 @@
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
if (process.env.NODE_ENV === 'production') {

View File

@ -5,6 +5,12 @@ import mongoSanitize from 'mongo-sanitize';
import api from './api';
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
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
// require routes
app.use('/', require('./routes/root'));
app.use('/', require('./routes/sample'));
app.use('/', require('./routes/material'));
app.use('/', require('./routes/template'));
app.use('/', require('./routes/user'));
app.use('/', require('./routes/condition'));
app.use('/', require('./routes/measurement'));
app.use('/api', require('./routes/root'));
app.use('/api', require('./routes/sample'));
app.use('/api', require('./routes/material'));
app.use('/api', require('./routes/template'));
app.use('/api', require('./routes/user'));
app.use('/api', require('./routes/condition'));
app.use('/api', require('./routes/measurement'));
// static files
app.use('/static', express.static('static'));
// Swagger UI
app.use('/api', api.serve(), api.setup());
app.use('/api-doc', api.serve(), api.setup());
app.use((req, res) => { // 404 error handling
res.status(404).json({status: 'Not found'});

View File

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

View File

@ -2,6 +2,10 @@ import should from 'should/as-function';
import ConditionModel from '../models/condition';
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', () => {
let server;
@ -16,7 +20,7 @@ describe('/condition', () => {
url: '/condition/700000000000000000000001',
auth: {basic: 'janedoe'},
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 => {
@ -25,7 +29,7 @@ describe('/condition', () => {
url: '/condition/700000000000000000000001',
auth: {key: 'janedoe'},
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 => {
@ -61,7 +65,7 @@ describe('/condition', () => {
auth: {basic: 'janedoe'},
httpStatus: 200,
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 => {
@ -73,7 +77,7 @@ describe('/condition', () => {
req: {parameters: {material: 'copper', weeks: 3}}
}).end((err, res) => {
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) => {
if (err) return done(err);
should(data).have.property('status', 10);
@ -90,7 +94,7 @@ describe('/condition', () => {
req: {parameters: {material: 'copper'}}
}).end((err, res) => {
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) => {
if (err) return done(err);
should(data).have.property('status', 10);
@ -107,12 +111,12 @@ describe('/condition', () => {
req: {parameters: {material: 'hot air', weeks: 10}}
}).end((err, res) => {
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) => {
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('400000000000000000000001');
should(data).have.property('number', 'B1');
should(data).have.property('number', 'A1');
should(data.treatment_template.toString()).be.eql('200000000000000000000001');
should(data).have.property('status', 0);
should(data).have.property('parameters');
@ -129,7 +133,17 @@ describe('/condition', () => {
auth: {basic: 'janedoe'},
httpStatus: 200,
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 => {
@ -198,7 +212,7 @@ describe('/condition', () => {
auth: {basic: 'admin'},
httpStatus: 200,
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 => {
@ -227,34 +241,41 @@ describe('/condition', () => {
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}', () => {
it('sets the status to deleted', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/condition/700000000000000000000002',
url: '/condition/700000000000000000000004',
auth: {basic: 'janedoe'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
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);
should(data).have.only.keys('_id', 'sample_id', 'number', 'parameters', 'treatment_template', 'status', '__v');
should(data.sample_id.toString()).be.eql('400000000000000000000002');
should(data).have.property('number', 'B1');
should(data.sample_id.toString()).be.eql('400000000000000000000001');
should(data).have.property('number', 'A2');
should(data.treatment_template.toString()).be.eql('200000000000000000000001');
should(data).have.property('status', -1);
should(data).have.property('parameters');
should(data.parameters).have.property('material', 'copper');
should(data.parameters).have.property('weeks', 3);
should(data.parameters).have.property('material', 'hot air');
should(data.parameters).have.property('weeks', 5);
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 => {
TestHelper.request(server, done, {
method: 'delete',
@ -266,7 +287,7 @@ describe('/condition', () => {
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/condition/700000000000000000000002',
url: '/condition/700000000000000000000004',
auth: {key: 'janedoe'},
httpStatus: 401
});
@ -274,7 +295,7 @@ describe('/condition', () => {
it('rejects requests from a read user', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/condition/700000000000000000000002',
url: '/condition/700000000000000000000004',
auth: {basic: 'user'},
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 => {
TestHelper.request(server, done, {
method: 'delete',
url: '/condition/700000000000000000000002',
url: '/condition/700000000000000000000004',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
@ -302,7 +323,7 @@ describe('/condition', () => {
it('returns 404 for an unknown id', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/condition/00000000000w000000000002',
url: '/condition/000000000000000000000002',
auth: {basic: 'janedoe'},
httpStatus: 404
});
@ -310,26 +331,26 @@ describe('/condition', () => {
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/condition/700000000000000000000002',
url: '/condition/700000000000000000000004',
httpStatus: 401
});
});
});
describe('POST /condition/new', () => { // TODO: sample number generation
describe('POST /condition/new', () => {
it('returns the right condition', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/condition/new',
auth: {basic: 'janedoe'},
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) => {
if (err) return done(err);
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('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('parameters');
should(res.body.parameters).have.property('material', 'hot air');
@ -343,14 +364,37 @@ describe('/condition', () => {
url: '/condition/new',
auth: {basic: 'janedoe'},
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) => {
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('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).have.property('status', 0);
should(data).have.property('parameters');
@ -366,7 +410,7 @@ describe('/condition', () => {
url: '/condition/new',
auth: {basic: 'janedoe'},
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}/'}
});
});
@ -376,7 +420,7 @@ describe('/condition', () => {
url: '/condition/new',
auth: {basic: 'janedoe'},
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'}
});
});
@ -386,7 +430,7 @@ describe('/condition', () => {
url: '/condition/new',
auth: {basic: 'janedoe'},
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}/'}
});
});
@ -396,18 +440,18 @@ describe('/condition', () => {
url: '/condition/new',
auth: {basic: 'janedoe'},
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'}
});
});
it('rejects a condition number already in use for this sample', done => {
it('rejects setting a condition number', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/condition/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {sample_id: '400000000000000000000001', number: 'B1', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'},
res: {status: 'Condition number already taken'}
req: {sample_id: '400000000000000000000001', number: 'A7', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'},
res: {status: 'Invalid body format', details: '"number" is not allowed'}
});
});
it('rejects not specified parameters', done => {
@ -416,7 +460,7 @@ describe('/condition', () => {
url: '/condition/new',
auth: {basic: 'janedoe'},
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'}
});
});
@ -426,7 +470,7 @@ describe('/condition', () => {
url: '/condition/new',
auth: {basic: 'janedoe'},
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'}
});
});
@ -436,7 +480,7 @@ describe('/condition', () => {
url: '/condition/new',
auth: {basic: 'janedoe'},
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]'}
});
});
@ -446,7 +490,7 @@ describe('/condition', () => {
url: '/condition/new',
auth: {basic: 'janedoe'},
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'}
});
});
@ -456,7 +500,7 @@ describe('/condition', () => {
url: '/condition/new',
auth: {basic: 'janedoe'},
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'}
});
});
@ -466,7 +510,7 @@ describe('/condition', () => {
url: '/condition/new',
auth: {basic: 'janedoe'},
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'}
});
});
@ -476,27 +520,17 @@ describe('/condition', () => {
url: '/condition/new',
auth: {basic: 'janedoe'},
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'}
});
});
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 => {
TestHelper.request(server, done, {
method: 'post',
url: '/condition/new',
auth: {basic: 'janedoe'},
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 => {
@ -505,13 +539,13 @@ describe('/condition', () => {
url: '/condition/new',
auth: {basic: 'admin'},
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) => {
if (err) return done(err);
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('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('parameters');
should(res.body.parameters).have.property('material', 'hot air');
@ -525,7 +559,7 @@ describe('/condition', () => {
url: '/condition/new',
auth: {key: 'janedoe'},
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 => {
@ -534,7 +568,7 @@ describe('/condition', () => {
url: '/condition/new',
auth: {basic: 'user'},
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 => {
@ -542,7 +576,7 @@ describe('/condition', () => {
method: 'post',
url: '/condition/new',
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 mongoose from 'mongoose';
import _ from 'lodash';
import ConditionValidate from './validate/condition';
@ -38,13 +37,14 @@ router.put('/condition/' + IdValidate.parameter(), async (req, res, next) => {
if (!data) {
res.status(404).json({status: 'Not found'});
}
// add properties needed for sampleIdCheck
condition.treatment_template = data.treatment_template;
condition.sample_id = data.sample_id;
if (!await sampleIdCheck(condition, req, res, next)) return;
if (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;
}
}
@ -79,10 +79,12 @@ router.post('/condition/new', async (req, res, next) => {
if (error) return res400(error, res);
if (!await sampleIdCheck(condition, req, res, next)) return;
if (!await numberCheck(condition, res, next)) return;
if (!await treatmentCheck(condition, 'new', res, next)) return;
const treatmentData = await treatmentCheck(condition, 'new', res, next)
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) => {
if (err) return next(err);
res.json(ConditionValidate.output(data.toObject()));
@ -104,24 +106,28 @@ async function sampleIdCheck (condition, req, res, next) { // validate sample_i
return true;
}
async function numberCheck (condition, res, next) { // validate number, returns false if invalid
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;
if (data.length) {
res.status(400).json({status: 'Condition number already taken'});
return false;
}
return true;
async function numberGenerate (condition, treatmentData, next) { // generate number, returns false on error
const conditionData = await ConditionModel // find condition with highest number belonging to the same sample
.find({sample_id: condition.sample_id, number: new RegExp('^' + treatmentData.number_prefix + '[0-9]+$', 'm')})
.sort({number: -1})
.limit(1)
.lean()
.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) {
const treatmentData = await TreatmentTemplateModel.findById(condition.treatment_template).lean().exec().catch(err => {next(err); return false;}) as any;
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)) as any;
if (treatmentData instanceof Error) return false;
if (!treatmentData) { // template not found
res.status(400).json({status: 'Treatment template not available'});
return false
return false;
}
// validate parameters
const {error, value: ignore} = ParametersValidate.input(condition.parameters, treatmentData.parameters, param);
if (error) {res400(error, res); return false;}
return true;
return treatmentData;
}

View File

@ -1,9 +1,10 @@
import should from 'should/as-function';
import _ from 'lodash';
import MaterialModel from '../models/material';
import TestHelper from "../test/helper";
// TODO: numbers with color only (no number)
// TODO: deal with numbers with leading zeros
// TODO: color name must be unique to get color number
// TODO: separate supplier/ material name into own collections
describe('/material', () => {
let server;
@ -21,7 +22,6 @@ describe('/material', () => {
}).end((err, res) => {
if (err) return done(err);
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).matchEach(material => {
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(number).have.only.keys('color', 'number');
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();
@ -63,7 +63,7 @@ describe('/material', () => {
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('number');
should(number).have.property('number').be.type('string');
});
});
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}', () => {
it('returns the right material', done => {
TestHelper.request(server, done, {
@ -85,7 +180,7 @@ describe('/material', () => {
url: '/material/100000000000000000000001',
auth: {basic: 'janedoe'},
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 => {
@ -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: []}
});
});
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 => {
TestHelper.request(server, done, {
method: 'get',
@ -130,7 +234,7 @@ describe('/material', () => {
auth: {basic: 'janedoe'},
httpStatus: 200,
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 => {
@ -139,10 +243,10 @@ describe('/material', () => {
url: '/material/100000000000000000000001',
auth: {basic: 'janedoe'},
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) => {
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) => {
if (err) return done(err);
should(data).have.property('status', 10);
@ -159,7 +263,7 @@ describe('/material', () => {
req: {name: 'Stanyl TW 200 F8'}
}).end((err, res) => {
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) => {
if (err) return done(err);
should(data).have.property('status', 10);
@ -173,20 +277,30 @@ describe('/material', () => {
url: '/material/100000000000000000000001',
auth: {basic: 'janedoe'},
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) => {
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) => {
if (err) return done(err);
data._id = data._id.toString();
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();
});
});
});
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 => {
TestHelper.request(server, done, {
method: 'put',
@ -233,20 +347,10 @@ describe('/material', () => {
url: '/material/100000000000000000000001',
auth: {basic: 'janedoe'},
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'}
});
});
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 => {
TestHelper.request(server, done, {
method: 'put',
@ -307,7 +411,7 @@ describe('/material', () => {
if (err) return done(err);
data._id = data._id.toString();
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();
});
@ -370,7 +474,7 @@ describe('/material', () => {
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: 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) => {
if (err) return done (err);
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(number).have.only.keys('color', 'number');
should(number).have.property('color', 'black');
should(number).have.property('number', 5515798402);
should(number).have.property('number', '05515798402');
});
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 => {
TestHelper.request(server, done, {
method: 'post',
url: '/material/new',
auth: {basic: 'janedoe'},
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'}
});
});
@ -431,7 +574,7 @@ describe('/material', () => {
url: '/material/new',
auth: {basic: 'janedoe'},
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'}
});
});
@ -441,7 +584,7 @@ describe('/material', () => {
url: '/material/new',
auth: {basic: 'janedoe'},
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'}
});
});
@ -451,7 +594,7 @@ describe('/material', () => {
url: '/material/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]},
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'}
});
});
@ -461,7 +604,7 @@ describe('/material', () => {
url: '/material/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]},
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'}
});
});
@ -471,7 +614,7 @@ describe('/material', () => {
url: '/material/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]},
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'}
});
});
@ -481,7 +624,7 @@ describe('/material', () => {
url: '/material/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, numbers: [{color: 'black', number: 5515798402}]},
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'}
});
});
@ -501,7 +644,7 @@ describe('/material', () => {
url: '/material/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{number: 5515798402}]},
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'}
});
});

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) => {
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
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) => {
@ -85,13 +101,12 @@ router.delete('/material/' + IdValidate.parameter(), (req, res, next) => {
router.post('/material/new', async (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
// validate input
const {error, value: material} = MaterialValidate.input(req.body, 'new');
if (error) return res400(error, res);
if (!await nameCheck(material, res, next)) return;
material.status = 0;
material.status = 0; // set status to new
await new MaterialModel(material).save((err, data) => {
if (err) return next(err);
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
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) { // could not find material_id
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 TestHelper from "../test/helper";
// TODO: allow empty values
describe('/measurement', () => {
let server;
before(done => TestHelper.before(done));

View File

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

View File

@ -3,8 +3,10 @@ import SampleModel from '../models/sample';
import NoteModel from '../models/note';
import NoteFieldModel from '../models/note_field';
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', () => {
let server;
@ -23,16 +25,16 @@ describe('/sample', () => {
if (err) return done(err);
const json = require('../test/db.json');
should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status === 10).length);
should(res.body).matchEach(material => {
should(material).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id');
should(material).have.property('_id').be.type('string');
should(material).have.property('number').be.type('string');
should(material).have.property('type').be.type('string');
should(material).have.property('color').be.type('string');
should(material).have.property('batch').be.type('string');
should(material).have.property('material_id').be.type('string');
should(material).have.property('note_id');
should(material).have.property('user_id').be.type('string');
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');
});
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}', () => {
it('returns the right sample', done => {
TestHelper.request(server, done, {
@ -87,7 +177,7 @@ describe('/sample', () => {
url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'},
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) => {
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'});
@ -156,14 +246,14 @@ describe('/sample', () => {
url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}
req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}
}).end(err => {
if (err) return done (err);
SampleModel.findById('400000000000000000000001').lean().exec((err, data: any) => {
if (err) return done (err);
should(data).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'material_id', 'note_id', 'user_id', 'status', '__v');
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('type', 'part');
should(data).have.property('batch', '114531');
@ -194,12 +284,10 @@ describe('/sample', () => {
}).end(err => {
if (err) return done(err);
NoteFieldModel.findOne({name: 'not allowed for new applications'}).lean().exec((err, data) => {
console.log(data);
if (err) return done(err);
should(data).have.property('qty', 1);
NoteFieldModel.findOne({name: 'field1'}).lean().exec((err, data) => {
if (err) return done(err);
console.log(data);
should(data).have.property('qty', 1);
done();
});
@ -228,12 +316,11 @@ describe('/sample', () => {
url: '/sample/400000000000000000000002',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {number: '111'}
req: {type: 'part'}
}).end((err, res) => {
if (err) return done (err);
NoteModel.findById(res.body.note_id).lean().exec((err, data) => {
if (err) return done (err);
console.log(data);
should(data).not.be.null();
should(data).have.property('comment', 'Stoff gesperrt');
should(data).have.property('sample_references').have.lengthOf(0);
@ -263,7 +350,7 @@ describe('/sample', () => {
url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
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'}
});
});
@ -273,18 +360,18 @@ describe('/sample', () => {
url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '000000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
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'}
});
});
it('rejects a sample number in use', done => {
it('rejects a sample number', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: '21', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Sample number already taken'}
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: 'Invalid body format', details: '"number" is not allowed'}
});
});
it('rejects an invalid sample reference', done => {
@ -293,7 +380,7 @@ describe('/sample', () => {
url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '000000000000000000000003', relation: 'part to this sample'}]}},
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'}
});
});
@ -303,7 +390,7 @@ describe('/sample', () => {
url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
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}/'}
});
});
@ -313,7 +400,7 @@ describe('/sample', () => {
url: '/sample/10000000000h000000000001',
auth: {basic: 'janedoe'},
httpStatus: 404,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
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 => {
@ -322,7 +409,7 @@ describe('/sample', () => {
url: '/sample/400000000000000000000001',
auth: {key: 'janedoe'},
httpStatus: 401,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
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 => {
@ -350,7 +437,7 @@ describe('/sample', () => {
url: '/sample/400000000000000000000001',
auth: {basic: 'user'},
httpStatus: 403,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
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 => {
@ -359,7 +446,7 @@ describe('/sample', () => {
url: '/sample/000000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 404,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}
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 => {
@ -367,7 +454,7 @@ describe('/sample', () => {
method: 'put',
url: '/sample/400000000000000000000001',
httpStatus: 401,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
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
NoteModel.findById('500000000000000000000003').lean().exec((err, data: any) => {
if (err) return done(err);
console.log(data);
should(data).have.property('sample_references').with.lengthOf(1);
should(data.sample_references[0].id.toString()).be.eql('400000000000000000000003');
should(data.sample_references[0]).have.property('relation', 'part to sample');
@ -490,6 +576,7 @@ describe('/sample', () => {
httpStatus: 404
});
});
it('rejects deleting a sample referenced by conditions'); // TODO after decision
it('rejects requests from a read user', done => {
TestHelper.request(server, done, {
method: 'delete',
@ -530,12 +617,12 @@ describe('/sample', () => {
url: '/sample/new',
auth: {basic: 'janedoe'},
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) => {
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', 'Rng172');
should(res.body).have.property('number', 'Rng34');
should(res.body).have.property('color', 'black');
should(res.body).have.property('type', 'granulate');
should(res.body).have.property('batch', '1560237365');
@ -551,15 +638,15 @@ describe('/sample', () => {
url: '/sample/new',
auth: {basic: 'janedoe'},
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 => {
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);
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.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('type', 'granulate');
should(data[0]).have.property('batch', '1560237365');
@ -586,7 +673,7 @@ describe('/sample', () => {
url: '/sample/new',
auth: {basic: 'janedoe'},
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) => {
if (err) return done (err);
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 => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'janedoe'},
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'}
});
});
@ -633,18 +741,18 @@ describe('/sample', () => {
url: '/sample/new',
auth: {basic: 'janedoe'},
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'}
});
});
it('rejects a sample number in use', done => {
it('rejects a sample number', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'janedoe'},
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'}]}},
res: {status: 'Sample number already taken'}
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: 'Invalid body format', details: '"number" is not allowed'}
});
});
it('rejects an invalid sample reference', done => {
@ -653,7 +761,7 @@ describe('/sample', () => {
url: '/sample/new',
auth: {basic: 'janedoe'},
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'}
});
});
@ -663,27 +771,17 @@ describe('/sample', () => {
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'Rng172', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
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'}
});
});
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 => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'Rng172', color: 'black', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
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'}
});
});
@ -693,7 +791,7 @@ describe('/sample', () => {
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'Rng172', color: 'black', type: 'granulate', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
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'}
});
});
@ -703,7 +801,7 @@ describe('/sample', () => {
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
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'}
});
});
@ -713,7 +811,7 @@ describe('/sample', () => {
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
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}/'}
});
});
@ -723,7 +821,7 @@ describe('/sample', () => {
url: '/sample/new',
auth: {key: 'janedoe'},
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 => {
@ -732,7 +830,7 @@ describe('/sample', () => {
url: '/sample/new',
auth: {basic: 'user'},
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 => {
@ -740,7 +838,7 @@ describe('/sample', () => {
method: 'post',
url: '/sample/new',
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) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
@ -33,12 +49,10 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
if (!sampleData) {
return res.status(404).json({status: 'Not found'});
}
// only maintain and admin are allowed to edit other user's data
if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return;
if (sample.hasOwnProperty('number') && sample.number !== sampleData.number) {
if (!await numberCheck(sample, res, next)) return;
}
if (sample.hasOwnProperty('material_id')) {
if (!await materialCheck(sample, res, next)) return;
}
@ -51,12 +65,12 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
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;
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 (data.hasOwnProperty('custom_fields')) { // update note_fields
customFieldsChange(Object.keys(data.custom_fields), -1);
}
NoteModel.findByIdAndDelete(sampleData.note_id).lean().exec(err => { // delete old notes
await NoteModel.findByIdAndDelete(sampleData.note_id).lean().exec(err => { // delete old notes
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']))) {
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);
res.json(SampleValidate.output(data));
});
@ -93,12 +108,13 @@ router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => {
if (!sampleData) {
return res.status(404).json({status: 'Not found'});
}
// only maintain and admin are allowed to edit other user's data
if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return;
SampleModel.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 (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
if (err) return next(err);
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');
if (error) return res400(error, res);
if (!await numberCheck(sample, res, next)) return;
if (!await materialCheck(sample, res, next)) return;
if (!await sampleRefCheck(sample, res, next)) return;
@ -128,13 +143,15 @@ router.post('/sample/new', async (req, res, next) => {
customFieldsChange(Object.keys(sample.notes.custom_fields), 1);
}
sample.status = 0;
new NoteModel(sample.notes).save((err, data) => {
sample.status = 0; // set status to new
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);
delete sample.notes;
sample.note_id = data._id;
sample.user_id = req.authDetails.id;
console.log(sample);
new SampleModel(sample).save((err, data) => {
if (err) return next(err);
res.json(SampleValidate.output(data.toObject()));
@ -155,17 +172,18 @@ router.get('/sample/notes/fields', (req, res, next) => {
module.exports = router;
async function numberCheck (sample, res, next) { // validate number, returns false if invalid
const sampleData = await SampleModel.findOne({number: sample.number}).lean().exec().catch(err => {next(err); return false;});
if (sampleData) { // found entry with sample number
res.status(400).json({status: 'Sample number already taken'});
return false
}
return true;
async function numberGenerate (sample, req, res, next) { // generate number, returns false on error
const sampleData = await SampleModel
.find({number: new RegExp('^' + req.authDetails.location + '[0-9]+$', 'm')})
.lean()
.exec()
.catch(err => next(err));
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
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) { // could not find material_id
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
return new Promise(resolve => {
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 => {
SampleModel.findById(reference.id).lean().exec((err, data) => {
if (err) {next(err); resolve(false)}
@ -190,7 +209,7 @@ function sampleRefCheck (sample, res, next) { // validate sample_references, re
return resolve(false);
}
referencesCount --;
if (referencesCount <= 0) {
if (referencesCount <= 0) { // all async requests done
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 => {
NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: amount}}, {new: true}).lean().exec((err, data: any) => { // check if field exists
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 TestHelper from "../test/helper";
// TODO: do not allow usage of old templates for new samples
describe('/template', () => {
let server;
@ -201,7 +201,6 @@ describe('/template', () => {
httpStatus: 200,
req: {parameters: [{name: 'time', range: {type: 'array'}}]}
}).end((err, res) => {
console.log(res.body);
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'}}]});
done();
@ -371,14 +370,14 @@ describe('/template', () => {
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, {
method: 'post',
url: '/template/treatment/new',
auth: {basic: 'admin'},
httpStatus: 400,
req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]},
res: {status: 'Invalid body format', details: '"number_prefix" is required'}
req: {name: 'heat aging', number_prefix: 'AB5', parameters: [{name: 'time', range: {min: 1}}]},
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 => {

View File

@ -7,14 +7,14 @@ import TemplateMeasurementModel from '../models/measurement_template';
import res400 from './validate/res400';
import IdValidate from './validate/id';
// TODO: remove f() for await
const router = express.Router();
router.get('/template/:collection(measurements|treatments)', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return;
req.params.collection = 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) => {
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
@ -52,8 +52,8 @@ router.put('/template/:collection(measurement|treatment)/' + IdValidate.paramete
}
if (!_.isEqual(_.pick(templateData, _.keys(template)), template)) { // data was changed
template.version = templateData.version + 1;
await new (model(req))(_.assign({}, _.omit(templateData, ['_id', '__v']), template)).save((err, data) => {
template.version = templateData.version + 1; // increase version
await new (model(req))(_.assign({}, _.omit(templateData, ['_id', '__v']), template)).save((err, data) => { // save new template, fill with old properties
if (err) next (err);
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;
}
template.version = 1;
template.version = 1; // set template version
await new (model(req))(template).save((err, data) => {
if (err) next (err);
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;
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;
if (data) {
res.status(400).json({status: 'Number prefix already taken'});
@ -93,6 +93,6 @@ async function numberPrefixCheck (template, req, res, next) {
return true;
}
function model (req) {
function model (req) { // return right template model
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 TestHelper from "../test/helper";
// TODO: reject usernames containing admin, etc.
describe('/user', () => {
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
req.params.username = req.params[0];
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) => {
if (err) return next(err);
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
req.params.username = req.params[0];
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
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;
console.log(username);
const {error, value: user} = UserValidate.input(req.body, 'change' + (req.authDetails.level === 'admin'? 'admin' : ''));
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
if (user.hasOwnProperty('name') && user.name !== username) {
UserModel.find({name: user.name}).lean().exec( (err, data:any) => {
if (err) return next(err);
if (data.length > 0 || UserValidate.isSpecialName(user.name)) {
res.status(400).json({status: 'Username already taken'});
return;
}
if (!await usernameCheck(user.name, res, next)) return;
}
UserModel.findOneAndUpdate({name: username}, user, {new: true}).lean().exec( (err, data:any) => {
if (err) return next(err);
if (data) {
res.json(UserValidate.output(data));
}
else {
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'});
}
});
}
await UserModel.findOneAndUpdate({name: username}, user, {new: true}).lean().exec( (err, data:any) => {
if (err) return next(err);
if (data) {
res.json(UserValidate.output(data));
}
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
req.params.username = req.params[0];
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.findOneAndDelete({name: username}).lean().exec( (err, data:any) => {
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;
// validate input
@ -124,20 +99,14 @@ router.post('/user/new', (req, res, next) => {
if (error) return res400(error, res);
// check that user does not already exist
UserModel.find({name: user.name}).lean().exec( (err, data:any) => {
if (err) return next(err);
if (data.length > 0 || UserValidate.isSpecialName(user.name)) {
res.status(400).json({status: 'Username already taken'});
return;
}
if (!await usernameCheck(user.name, res, next)) return;
user.key = mongoose.Types.ObjectId(); // use object id as unique API key
bcrypt.hash(user.pass, 10, (err, hash) => { // password hashing
user.pass = hash;
new UserModel(user).save((err, data) => { // store user
if (err) return next(err);
res.json(UserValidate.output(data.toObject()));
});
user.key = mongoose.Types.ObjectId(); // use object id as unique API key
bcrypt.hash(user.pass, 10, (err, hash) => { // password hashing
user.pass = hash;
new UserModel(user).save((err, data) => { // store user
if (err) return next(err);
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) => {
if (err) return next(err);
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
if (err) return next(err);
UserModel.findByIdAndUpdate(data[0]._id, {pass: hash}, err => { // write new password
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 => {
if (err) return next(err);
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') {
return Joi.object({
sample_id: IdValidate.get().required(),
number: this.condition.number.required(),
parameters: this.condition.parameters.required(),
treatment_template: IdValidate.get().required()
}).validate(data);
}
else if (param === 'change') {
return Joi.object({
number: this.condition.number,
parameters: this.condition.parameters
}).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);
const {value, error} = Joi.object({
_id: IdValidate.get(),

View File

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

View File

@ -33,13 +33,14 @@ export default class MaterialValidate { // validate input for material
color: joi.string()
.max(128)
.required(),
number: joi.number()
.min(0)
number: joi.string()
.max(128)
.allow('')
.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') {
return joi.object({
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);
const {value, error} = joi.object({
_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') {
return Joi.object({
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);
const {value, error} = Joi.object({
_id: IdValidate.get(),

View File

@ -8,7 +8,7 @@ export default class NoteFieldValidate {
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({
name: this.note_field.name,
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'
let joiObject = {};
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()
.try(Joi.string().max(128), Joi.number(), Joi.boolean())
.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) {
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') {
return Joi.object({
number: this.sample.number.required(),
color: this.sample.color.required(),
type: this.sample.type.required(),
batch: this.sample.batch.required(),
@ -54,7 +53,6 @@ export default class SampleValidate {
}
else if (param === 'change') {
return Joi.object({
number: this.sample.number,
color: this.sample.color,
type: this.sample.type,
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);
const {value, error} = Joi.object({
_id: IdValidate.get(),

View File

@ -1,35 +1,36 @@
import joi from '@hapi/joi';
import Joi from '@hapi/joi';
import IdValidate from './id';
export default class TemplateValidate {
private static template = {
name: joi.string()
name: Joi.string()
.max(128),
version: joi.number()
version: Joi.number()
.min(1),
number_prefix: joi.string()
number_prefix: Joi.string()
.pattern(/^[a-zA-Z]+$/)
.min(1)
.max(16),
parameters: joi.array()
parameters: Joi.array()
.min(1)
.items(
joi.object({
name: joi.string()
Joi.object({
name: Joi.string()
.max(128)
.required(),
range: joi.object({
values: joi.array()
range: Joi.object({
values: Joi.array()
.min(1),
min: joi.number(),
min: Joi.number(),
max: joi.number(),
max: Joi.number(),
type: joi.string()
type: Joi.string()
.valid('array')
})
.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 (template === 'treatment') {
return joi.object({
return Joi.object({
name: this.template.name.required(),
number_prefix: this.template.number_prefix.required(),
parameters: this.template.parameters.required()
}).validate(data);
}
else {
return joi.object({
return Joi.object({
name: this.template.name.required(),
parameters: this.template.parameters.required()
}).validate(data);
@ -60,14 +61,14 @@ export default class TemplateValidate {
}
else if (param === 'change') {
if (template === 'treatment') {
return joi.object({
return Joi.object({
name: this.template.name,
number_prefix: this.template.number_prefix,
parameters: this.template.parameters
}).validate(data);
}
else {
return joi.object({
return Joi.object({
name: this.template.name,
parameters: this.template.parameters
}).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);
let joiObject;
if (template === 'treatment') {
if (template === 'treatment') { // differentiate between measurement and treatment (has number_prefix) template
joiObject = {
_id: IdValidate.get(),
name: this.template.name,
@ -98,7 +99,7 @@ export default class TemplateValidate {
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;
}
}

View File

@ -5,9 +5,9 @@ import IdValidate from './id';
export default class UserValidate { // validate input for user
private static user = {
name: Joi.string() // TODO: check allowed characters
.alphanum()
name: Joi.string()
.lowercase()
.pattern(new RegExp('^[a-z0-9-_.]+$'))
.max(128),
email: Joi.string()
@ -16,7 +16,7 @@ export default class UserValidate { // validate input for user
.max(128),
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),
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
static input (data, param) {
static input (data, param) { // validate input, set param to 'new' to make all attributes required
if (param === 'new') {
return Joi.object({
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);
const {value, error} = Joi.object({
_id: IdValidate.get(),

View File

@ -51,7 +51,7 @@
},
{
"_id": {"$oid":"400000000000000000000005"},
"number": "33",
"number": "Rng33",
"type": "granulate",
"color": "black",
"batch": "1653000308",
@ -121,11 +121,11 @@
"numbers": [
{
"color": "black",
"number": 5514263423
"number": "5514263423"
},
{
"color": "natural",
"number": 5514263422
"number": "5514263422"
}
],
"status": 10,
@ -142,11 +142,11 @@
"numbers": [
{
"color": "black",
"number": 5514212901
"number": "5514212901"
},
{
"color": "signalviolet",
"number": 5514612901
"number": "5514612901"
}
],
"status": 10,
@ -176,7 +176,7 @@
"numbers": [
{
"color": "black",
"number": 5513933405
"number": "5513933405"
}
],
"status": 10,
@ -193,7 +193,7 @@
"numbers": [
{
"color": "black",
"number": 5514262406
"number": "5514262406"
}
],
"status": 10,
@ -210,18 +210,35 @@
"numbers": [
{
"color": "natural",
"number": 10000000
"number": "10000000"
}
],
"status": -1,
"__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": [
{
"_id": {"$oid":"700000000000000000000001"},
"sample_id": {"$oid":"400000000000000000000001"},
"number": "B1",
"number": "A1",
"parameters": {
"material": "copper",
"weeks": 3
@ -233,7 +250,7 @@
{
"_id": {"$oid":"700000000000000000000002"},
"sample_id": {"$oid":"400000000000000000000002"},
"number": "B1",
"number": "A1",
"parameters": {
"material": "copper",
"weeks": 3
@ -245,7 +262,7 @@
{
"_id": {"$oid":"700000000000000000000003"},
"sample_id": {"$oid":"400000000000000000000004"},
"number": "B1",
"number": "A1",
"parameters": {
"material": "copper",
"weeks": 3
@ -257,7 +274,7 @@
{
"_id": {"$oid":"700000000000000000000004"},
"sample_id": {"$oid":"400000000000000000000001"},
"number": "B3",
"number": "A2",
"parameters": {
"material": "hot air",
"weeks": 5
@ -429,6 +446,17 @@
"device_name": "",
"key": "000000000000000000001003",
"__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 {
public static auth = {
public static auth = { // test user credentials
admin: {pass: 'Abc123!#', key: '000000000000000000001003'},
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'},
401: {status: 'Unauthorized'},
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}
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);
}
switch (options.method) {
switch (options.method) { // http method
case 'get':
st = st.get(options.url)
break;
@ -56,10 +57,10 @@ export default class TestHelper {
st = st.delete(options.url)
break;
}
if (options.hasOwnProperty('req')) {
if (options.hasOwnProperty('req')) { // request body
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)) {
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/)
.expect(options.httpStatus);
if (options.hasOwnProperty('res')) {
if (options.hasOwnProperty('res')) { // evaluate result
return st.end((err, res) => {
if (err) return done (err);
should(res.body).be.eql(options.res);
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) => {
if (err) return done (err);
should(res.body).be.eql(this.res[options.httpStatus]);
done();
});
}
else {
else { // return object to do .end() manually
return st;
}
}

View File

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