Archived
2

refactored user.ts

This commit is contained in:
VLE2FE 2020-05-18 14:47:22 +02:00
parent 70aca017f8
commit 5209410009
26 changed files with 282 additions and 156 deletions

View File

@ -2,7 +2,7 @@
get: get:
summary: all samples in overview summary: all samples in overview
description: 'Auth: all, levels: read, write, maintain, dev, admin' description: 'Auth: all, levels: read, write, maintain, dev, admin'
x-doc: returns only samples with status 10 # TODO: methods /samples/new|deleted x-doc: returns only samples with status 10
tags: tags:
- /sample - /sample
responses: responses:
@ -18,6 +18,30 @@
$ref: 'api.yaml#/components/responses/401' $ref: 'api.yaml#/components/responses/401'
500: 500:
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
/samples{group}:
parameters:
- $ref: 'api.yaml#/components/parameters/Group'
get:
summary: all new/deleted samples in overview
description: 'Auth: basic, levels: maintain, admin'
x-doc: returns only samples with status 0/-1
tags:
- /sample
responses:
200:
description: samples overview
content:
application/json:
schema:
type: array
items:
$ref: 'api.yaml#/components/schemas/SampleRefs'
401:
$ref: 'api.yaml#/components/responses/401'
500:
$ref: 'api.yaml#/components/responses/500'
/sample/{id}: /sample/{id}:
parameters: parameters:
- $ref: 'api.yaml#/components/parameters/Id' - $ref: 'api.yaml#/components/parameters/Id'
@ -130,7 +154,7 @@
get: get:
summary: list all existing field names for custom notes fields summary: list all existing field names for custom notes fields
description: 'Auth: all, levels: read, write, maintain, dev, admin' description: 'Auth: all, levels: read, write, maintain, dev, admin'
x-doc: integrity has to be ensured # TODO: implement mechanism to regularly check note_fields x-doc: integrity has to be ensured
tags: tags:
- /sample - /sample
responses: responses:

View File

