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