diff --git a/.idea/libraries/dist.xml b/.idea/libraries/dist.xml
new file mode 100644
index 0000000..3d92275
--- /dev/null
+++ b/.idea/libraries/dist.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/api/condition.yaml b/api/condition.yaml
index f924707..ec8b245 100644
--- a/api/condition.yaml
+++ b/api/condition.yaml
@@ -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:
diff --git a/api/material.yaml b/api/material.yaml
index 6a86b38..d184a3f 100644
--- a/api/material.yaml
+++ b/api/material.yaml
@@ -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:
diff --git a/api/measurement.yaml b/api/measurement.yaml
index 2882883..298b04e 100644
--- a/api/measurement.yaml
+++ b/api/measurement.yaml
@@ -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:
diff --git a/api/parameters.yaml b/api/parameters.yaml
index ba8d046..b4586f7 100644
--- a/api/parameters.yaml
+++ b/api/parameters.yaml
@@ -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
\ No newline at end of file
+ type: string
+
+Group:
+ name: group
+ description: 'possible values: new, deleted'
+ in: path
+ required: true
+ schema:
+ type: string
+ example: deleted
\ No newline at end of file
diff --git a/api/sample.yaml b/api/sample.yaml
index 9539053..c699809 100644
--- a/api/sample.yaml
+++ b/api/sample.yaml
@@ -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:
diff --git a/api/schemas.yaml b/api/schemas.yaml
index 3f5098c..c872443 100644
--- a/api/schemas.yaml
+++ b/api/schemas.yaml
@@ -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
diff --git a/src/api.ts b/src/api.ts
index 228f166..59ce0b3 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -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 += 'docs
' + doc[key]['x-doc'] + ' ';
}
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 'docs
' + text + ' ';
- }
}
\ No newline at end of file
diff --git a/src/db.ts b/src/db.ts
index 89c3183..c1d1fbb 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -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]);
}
});
diff --git a/src/helpers/authorize.ts b/src/helpers/authorize.ts
index e2f626a..21d43d5 100644
--- a/src/helpers/authorize.ts
+++ b/src/helpers/authorize.ts
@@ -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);
diff --git a/src/helpers/mail.ts b/src/helpers/mail.ts
index 792f35f..a3d79c1 100644
--- a/src/helpers/mail.ts
+++ b/src/helpers/mail.ts
@@ -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') {
diff --git a/src/index.ts b/src/index.ts
index fc1b149..4ce0581 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -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'});
diff --git a/src/models/material.ts b/src/models/material.ts
index a5378e0..71d6b34 100644
--- a/src/models/material.ts
+++ b/src/models/material.ts
@@ -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);
\ No newline at end of file
diff --git a/src/routes/condition.spec.ts b/src/routes/condition.spec.ts
index 2967108..90c7c43 100644
--- a/src/routes/condition.spec.ts
+++ b/src/routes/condition.spec.ts
@@ -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'}
});
});
});
diff --git a/src/routes/condition.ts b/src/routes/condition.ts
index 89ddce0..f66d10a 100644
--- a/src/routes/condition.ts
+++ b/src/routes/condition.ts
@@ -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;
}
\ No newline at end of file
diff --git a/src/routes/material.spec.ts b/src/routes/material.spec.ts
index 0faf04e..21a278b 100644
--- a/src/routes/material.spec.ts
+++ b/src/routes/material.spec.ts
@@ -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'}
});
});
diff --git a/src/routes/material.ts b/src/routes/material.ts
index 1c33591..dd89985 100644
--- a/src/routes/material.ts
+++ b/src/routes/material.ts
@@ -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'});
diff --git a/src/routes/measurement.spec.ts b/src/routes/measurement.spec.ts
index 7a604d2..7fe4b7f 100644
--- a/src/routes/measurement.spec.ts
+++ b/src/routes/measurement.spec.ts
@@ -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));
diff --git a/src/routes/measurement.ts b/src/routes/measurement.ts
index bb69b3f..eda839e 100644
--- a/src/routes/measurement.ts
+++ b/src/routes/measurement.ts
@@ -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;
}
\ No newline at end of file
diff --git a/src/routes/sample.spec.ts b/src/routes/sample.spec.ts
index 42c8435..df1ad05 100644
--- a/src/routes/sample.spec.ts
+++ b/src/routes/sample.spec.ts
@@ -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'}]}}
});
});
});
diff --git a/src/routes/sample.ts b/src/routes/sample.ts
index 6acb7d2..43acd6e 100644
--- a/src/routes/sample.ts
+++ b/src/routes/sample.ts
@@ -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);
diff --git a/src/routes/template.spec.ts b/src/routes/template.spec.ts
index d9673b7..878b778 100644
--- a/src/routes/template.spec.ts
+++ b/src/routes/template.spec.ts
@@ -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 => {
diff --git a/src/routes/template.ts b/src/routes/template.ts
index 3997944..a8f7413 100644
--- a/src/routes/template.ts
+++ b/src/routes/template.ts
@@ -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;
}
\ No newline at end of file
diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts
index e294cb2..6a7d69e 100644
--- a/src/routes/user.spec.ts
+++ b/src/routes/user.spec.ts
@@ -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;
diff --git a/src/routes/user.ts b/src/routes/user.ts
index 5a2485c..4fb2c0f 100644
--- a/src/routes/user.ts
+++ b/src/routes/user.ts
@@ -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,
You requested to reset your password.
Your new password is:
' + newPass + '
If you did not request a password reset, talk to the sysadmin quickly!
Have a nice day.
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;
\ No newline at end of file
+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;
+}
\ No newline at end of file
diff --git a/src/routes/validate/condition.ts b/src/routes/validate/condition.ts
index f130076..d752ff3 100644
--- a/src/routes/validate/condition.ts
+++ b/src/routes/validate/condition.ts
@@ -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(),
diff --git a/src/routes/validate/id.ts b/src/routes/validate/id.ts
index a9bb70a..6b7b677 100644
--- a/src/routes/validate/id.ts
+++ b/src/routes/validate/id.ts
@@ -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;
}
diff --git a/src/routes/validate/material.ts b/src/routes/validate/material.ts
index c8b6e91..c92f440 100644
--- a/src/routes/validate/material.ts
+++ b/src/routes/validate/material.ts
@@ -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(),
diff --git a/src/routes/validate/measurement.ts b/src/routes/validate/measurement.ts
index 0efaaea..21b38a2 100644
--- a/src/routes/validate/measurement.ts
+++ b/src/routes/validate/measurement.ts
@@ -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(),
diff --git a/src/routes/validate/note_field.ts b/src/routes/validate/note_field.ts
index 7d34d98..68856c9 100644
--- a/src/routes/validate/note_field.ts
+++ b/src/routes/validate/note_field.ts
@@ -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
diff --git a/src/routes/validate/parameters.ts b/src/routes/validate/parameters.ts
index d855815..79e62ef 100644
--- a/src/routes/validate/parameters.ts
+++ b/src/routes/validate/parameters.ts
@@ -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);
diff --git a/src/routes/validate/res400.ts b/src/routes/validate/res400.ts
index 5e032f7..e4595c8 100644
--- a/src/routes/validate/res400.ts
+++ b/src/routes/validate/res400.ts
@@ -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});
}
\ No newline at end of file
diff --git a/src/routes/validate/sample.ts b/src/routes/validate/sample.ts
index aa28304..1b23cb1 100644
--- a/src/routes/validate/sample.ts
+++ b/src/routes/validate/sample.ts
@@ -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(),
diff --git a/src/routes/validate/template.ts b/src/routes/validate/template.ts
index 7cb461d..571f48c 100644
--- a/src/routes/validate/template.ts
+++ b/src/routes/validate/template.ts
@@ -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;
}
}
\ No newline at end of file
diff --git a/src/routes/validate/user.ts b/src/routes/validate/user.ts
index 4472aa8..bd4dfbd 100644
--- a/src/routes/validate/user.ts
+++ b/src/routes/validate/user.ts
@@ -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(),
diff --git a/src/test/db.json b/src/test/db.json
index 619fb75..b78f8e7 100644
--- a/src/test/db.json
+++ b/src/test/db.json
@@ -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
}
]
}
diff --git a/src/test/helper.ts b/src/test/helper.ts
index 26cb5a5..3983959 100644
--- a/src/test/helper.ts
+++ b/src/test/helper.ts
@@ -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;
}
}
diff --git a/src/test/loadDev.ts b/src/test/loadDev.ts
index 690044d..15a6868 100644
--- a/src/test/loadDev.ts
+++ b/src/test/loadDev.ts
@@ -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