@ -20,7 +20,7 @@ export default class api {
apiDoc = doc; apiDoc = doc;
apiDoc.paths = apiDoc.paths.allOf.reduce((s, e) => Object.assign(s, e)); // bundle routes apiDoc.paths = apiDoc.paths.allOf.reduce((s, e) => Object.assign(s, e)); // bundle routes
apiDoc = this.resolveXDoc(apiDoc); apiDoc = this.resolveXDoc(apiDoc);
oasParser.validate(apiDoc, (err, api) => { oasParser.validate(apiDoc, (err, api) => { // validate oas schema
if (err) { if (err) {
console.error(err); console.error(err);
} }
@ -35,8 +35,8 @@ export default class api {
private static resolveXDoc (doc) { // resolve x-doc properties recursively private static resolveXDoc (doc) { // resolve x-doc properties recursively
Object.keys(doc).forEach(key => { Object.keys(doc).forEach(key => {
if (doc[key] !== null && doc[key].hasOwnProperty('x-doc')) { if (doc[key] !== null && doc[key].hasOwnProperty('x-doc')) { // add x-doc to description, is styled via css
doc[key].description += this.addHtml(doc[key]['x-doc']); doc[key].description += '<details class="docs"><summary>docs</summary>' + doc[key]['x-doc'] + '</details>';
} }
else if (typeof doc[key] === 'object' && doc[key] !== null) { // go deeper into recursion else if (typeof doc[key] === 'object' && doc[key] !== null) { // go deeper into recursion
doc[key] = this.resolveXDoc(doc[key]); doc[key] = this.resolveXDoc(doc[key]);
@ -44,8 +44,4 @@ export default class api {
}); });
return doc; return doc;
} }
private static addHtml (text) { // add docs HTML
return '<details class="docs"><summary>docs</summary>' + text + '</details>';
}
} }

View File

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

View File

@ -63,7 +63,7 @@ function basic (req, next): any { // checks basic auth and returns changed user
if (data.length === 1) { // one user found if (data.length === 1) { // one user found
bcrypt.compare(auth.pass, data[0].pass, (err, res) => { // check password bcrypt.compare(auth.pass, data[0].pass, (err, res) => { // check password
if (err) return next(err); if (err) return next(err);
if (res === true) { if (res === true) { // password correct
resolve({level: data[0].level, name: data[0].name, id: data[0]._id.toString(), location: data[0].location}); resolve({level: data[0].level, name: data[0].name, id: data[0]._id.toString(), location: data[0].location});
} }
else { else {
@ -84,7 +84,7 @@ function basic (req, next): any { // checks basic auth and returns changed user
function key (req, next): any { // checks API key and returns changed user object function key (req, next): any { // checks API key and returns changed user object
return new Promise(resolve => { return new Promise(resolve => {
if (req.query.key !== undefined) { if (req.query.key !== undefined) { // key available
UserModel.find({key: req.query.key}).lean().exec( (err, data: any) => { // find user UserModel.find({key: req.query.key}).lean().exec( (err, data: any) => { // find user
if (err) return next(err); if (err) return next(err);
if (data.length === 1) { // one user found if (data.length === 1) { // one user found

View File

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

View File

@ -5,7 +5,7 @@ import mongoSanitize from 'mongo-sanitize';
import api from './api'; import api from './api';
import db from './db'; import db from './db';
// TODO: overall commenting/documentation review
// tell if server is running in debug or production environment // tell if server is running in debug or production environment
console.info(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT ====='); console.info(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT =====');

View File

@ -37,13 +37,14 @@ router.put('/condition/' + IdValidate.parameter(), async (req, res, next) => {
if (!data) { if (!data) {
res.status(404).json({status: 'Not found'}); res.status(404).json({status: 'Not found'});
} }
// add properties needed for sampleIdCheck // add properties needed for sampleIdCheck
condition.treatment_template = data.treatment_template; condition.treatment_template = data.treatment_template;
condition.sample_id = data.sample_id; condition.sample_id = data.sample_id;
if (!await sampleIdCheck(condition, req, res, next)) return; if (!await sampleIdCheck(condition, req, res, next)) return;
if (condition.parameters) { if (condition.parameters) {
condition.parameters = _.assign({}, data.parameters, condition.parameters); condition.parameters = _.assign({}, data.parameters, condition.parameters);
if (!_.isEqual(condition.parameters, data.parameters)) { if (!_.isEqual(condition.parameters, data.parameters)) { // parameters did not change
condition.status = 0; condition.status = 0;
} }
} }
@ -83,7 +84,7 @@ router.post('/condition/new', async (req, res, next) => {
condition.number = await numberGenerate(condition, treatmentData, next); condition.number = await numberGenerate(condition, treatmentData, next);
if (!condition.number) return; if (!condition.number) return;
condition.status = 0; condition.status = 0; // set status to new
await new ConditionModel(condition).save((err, data) => { await new ConditionModel(condition).save((err, data) => {
if (err) return next(err); if (err) return next(err);
res.json(ConditionValidate.output(data.toObject())); res.json(ConditionValidate.output(data.toObject()));
@ -105,8 +106,8 @@ async function sampleIdCheck (condition, req, res, next) { // validate sample_i
return true; return true;
} }
async function numberGenerate (condition, treatmentData, next) { // validate number, returns false if invalid async function numberGenerate (condition, treatmentData, next) { // generate number, returns false on error
const conditionData = await ConditionModel 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')}) .find({sample_id: condition.sample_id, number: new RegExp('^' + treatmentData.number_prefix + '[0-9]+$', 'm')})
.sort({number: -1}) .sort({number: -1})
.limit(1) .limit(1)
@ -114,7 +115,7 @@ async function numberGenerate (condition, treatmentData, next) { // validate nu
.exec() .exec()
.catch(err => next(err)) as any; .catch(err => next(err)) as any;
if (conditionData instanceof Error) return false; if (conditionData instanceof Error) return false;
return treatmentData.number_prefix + (conditionData.length > 0 ? Number(conditionData[0].number.replace(/[^0-9]+/g, '')) + 1 : 1); 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) { // validate treatment template, returns false if invalid, otherwise template data async function treatmentCheck (condition, param, res, next) { // validate treatment template, returns false if invalid, otherwise template data

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import NoteModel from '../models/note';
import NoteFieldModel from '../models/note_field'; import NoteFieldModel from '../models/note_field';
import TestHelper from "../test/helper"; import TestHelper from "../test/helper";
// TODO: think again which parameters are required at POST
describe('/sample', () => { describe('/sample', () => {
let server; let server;
@ -23,16 +22,16 @@ describe('/sample', () => {
if (err) return done(err); if (err) return done(err);
const json = require('../test/db.json'); const json = require('../test/db.json');
should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status === 10).length); should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status === 10).length);
should(res.body).matchEach(material => { should(res.body).matchEach(sample => {
should(material).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id'); should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id');
should(material).have.property('_id').be.type('string'); should(sample).have.property('_id').be.type('string');
should(material).have.property('number').be.type('string'); should(sample).have.property('number').be.type('string');
should(material).have.property('type').be.type('string'); should(sample).have.property('type').be.type('string');
should(material).have.property('color').be.type('string'); should(sample).have.property('color').be.type('string');
should(material).have.property('batch').be.type('string'); should(sample).have.property('batch').be.type('string');
should(material).have.property('material_id').be.type('string'); should(sample).have.property('material_id').be.type('string');
should(material).have.property('note_id'); should(sample).have.property('note_id');
should(material).have.property('user_id').be.type('string'); should(sample).have.property('user_id').be.type('string');
}); });
done(); done();
}); });
@ -70,6 +69,94 @@ describe('/sample', () => {
}); });
}); });
describe('GET /samples/{group}', () => {
it('returns all new samples', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/samples/new',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
const json = require('../test/db.json');
let asyncCounter = res.body.length;
should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status === 0).length);
should(res.body).matchEach(sample => {
should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id');
should(sample).have.property('_id').be.type('string');
should(sample).have.property('number').be.type('string');
should(sample).have.property('type').be.type('string');
should(sample).have.property('color').be.type('string');
should(sample).have.property('batch').be.type('string');
should(sample).have.property('material_id').be.type('string');
should(sample).have.property('note_id');
should(sample).have.property('user_id').be.type('string');
SampleModel.findById(sample._id).lean().exec((err, data) => {
should(data).have.property('status', 0);
if (--asyncCounter === 0) {
done();
}
});
});
done();
});
});
it('returns all deleted samples', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/samples/deleted',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
const json = require('../test/db.json');
let asyncCounter = res.body.length;
should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status === -1).length);
should(res.body).matchEach(sample => {
should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id');
should(sample).have.property('_id').be.type('string');
should(sample).have.property('number').be.type('string');
should(sample).have.property('type').be.type('string');
should(sample).have.property('color').be.type('string');
should(sample).have.property('batch').be.type('string');
should(sample).have.property('material_id').be.type('string');
should(sample).have.property('note_id');
should(sample).have.property('user_id').be.type('string');
SampleModel.findById(sample._id).lean().exec((err, data) => {
should(data).have.property('status', -1);
if (--asyncCounter === 0) {
done();
}
});
});
done();
});
});
it('rejects requests from a write user', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/samples/new',
auth: {basic: 'janedoe'},
httpStatus: 403
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/samples/new',
auth: {key: 'admin'},
httpStatus: 401
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/samples/new',
httpStatus: 401
});
});
});
describe('PUT /sample/{id}', () => { describe('PUT /sample/{id}', () => {
it('returns the right sample', done => { it('returns the right sample', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
@ -194,12 +281,10 @@ describe('/sample', () => {
}).end(err => { }).end(err => {
if (err) return done(err); if (err) return done(err);
NoteFieldModel.findOne({name: 'not allowed for new applications'}).lean().exec((err, data) => { NoteFieldModel.findOne({name: 'not allowed for new applications'}).lean().exec((err, data) => {
console.log(data);
if (err) return done(err); if (err) return done(err);
should(data).have.property('qty', 1); should(data).have.property('qty', 1);
NoteFieldModel.findOne({name: 'field1'}).lean().exec((err, data) => { NoteFieldModel.findOne({name: 'field1'}).lean().exec((err, data) => {
if (err) return done(err); if (err) return done(err);
console.log(data);
should(data).have.property('qty', 1); should(data).have.property('qty', 1);
done(); done();
}); });
@ -233,7 +318,6 @@ describe('/sample', () => {
if (err) return done (err); if (err) return done (err);
NoteModel.findById(res.body.note_id).lean().exec((err, data) => { NoteModel.findById(res.body.note_id).lean().exec((err, data) => {
if (err) return done (err); if (err) return done (err);
console.log(data);
should(data).not.be.null(); should(data).not.be.null();
should(data).have.property('comment', 'Stoff gesperrt'); should(data).have.property('comment', 'Stoff gesperrt');
should(data).have.property('sample_references').have.lengthOf(0); should(data).have.property('sample_references').have.lengthOf(0);
@ -448,7 +532,6 @@ describe('/sample', () => {
setTimeout(() => { // background action takes some time before we can check setTimeout(() => { // background action takes some time before we can check
NoteModel.findById('500000000000000000000003').lean().exec((err, data: any) => { NoteModel.findById('500000000000000000000003').lean().exec((err, data: any) => {
if (err) return done(err); if (err) return done(err);
console.log(data);
should(data).have.property('sample_references').with.lengthOf(1); should(data).have.property('sample_references').with.lengthOf(1);
should(data.sample_references[0].id.toString()).be.eql('400000000000000000000003'); should(data.sample_references[0].id.toString()).be.eql('400000000000000000000003');
should(data.sample_references[0]).have.property('relation', 'part to sample'); should(data.sample_references[0]).have.property('relation', 'part to sample');

View File

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

View File

@ -200,7 +200,6 @@ describe('/template', () => {
httpStatus: 200, httpStatus: 200,
req: {parameters: [{name: 'time', range: {type: 'array'}}]} req: {parameters: [{name: 'time', range: {type: 'array'}}]}
}).end((err, res) => { }).end((err, res) => {
console.log(res.body);
if (err) return done(err); if (err) return done(err);
should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, number_prefix: 'A', parameters: [{name: 'time', range: {type: 'array'}}]}); should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, number_prefix: 'A', parameters: [{name: 'time', range: {type: 'array'}}]});
done(); done();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -43,7 +43,7 @@ export default class TemplateValidate {
) )
}; };
static input (data, param, template) { // validate data, param: new(everything required)/change(available attributes are validated) static input (data, param, template) { // validate input, set param to 'new' to make all attributes required
if (param === 'new') { if (param === 'new') {
if (template === 'treatment') { if (template === 'treatment') {
return Joi.object({ return Joi.object({
@ -79,10 +79,10 @@ export default class TemplateValidate {
} }
} }
static output (data, template) { // validate output from database for needed properties, strip everything else static output (data, template) { // validate output and strip unwanted properties, returns null if not valid
data = IdValidate.stringify(data); data = IdValidate.stringify(data);
let joiObject; let joiObject;
if (template === 'treatment') { if (template === 'treatment') { // differentiate between measurement and treatment (has number_prefix) template
joiObject = { joiObject = {
_id: IdValidate.get(), _id: IdValidate.get(),
name: this.template.name, name: this.template.name,

View File

@ -33,7 +33,7 @@ export default class UserValidate { // validate input for user
private static specialUsernames = ['admin', 'user', 'key', 'new', 'passreset']; // names a user cannot take private static specialUsernames = ['admin', 'user', 'key', 'new', 'passreset']; // names a user cannot take
static input (data, param) { static input (data, param) { // validate input, set param to 'new' to make all attributes required
if (param === 'new') { if (param === 'new') {
return Joi.object({ return Joi.object({
name: this.user.name.required(), name: this.user.name.required(),
@ -68,7 +68,7 @@ export default class UserValidate { // validate input for user
} }
} }
static output (data) { // validate output from database for needed properties, strip everything else static output (data) { // validate output and strip unwanted properties, returns null if not valid
data = IdValidate.stringify(data); data = IdValidate.stringify(data);
const {value, error} = Joi.object({ const {value, error} = Joi.object({
_id: IdValidate.get(), _id: IdValidate.get(),

View File

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

View File

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