diff --git a/package-lock.json b/package-lock.json index c72166e..2372f4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { - "name": "dfop-api", - "version": "1.0.0", + "name": "definma-api", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/customTypes/express.ts b/src/customTypes/express.ts index ce89d49..1e5a5cc 100644 --- a/src/customTypes/express.ts +++ b/src/customTypes/express.ts @@ -9,10 +9,10 @@ /* =================== USAGE =================== - import * as express from "express"; - let app = express(); + import * as express from "express"; + let app = express(); - =============================================== */ + =============================================== */ /// /// @@ -28,96 +28,96 @@ import * as qs from "qs"; declare function e(): core.Express; declare namespace e { - /** - * This is a built-in middleware function in Express. It parses incoming requests with JSON payloads and is based on - * body-parser. - * @since 4.16.0 - */ - let json: typeof bodyParser.json; + /** + * This is a built-in middleware function in Express. It parses incoming requests with JSON payloads and is based on + * body-parser. + * @since 4.16.0 + */ + let json: typeof bodyParser.json; - /** - * This is a built-in middleware function in Express. It parses incoming requests with Buffer payloads and is based - * on body-parser. - * @since 4.17.0 - */ - let raw: typeof bodyParser.raw; + /** + * This is a built-in middleware function in Express. It parses incoming requests with Buffer payloads and is based + * on body-parser. + * @since 4.17.0 + */ + let raw: typeof bodyParser.raw; - /** - * This is a built-in middleware function in Express. It parses incoming requests with text payloads and is based on - * body-parser. - * @since 4.17.0 - */ - let text: typeof bodyParser.text; + /** + * This is a built-in middleware function in Express. It parses incoming requests with text payloads and is based on + * body-parser. + * @since 4.17.0 + */ + let text: typeof bodyParser.text; - /** - * These are the exposed prototypes. - */ - let application: Application; - let request: Request; - let response: Response; + /** + * These are the exposed prototypes. + */ + let application: Application; + let request: Request; + let response: Response; - /** - * This is a built-in middleware function in Express. It serves static files and is based on serve-static. - */ - let static: typeof serveStatic; + /** + * This is a built-in middleware function in Express. It serves static files and is based on serve-static. + */ + let static: typeof serveStatic; - /** - * This is a built-in middleware function in Express. It parses incoming requests with urlencoded payloads and is - * based on body-parser. - * @since 4.16.0 - */ - let urlencoded: typeof bodyParser.urlencoded; + /** + * This is a built-in middleware function in Express. It parses incoming requests with urlencoded payloads and is + * based on body-parser. + * @since 4.16.0 + */ + let urlencoded: typeof bodyParser.urlencoded; - /** - * This is a built-in middleware function in Express. It parses incoming request query parameters. - */ - export function query(options: qs.IParseOptions | typeof qs.parse): Handler; + /** + * This is a built-in middleware function in Express. It parses incoming request query parameters. + */ + export function query(options: qs.IParseOptions | typeof qs.parse): Handler; - export function Router(options?: RouterOptions): core.Router; + export function Router(options?: RouterOptions): core.Router; - interface RouterOptions { - /** - * Enable case sensitivity. - */ - caseSensitive?: boolean; + interface RouterOptions { + /** + * Enable case sensitivity. + */ + caseSensitive?: boolean; - /** - * Preserve the req.params values from the parent router. - * If the parent and the child have conflicting param names, the child’s value take precedence. - * - * @default false - * @since 4.5.0 - */ - mergeParams?: boolean; + /** + * Preserve the req.params values from the parent router. + * If the parent and the child have conflicting param names, the child’s value take precedence. + * + * @default false + * @since 4.5.0 + */ + mergeParams?: boolean; - /** - * Enable strict routing. - */ - strict?: boolean; - } + /** + * Enable strict routing. + */ + strict?: boolean; + } - interface Application extends core.Application { } - interface CookieOptions extends core.CookieOptions { } - interface Errback extends core.Errback { } - interface ErrorRequestHandler

- extends core.ErrorRequestHandler { } - interface Express extends core.Express { } - interface Handler extends core.Handler { } - interface IRoute extends core.IRoute { } - interface IRouter extends core.IRouter { } - interface IRouterHandler extends core.IRouterHandler { } - interface IRouterMatcher extends core.IRouterMatcher { } - interface MediaType extends core.MediaType { } - interface NextFunction extends core.NextFunction { } - interface Request

extends core.Request { } - interface RequestHandler

extends core.RequestHandler { } - interface RequestParamHandler extends core.RequestParamHandler { } - export interface Response extends core.Response { } - interface Router extends core.Router { } - interface Send extends core.Send { } + interface Application extends core.Application { } + interface CookieOptions extends core.CookieOptions { } + interface Errback extends core.Errback { } + interface ErrorRequestHandler

+ extends core.ErrorRequestHandler { } + interface Express extends core.Express { } + interface Handler extends core.Handler { } + interface IRoute extends core.IRoute { } + interface IRouter extends core.IRouter { } + interface IRouterHandler extends core.IRouterHandler { } + interface IRouterMatcher extends core.IRouterMatcher { } + interface MediaType extends core.MediaType { } + interface NextFunction extends core.NextFunction { } + interface Request

extends core.Request { } + interface RequestHandler

extends core.RequestHandler { } + interface RequestParamHandler extends core.RequestParamHandler { } + export interface Response extends core.Response { } + interface Router extends core.Router { } + interface Send extends core.Send { } } export = e; diff --git a/src/helpers/authorize.ts b/src/helpers/authorize.ts index 49b9278..82573c8 100644 --- a/src/helpers/authorize.ts +++ b/src/helpers/authorize.ts @@ -9,111 +9,111 @@ import globals from '../globals'; // req.authDetails returns eg. {methods: ['basic'], username: 'johndoe', level: 'write'} module.exports = async (req, res, next) => { - let givenMethod = ''; // authorization method given by client, basic taken preferred - let user = {name: '', level: '', id: '', location: '', models: []}; // user object + let givenMethod = ''; // authorization method given by client, basic taken preferred + let user = {name: '', level: '', id: '', location: '', models: []}; // user object - // test authentications - const userBasic = await basic(req, next); + // test authentications + const userBasic = await basic(req, next); - if (userBasic) { // basic available - givenMethod = 'basic'; - user = userBasic; - } - else { // if basic not available, test key - const userKey = await key(req, next); - if (userKey) { - givenMethod = 'key'; - user = userKey; - } - } + if (userBasic) { // basic available + givenMethod = 'basic'; + user = userBasic; + } + else { // if basic not available, test key + const userKey = await key(req, next); + if (userKey) { + givenMethod = 'key'; + user = userKey; + } + } - req.auth = (res, levels, method = 'all') => { - if (givenMethod === method || (method === 'all' && givenMethod !== '')) { // method is available - if (levels.indexOf(user.level) > -1) { // level is available - return true; - } - else { - res.status(403).json({status: 'Forbidden'}); - return false; - } - } - else { - res.status(401).json({status: 'Unauthorized'}); - return false; - } - } + req.auth = (res, levels, method = 'all') => { + if (givenMethod === method || (method === 'all' && givenMethod !== '')) { // method is available + if (levels.indexOf(user.level) > -1) { // level is available + return true; + } + else { + res.status(403).json({status: 'Forbidden'}); + return false; + } + } + else { + res.status(401).json({status: 'Unauthorized'}); + return false; + } + } - req.authDetails = { - method: givenMethod, - username: user.name, - level: user.level, - id: user.id, - location: user.location, - models: user.models - }; + req.authDetails = { + method: givenMethod, + username: user.name, + level: user.level, + id: user.id, + location: user.location, + models: user.models + }; - next(); + next(); } function basic (req, next): any { // checks basic auth and returns changed user object - return new Promise(resolve => { - const auth = basicAuth(req); - if (auth !== undefined) { // basic auth available - UserModel.find({name: auth.name, status: globals.status.new}).lean().exec( (err, data: any) => { // find user - if (err) return next(err); - 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) { // password correct - resolve({ - level: Object.entries(globals.levels).find(e => e[1] === data[0].level)[0], - name: data[0].name, - id: data[0]._id.toString(), - location: data[0].location, - models: data[0].models - }); - } - else { - resolve(null); - } - }); - } - else { - resolve(null); - } - }); - } - else { - resolve(null); - } - }); + return new Promise(resolve => { + const auth = basicAuth(req); + if (auth !== undefined) { // basic auth available + UserModel.find({name: auth.name, status: globals.status.new}).lean().exec( (err, data: any) => { // find user + if (err) return next(err); + 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) { // password correct + resolve({ + level: Object.entries(globals.levels).find(e => e[1] === data[0].level)[0], + name: data[0].name, + id: data[0]._id.toString(), + location: data[0].location, + models: data[0].models + }); + } + else { + resolve(null); + } + }); + } + else { + resolve(null); + } + }); + } + else { + resolve(null); + } + }); } function key (req, next): any { // checks API key and returns changed user object - return new Promise(resolve => { - if (req.query.key !== undefined) { // key available - UserModel.find({key: req.query.key, status: globals.status.new}).lean().exec( (err, data: any) => { // find user - if (err) return next(err); - if (data.length === 1) { // one user found - resolve({ - level: Object.entries(globals.levels).find(e => e[1] === data[0].level)[0], - name: data[0].name, - id: data[0]._id.toString(), - location: data[0].location, - models: data[0].models - }); - if (!/^\/api/m.test(req.url)){ - delete req.query.key; // delete query parameter to avoid interference with later validation - } - } - else { - resolve(null); - } - }); - } - else { - resolve(null); - } - }); -} \ No newline at end of file + return new Promise(resolve => { + if (req.query.key !== undefined) { // key available + UserModel.find({key: req.query.key, status: globals.status.new}).lean().exec( (err, data: any) => { // find user + if (err) return next(err); + if (data.length === 1) { // one user found + resolve({ + level: Object.entries(globals.levels).find(e => e[1] === data[0].level)[0], + name: data[0].name, + id: data[0]._id.toString(), + location: data[0].location, + models: data[0].models + }); + if (!/^\/api/m.test(req.url)){ + delete req.query.key; // delete query parameter to avoid interference with later validation + } + } + else { + resolve(null); + } + }); + } + else { + resolve(null); + } + }); +} diff --git a/src/helpers/csv.ts b/src/helpers/csv.ts index 5e8ae03..d5b16ab 100644 --- a/src/helpers/csv.ts +++ b/src/helpers/csv.ts @@ -2,8 +2,8 @@ import {parseAsync} from 'json2csv'; import flatten from './flatten'; export default function csv(input: any[], f: (err, data) => void) { // parse JSON to CSV - parseAsync(input.map(e => flatten(e)), {includeEmptyRows: true}) - .then(csv => f(null, csv)) - .catch(err => f(err, null)); + parseAsync(input.map(e => flatten(e)), {includeEmptyRows: true}) + .then(csv => f(null, csv)) + .catch(err => f(err, null)); } diff --git a/src/helpers/flatten.ts b/src/helpers/flatten.ts index d28bd64..54c07c8 100644 --- a/src/helpers/flatten.ts +++ b/src/helpers/flatten.ts @@ -1,42 +1,42 @@ import globals from '../globals'; export default function flatten (data, keepArray = false) { // flatten object: {a: {b: true}} -> {a.b: true} - const result = {}; - function recurse (cur, prop) { - if (Object(cur) !== cur || Object.keys(cur).length === 0) { // simple value - result[prop] = cur; - } - else if (prop === `${globals.spectrum.spectrum}.${globals.spectrum.dpt}`) { // convert spectrum for ML - result[prop + '.labels'] = cur.map(e => parseFloat(e[0])); - result[prop + '.values'] = cur.map(e => parseFloat(e[1])); - } - else if (Array.isArray(cur)) { - if (keepArray) { - result[prop] = cur; - } - else { // array to string - if (cur.length && (Object(cur[0]) !== cur || Object.keys(cur[0]).length === 0)) { // array of non-objects - result[prop] = '[' + cur.join(', ') + ']'; - } - else { - let l = 0; - for(let i = 0, l = cur.length; i < l; i++) - recurse(cur[i], prop + "[" + i + "]"); - if (l == 0) - result[prop] = []; - } - } - } - else { // object - let isEmpty = true; - for (let p in cur) { - isEmpty = false; - recurse(cur[p], prop ? prop+"."+p : p); - } - if (isEmpty && prop) - result[prop] = {}; - } - } - recurse(data, ''); - return result; -} \ No newline at end of file + const result = {}; + function recurse (cur, prop) { + if (Object(cur) !== cur || Object.keys(cur).length === 0) { // simple value + result[prop] = cur; + } + else if (prop === `${globals.spectrum.spectrum}.${globals.spectrum.dpt}`) { // convert spectrum for ML + result[prop + '.labels'] = cur.map(e => parseFloat(e[0])); + result[prop + '.values'] = cur.map(e => parseFloat(e[1])); + } + else if (Array.isArray(cur)) { + if (keepArray) { + result[prop] = cur; + } + else { // array to string + if (cur.length && (Object(cur[0]) !== cur || Object.keys(cur[0]).length === 0)) { // array of non-objects + result[prop] = '[' + cur.join(', ') + ']'; + } + else { + let l = 0; + for(let i = 0, l = cur.length; i < l; i++) + recurse(cur[i], prop + "[" + i + "]"); + if (l == 0) + result[prop] = []; + } + } + } + else { // object + let isEmpty = true; + for (let p in cur) { + isEmpty = false; + recurse(cur[p], prop ? prop+"."+p : p); + } + if (isEmpty && prop) + result[prop] = {}; + } + } + recurse(data, ''); + return result; +} diff --git a/src/helpers/mail.ts b/src/helpers/mail.ts index 40326c3..1fdf01e 100644 --- a/src/helpers/mail.ts +++ b/src/helpers/mail.ts @@ -4,86 +4,86 @@ import axios from 'axios'; export default class Mail{ - static readonly address = 'definma@bosch-iot.com'; // email address - static uri: string; // mail API URI - static auth = {username: '', password: ''}; // mail API credentials - static mailPass: string; // mail API generates password + static readonly address = 'definma@bosch-iot.com'; // email address + static uri: string; // mail API URI + static auth = {username: '', password: ''}; // mail API credentials + static mailPass: string; // mail API generates password - static init() { - if (process.env.NODE_ENV === 'production') { // only send mails in production - this.mailPass = Array(64).fill(0).map(() => Math.floor(Math.random() * 10)).join(''); - this.uri = JSON.parse(process.env.VCAP_SERVICES).Mail[0].credentials.uri; - this.auth.username = JSON.parse(process.env.VCAP_SERVICES).Mail[0].credentials.username; - this.auth.password = JSON.parse(process.env.VCAP_SERVICES).Mail[0].credentials.password; - axios({ // get registered mail addresses - method: 'get', - url: this.uri + '/management/userDomainMapping', - auth: this.auth - }).then(res => { - return new Promise(async (resolve, reject) => { - try { - if (res.data.addresses.indexOf(this.address) < 0) { // mail address not registered - if (res.data.addresses.length) { // delete wrong registered mail address - await axios({ - method: 'delete', - url: this.uri + '/management/mailAddresses/' + res.data.addresses[0], - auth: this.auth - }); - } - await axios({ // register right mail address - method: 'post', - url: this.uri + '/management/mailAddresses/' + this.address, - auth: this.auth - }); - } - resolve(); - } - catch (e) { - reject(e); - } - }); - }).then(() => { - return axios({ // set new mail password - method: 'put', - url: this.uri + '/management/mailAddresses/' + this.address + '/password/' + this.mailPass, - auth: this.auth - }); - }).then(() => { // init done successfully - console.info('Mail service established successfully'); - }).catch(err => { // somewhere an error occurred - console.error(`Mail init error: ${err.request.method} ${err.request.path}: ${err.response.status}`, - err.response.data); - }); - } - } + static init() { + if (process.env.NODE_ENV === 'production') { // only send mails in production + this.mailPass = Array(64).fill(0).map(() => Math.floor(Math.random() * 10)).join(''); + this.uri = JSON.parse(process.env.VCAP_SERVICES).Mail[0].credentials.uri; + this.auth.username = JSON.parse(process.env.VCAP_SERVICES).Mail[0].credentials.username; + this.auth.password = JSON.parse(process.env.VCAP_SERVICES).Mail[0].credentials.password; + axios({ // get registered mail addresses + method: 'get', + url: this.uri + '/management/userDomainMapping', + auth: this.auth + }).then(res => { + return new Promise(async (resolve, reject) => { + try { + if (res.data.addresses.indexOf(this.address) < 0) { // mail address not registered + if (res.data.addresses.length) { // delete wrong registered mail address + await axios({ + method: 'delete', + url: this.uri + '/management/mailAddresses/' + res.data.addresses[0], + auth: this.auth + }); + } + await axios({ // register right mail address + method: 'post', + url: this.uri + '/management/mailAddresses/' + this.address, + auth: this.auth + }); + } + resolve(); + } + catch (e) { + reject(e); + } + }); + }).then(() => { + return axios({ // set new mail password + method: 'put', + url: this.uri + '/management/mailAddresses/' + this.address + '/password/' + this.mailPass, + auth: this.auth + }); + }).then(() => { // init done successfully + console.info('Mail service established successfully'); + }).catch(err => { // somewhere an error occurred + console.error(`Mail init error: ${err.request.method} ${err.request.path}: ${err.response.status}`, + err.response.data); + }); + } + } - static send (mailAddress, subject, content, f: (x?) => void = () => {}) { // callback executed empty or with error - if (process.env.NODE_ENV === 'production') { // only send mails in production - axios({ - method: 'post', - url: this.uri + '/email', - auth: this.auth, - data: { - recipients: [{to: mailAddress}], - subject: {content: subject}, - body: { - content: content, - contentType: "text/html" - }, - from: { - eMail: this.address, - password: this.mailPass - } - } - }).then(() => { - f(); - }).catch((err) => { - f(err); - }); - } - else { // dev dummy replacement - console.info('Sending mail to ' + mailAddress + ': -- ' + subject + ' -- ' + content); - f(); - } - } + static send (mailAddress, subject, content, f: (x?) => void = () => {}) { // callback executed empty or with error + if (process.env.NODE_ENV === 'production') { // only send mails in production + axios({ + method: 'post', + url: this.uri + '/email', + auth: this.auth, + data: { + recipients: [{to: mailAddress}], + subject: {content: subject}, + body: { + content: content, + contentType: "text/html" + }, + from: { + eMail: this.address, + password: this.mailPass + } + } + }).then(() => { + f(); + }).catch((err) => { + f(err); + }); + } + else { // dev dummy replacement + console.info('Sending mail to ' + mailAddress + ': -- ' + subject + ' -- ' + content); + f(); + } + } } diff --git a/src/models/changelog.ts b/src/models/changelog.ts index 57701a9..0a30f32 100644 --- a/src/models/changelog.ts +++ b/src/models/changelog.ts @@ -1,11 +1,11 @@ import mongoose from 'mongoose'; const ChangelogSchema = new mongoose.Schema({ - action: String, - collection_name: String, - conditions: mongoose.Schema.Types.Mixed, - data: Object, - user_id: mongoose.Schema.Types.ObjectId + action: String, + collection_name: String, + conditions: mongoose.Schema.Types.Mixed, + data: Object, + user_id: mongoose.Schema.Types.ObjectId }, {minimize: false, strict: false}); -export default mongoose.model>('changelog', ChangelogSchema); \ No newline at end of file +export default mongoose.model>('changelog', ChangelogSchema); diff --git a/src/models/condition_template.ts b/src/models/condition_template.ts index ca61da2..ab80dfa 100644 --- a/src/models/condition_template.ts +++ b/src/models/condition_template.ts @@ -2,19 +2,19 @@ import mongoose from 'mongoose'; import db from '../db'; const ConditionTemplateSchema = new mongoose.Schema({ - first_id: mongoose.Schema.Types.ObjectId, - name: String, - version: Number, - parameters: [new mongoose.Schema({ - name: String, - range: mongoose.Schema.Types.Mixed - } ,{ _id : false })] + first_id: mongoose.Schema.Types.ObjectId, + name: String, + version: Number, + parameters: [new mongoose.Schema({ + name: String, + range: mongoose.Schema.Types.Mixed + } ,{ _id : false })] }, {minimize: false}); // to allow empty objects // changelog query helper ConditionTemplateSchema.query.log = function > (req) { - db.log(req, this); - return this; + db.log(req, this); + return this; } -export default mongoose.model>('condition_template', ConditionTemplateSchema); \ No newline at end of file +export default mongoose.model>('condition_template', ConditionTemplateSchema); diff --git a/src/models/help.ts b/src/models/help.ts index a04ae33..da5ee6d 100644 --- a/src/models/help.ts +++ b/src/models/help.ts @@ -2,15 +2,15 @@ import db from '../db'; import mongoose from 'mongoose'; const HelpSchema = new mongoose.Schema({ - key: {type: String, index: {unique: true}}, - level: String, - text: String + key: {type: String, index: {unique: true}}, + level: String, + text: String }, {minimize: false}); // changelog query helper HelpSchema.query.log = function > (req) { - db.log(req, this); - return this; + db.log(req, this); + return this; } -export default mongoose.model>('help', HelpSchema); \ No newline at end of file +export default mongoose.model>('help', HelpSchema); diff --git a/src/models/material.ts b/src/models/material.ts index 4790a44..b36b51d 100644 --- a/src/models/material.ts +++ b/src/models/material.ts @@ -4,20 +4,20 @@ import MaterialGroupsModel from '../models/material_groups'; import db from '../db'; const MaterialSchema = new mongoose.Schema({ - name: {type: String, index: {unique: true}}, - supplier_id: {type: mongoose.Schema.Types.ObjectId, ref: MaterialSupplierModel}, - group_id: {type: mongoose.Schema.Types.ObjectId, ref: MaterialGroupsModel}, - properties: mongoose.Schema.Types.Mixed, - numbers: [String], - status: String + name: {type: String, index: {unique: true}}, + supplier_id: {type: mongoose.Schema.Types.ObjectId, ref: MaterialSupplierModel}, + group_id: {type: mongoose.Schema.Types.ObjectId, ref: MaterialGroupsModel}, + properties: mongoose.Schema.Types.Mixed, + numbers: [String], + status: String }, {minimize: false}); // changelog query helper MaterialSchema.query.log = function > (req) { - db.log(req, this); - return this; + db.log(req, this); + return this; } MaterialSchema.index({supplier_id: 1}); MaterialSchema.index({group_id: 1}); -export default mongoose.model>('material', MaterialSchema); \ No newline at end of file +export default mongoose.model>('material', MaterialSchema); diff --git a/src/models/material_groups.ts b/src/models/material_groups.ts index 00be706..6aba0e6 100644 --- a/src/models/material_groups.ts +++ b/src/models/material_groups.ts @@ -2,13 +2,13 @@ import mongoose from 'mongoose'; import db from '../db'; const MaterialGroupsSchema = new mongoose.Schema({ - name: {type: String, index: {unique: true}} + name: {type: String, index: {unique: true}} }); // changelog query helper MaterialGroupsSchema.query.log = function > (req) { - db.log(req, this); - return this; + db.log(req, this); + return this; } -export default mongoose.model>('material_groups', MaterialGroupsSchema); \ No newline at end of file +export default mongoose.model>('material_groups', MaterialGroupsSchema); diff --git a/src/models/material_suppliers.ts b/src/models/material_suppliers.ts index 5c47e3b..24cb102 100644 --- a/src/models/material_suppliers.ts +++ b/src/models/material_suppliers.ts @@ -2,13 +2,13 @@ import mongoose from 'mongoose'; import db from '../db'; const MaterialSuppliersSchema = new mongoose.Schema({ - name: {type: String, index: {unique: true}} + name: {type: String, index: {unique: true}} }); // changelog query helper MaterialSuppliersSchema.query.log = function > (req) { - db.log(req, this); - return this; + db.log(req, this); + return this; } -export default mongoose.model>('material_suppliers', MaterialSuppliersSchema); \ No newline at end of file +export default mongoose.model>('material_suppliers', MaterialSuppliersSchema); diff --git a/src/models/material_template.ts b/src/models/material_template.ts index 5e06819..3e75797 100644 --- a/src/models/material_template.ts +++ b/src/models/material_template.ts @@ -2,19 +2,19 @@ import mongoose from 'mongoose'; import db from '../db'; const MaterialTemplateSchema = new mongoose.Schema({ - first_id: mongoose.Schema.Types.ObjectId, - name: String, - version: Number, - parameters: [new mongoose.Schema({ - name: String, - range: mongoose.Schema.Types.Mixed - } ,{ _id : false })] + first_id: mongoose.Schema.Types.ObjectId, + name: String, + version: Number, + parameters: [new mongoose.Schema({ + name: String, + range: mongoose.Schema.Types.Mixed + } ,{ _id : false })] }, {minimize: false}); // to allow empty objects // changelog query helper MaterialTemplateSchema.query.log = function > (req) { - db.log(req, this); - return this; + db.log(req, this); + return this; } -export default mongoose.model>('material_template', MaterialTemplateSchema); \ No newline at end of file +export default mongoose.model>('material_template', MaterialTemplateSchema); diff --git a/src/models/measurement.ts b/src/models/measurement.ts index d83c466..55706fe 100644 --- a/src/models/measurement.ts +++ b/src/models/measurement.ts @@ -6,18 +6,18 @@ import db from '../db'; const MeasurementSchema = new mongoose.Schema({ - sample_id: {type: mongoose.Schema.Types.ObjectId, ref: SampleModel}, - values: mongoose.Schema.Types.Mixed, - measurement_template: {type: mongoose.Schema.Types.ObjectId, ref: MeasurementTemplateModel}, - status: String + sample_id: {type: mongoose.Schema.Types.ObjectId, ref: SampleModel}, + values: mongoose.Schema.Types.Mixed, + measurement_template: {type: mongoose.Schema.Types.ObjectId, ref: MeasurementTemplateModel}, + status: String }, {minimize: false}); // changelog query helper MeasurementSchema.query.log = function > (req) { - db.log(req, this); - return this; + db.log(req, this); + return this; } MeasurementSchema.index({sample_id: 1}); MeasurementSchema.index({measurement_template: 1}); -export default mongoose.model>('measurement', MeasurementSchema); \ No newline at end of file +export default mongoose.model>('measurement', MeasurementSchema); diff --git a/src/models/measurement_template.ts b/src/models/measurement_template.ts index b34e847..19228c1 100644 --- a/src/models/measurement_template.ts +++ b/src/models/measurement_template.ts @@ -2,19 +2,19 @@ import mongoose from 'mongoose'; import db from '../db'; const MeasurementTemplateSchema = new mongoose.Schema({ - first_id: mongoose.Schema.Types.ObjectId, - name: String, - version: Number, - parameters: [new mongoose.Schema({ - name: String, - range: mongoose.Schema.Types.Mixed - } ,{ _id : false })] + first_id: mongoose.Schema.Types.ObjectId, + name: String, + version: Number, + parameters: [new mongoose.Schema({ + name: String, + range: mongoose.Schema.Types.Mixed + } ,{ _id : false })] }, {minimize: false}); // to allow empty objects // changelog query helper MeasurementTemplateSchema.query.log = function > (req) { - db.log(req, this); - return this; + db.log(req, this); + return this; } -export default mongoose.model>('measurement_template', MeasurementTemplateSchema); \ No newline at end of file +export default mongoose.model>('measurement_template', MeasurementTemplateSchema); diff --git a/src/models/model.ts b/src/models/model.ts index 393b955..2059c98 100644 --- a/src/models/model.ts +++ b/src/models/model.ts @@ -2,19 +2,19 @@ import mongoose from 'mongoose'; import db from '../db'; const ModelSchema = new mongoose.Schema({ - group: {type: String, index: {unique: true}}, - models: [new mongoose.Schema({ - name: String, - url: String, - label: String - } ,{ _id : true })] + group: {type: String, index: {unique: true}}, + models: [new mongoose.Schema({ + name: String, + url: String, + label: String + } ,{ _id : true })] }); // changelog query helper ModelSchema.query.log = function > (req) { - db.log(req, this); - return this; + db.log(req, this); + return this; } ModelSchema.index({group: 1}); -export default mongoose.model>('model', ModelSchema); \ No newline at end of file +export default mongoose.model>('model', ModelSchema); diff --git a/src/models/model_file.ts b/src/models/model_file.ts index 891353a..9356b39 100644 --- a/src/models/model_file.ts +++ b/src/models/model_file.ts @@ -1,8 +1,8 @@ import mongoose from 'mongoose'; const ModelFileSchema = new mongoose.Schema({ - name: {type: String, index: {unique: true}}, - data: Buffer + name: {type: String, index: {unique: true}}, + data: Buffer }); -export default mongoose.model>('model_file', ModelFileSchema); \ No newline at end of file +export default mongoose.model>('model_file', ModelFileSchema); diff --git a/src/models/note.ts b/src/models/note.ts index 5d02502..8df74a4 100644 --- a/src/models/note.ts +++ b/src/models/note.ts @@ -2,18 +2,18 @@ import mongoose from 'mongoose'; import db from '../db'; const NoteSchema = new mongoose.Schema({ - comment: String, - sample_references: [{ - sample_id: mongoose.Schema.Types.ObjectId, - relation: String - }], - custom_fields: mongoose.Schema.Types.Mixed + comment: String, + sample_references: [{ + sample_id: mongoose.Schema.Types.ObjectId, + relation: String + }], + custom_fields: mongoose.Schema.Types.Mixed }); // changelog query helper NoteSchema.query.log = function > (req) { - db.log(req, this); - return this; + db.log(req, this); + return this; } -export default mongoose.model>('note', NoteSchema); \ No newline at end of file +export default mongoose.model>('note', NoteSchema); diff --git a/src/models/note_field.ts b/src/models/note_field.ts index 733ba02..715e924 100644 --- a/src/models/note_field.ts +++ b/src/models/note_field.ts @@ -2,14 +2,14 @@ import mongoose from 'mongoose'; import db from '../db'; const NoteFieldSchema = new mongoose.Schema({ - name: {type: String, index: {unique: true}}, - qty: Number + name: {type: String, index: {unique: true}}, + qty: Number }); // changelog query helper NoteFieldSchema.query.log = function > (req) { - db.log(req, this); - return this; + db.log(req, this); + return this; } -export default mongoose.model>('note_field', NoteFieldSchema); \ No newline at end of file +export default mongoose.model>('note_field', NoteFieldSchema); diff --git a/src/models/sample.ts b/src/models/sample.ts index fd8f53e..10df124 100644 --- a/src/models/sample.ts +++ b/src/models/sample.ts @@ -6,24 +6,24 @@ import UserModel from './user'; import db from '../db'; const SampleSchema = new mongoose.Schema({ - number: {type: String, index: {unique: true}}, - type: String, - color: String, - batch: String, - condition: mongoose.Schema.Types.Mixed, - material_id: {type: mongoose.Schema.Types.ObjectId, ref: MaterialModel}, - note_id: {type: mongoose.Schema.Types.ObjectId, ref: NoteModel}, - user_id: {type: mongoose.Schema.Types.ObjectId, ref: UserModel}, - status: String + number: {type: String, index: {unique: true}}, + type: String, + color: String, + batch: String, + condition: mongoose.Schema.Types.Mixed, + material_id: {type: mongoose.Schema.Types.ObjectId, ref: MaterialModel}, + note_id: {type: mongoose.Schema.Types.ObjectId, ref: NoteModel}, + user_id: {type: mongoose.Schema.Types.ObjectId, ref: UserModel}, + status: String }, {minimize: false}); // changelog query helper SampleSchema.query.log = function > (req) { - db.log(req, this); - return this; + db.log(req, this); + return this; } SampleSchema.index({material_id: 1}); SampleSchema.index({note_id: 1}); SampleSchema.index({user_id: 1}); -export default mongoose.model>('sample', SampleSchema); \ No newline at end of file +export default mongoose.model>('sample', SampleSchema); diff --git a/src/models/user.ts b/src/models/user.ts index b83a1e2..d546bd2 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -3,21 +3,21 @@ import db from '../db'; import ModelModel from './model'; const UserSchema = new mongoose.Schema({ - name: {type: String, index: {unique: true}}, - email: String, - pass: String, - key: String, - level: String, - location: String, - devices: [String], - models: [{type: mongoose.Schema.Types.ObjectId, ref: ModelModel}], - status: String + name: {type: String, index: {unique: true}}, + email: String, + pass: String, + key: String, + level: String, + location: String, + devices: [String], + models: [{type: mongoose.Schema.Types.ObjectId, ref: ModelModel}], + status: String }); // changelog query helper UserSchema.query.log = function > (req) { - db.log(req, this); - return this; + db.log(req, this); + return this; } -export default mongoose.model>('user', UserSchema); \ No newline at end of file +export default mongoose.model>('user', UserSchema); diff --git a/src/routes/help.spec.ts b/src/routes/help.spec.ts index 590d47b..010dfb6 100644 --- a/src/routes/help.spec.ts +++ b/src/routes/help.spec.ts @@ -4,181 +4,181 @@ import HelpModel from '../models/help'; describe('/help', () => { - let server; - before(done => TestHelper.before(done)); - beforeEach(done => server = TestHelper.beforeEach(server, done)); - afterEach(done => TestHelper.afterEach(server, done)); - after(done => TestHelper.after(done)); + let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); + after(done => TestHelper.after(done)); - describe('GET /help/{key}', () => { - it('returns the required text', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/help/%2Fsamples', - auth: {basic: 'janedoe'}, - httpStatus: 200, - res: {text: 'Samples help', level: 'read'} - }); - }); - it('returns the required text without authorization if allowed', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/help/%2Fdocumentation', - auth: {basic: 'janedoe'}, - httpStatus: 200, - res: {text: 'Documentation help', level: 'none'} - }); - }); - it('returns 404 for an invalid key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/help/documentation/database', - httpStatus: 404 - }); - }); - it('returns 404 for an unknown key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/help/xxx', - auth: {basic: 'janedoe'}, - httpStatus: 404 - }); - }); - it('returns 403 for a text with a higher level than given', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/help/%2Fmodels', - auth: {basic: 'janedoe'}, - httpStatus: 403 - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/help/%2Fsamples', - auth: {api: 'janedoe'}, - httpStatus: 401, - }); - }); - it('rejects an unauthorized request if a level is given', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/help/%2Fsamples', - httpStatus: 401 - }); - }); - }); + describe('GET /help/{key}', () => { + it('returns the required text', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/help/%2Fsamples', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {text: 'Samples help', level: 'read'} + }); + }); + it('returns the required text without authorization if allowed', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/help/%2Fdocumentation', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {text: 'Documentation help', level: 'none'} + }); + }); + it('returns 404 for an invalid key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/help/documentation/database', + httpStatus: 404 + }); + }); + it('returns 404 for an unknown key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/help/xxx', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('returns 403 for a text with a higher level than given', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/help/%2Fmodels', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/help/%2Fsamples', + auth: {api: 'janedoe'}, + httpStatus: 401, + }); + }); + it('rejects an unauthorized request if a level is given', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/help/%2Fsamples', + httpStatus: 401 + }); + }); + }); - describe('POST /help/{key}', () => { - it('changes the required text', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/help/%2Fsamples', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {text: 'New samples help', level: 'write'} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - HelpModel.find({key: '/samples'}).lean().exec((err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.only.keys('_id', 'key', 'text', 'level'); - should(data[0]).property('key', '/samples'); - should(data[0]).property('text', 'New samples help'); - should(data[0]).property('level', 'write'); - done(); - }); - }); - }); - it('saves a new text', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/help/%2Fmaterials', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {text: 'Materials help', level: 'dev'} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - HelpModel.find({key: '/materials'}).lean().exec((err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.only.keys('_id', 'key', 'text', 'level', '__v'); - should(data[0]).property('key', '/materials'); - should(data[0]).property('text', 'Materials help'); - should(data[0]).property('level', 'dev'); - done(); - }); - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/help/%2Fsamples', - auth: {key: 'admin'}, - httpStatus: 401, - req: {text: 'New samples help', level: 'write'} - }); - }); - it('rejects a write user', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/help/%2Fsamples', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {text: 'New samples help', level: 'write'} - }); - }); - it('rejects an unauthorized request', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/help/%2Fsamples', - httpStatus: 401, - req: {text: 'New samples help', level: 'write'} - }); - }); - }); + describe('POST /help/{key}', () => { + it('changes the required text', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/help/%2Fsamples', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {text: 'New samples help', level: 'write'} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + HelpModel.find({key: '/samples'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'key', 'text', 'level'); + should(data[0]).property('key', '/samples'); + should(data[0]).property('text', 'New samples help'); + should(data[0]).property('level', 'write'); + done(); + }); + }); + }); + it('saves a new text', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/help/%2Fmaterials', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {text: 'Materials help', level: 'dev'} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + HelpModel.find({key: '/materials'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'key', 'text', 'level', '__v'); + should(data[0]).property('key', '/materials'); + should(data[0]).property('text', 'Materials help'); + should(data[0]).property('level', 'dev'); + done(); + }); + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/help/%2Fsamples', + auth: {key: 'admin'}, + httpStatus: 401, + req: {text: 'New samples help', level: 'write'} + }); + }); + it('rejects a write user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/help/%2Fsamples', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {text: 'New samples help', level: 'write'} + }); + }); + it('rejects an unauthorized request', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/help/%2Fsamples', + httpStatus: 401, + req: {text: 'New samples help', level: 'write'} + }); + }); + }); - describe('DELETE /help/{key}', () => { - it('deletes the required entry', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/help/%2Fsamples', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - HelpModel.find({key: '/materials'}).lean().exec((err, data) => { - if (err) return done(err); - should(data).have.lengthOf(0); - done(); - }); - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/help/%2Fsamples', - auth: {key: 'admin'}, - httpStatus: 401 - }); - }); - it('rejects a write user', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/help/%2Fsamples', - auth: {basic: 'janedoe'}, - httpStatus: 403 - }); - }); - it('rejects an unauthorized request', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/help/%2Fsamples', - httpStatus: 401 - }); - }); - }); -}); \ No newline at end of file + describe('DELETE /help/{key}', () => { + it('deletes the required entry', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/help/%2Fsamples', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + HelpModel.find({key: '/materials'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(0); + done(); + }); + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/help/%2Fsamples', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('rejects a write user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/help/%2Fsamples', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('rejects an unauthorized request', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/help/%2Fsamples', + httpStatus: 401 + }); + }); + }); +}); diff --git a/src/routes/help.ts b/src/routes/help.ts index 3a2cb51..9c56d81 100644 --- a/src/routes/help.ts +++ b/src/routes/help.ts @@ -7,49 +7,49 @@ import globals from '../globals'; const router = express.Router(); router.get('/help/:key', (req, res, next) => { - const {error: paramError, value: key} = HelpValidate.params(req.params); - if (paramError) return res400(paramError, res); + const {error: paramError, value: key} = HelpValidate.params(req.params); + if (paramError) return res400(paramError, res); - HelpModel.findOne(key).lean().exec((err, data) => { - if (err) return next(err); + HelpModel.findOne(key).lean().exec((err, data) => { + if (err) return next(err); - if (!data) { - return res.status(404).json({status: 'Not found'}); - } - if (data.level !== 'none') { // check level - if (!req.auth(res, - Object.values(globals.levels).slice(Object.values(globals.levels).findIndex(e => e === data.level)) - , 'basic')) return; - } - res.json(HelpValidate.output(data)); - }) + if (!data) { + return res.status(404).json({status: 'Not found'}); + } + if (data.level !== 'none') { // check level + if (!req.auth(res, + Object.values(globals.levels).slice(Object.values(globals.levels).findIndex(e => e === data.level)) + , 'basic')) return; + } + res.json(HelpValidate.output(data)); + }) }); router.post('/help/:key', (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - const {error: paramError, value: key} = HelpValidate.params(req.params); - if (paramError) return res400(paramError, res); - const {error, value: help} = HelpValidate.input(req.body); - if (error) return res400(error, res); + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + const {error: paramError, value: key} = HelpValidate.params(req.params); + if (paramError) return res400(paramError, res); + const {error, value: help} = HelpValidate.input(req.body); + if (error) return res400(error, res); - HelpModel.findOneAndUpdate(key, help, {upsert: true}).log(req).lean().exec(err => { - if (err) return next(err); - res.json({status: 'OK'}); - }); + HelpModel.findOneAndUpdate(key, help, {upsert: true}).log(req).lean().exec(err => { + if (err) return next(err); + res.json({status: 'OK'}); + }); }); router.delete('/help/:key', (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - const {error: paramError, value: key} = HelpValidate.params(req.params); - if (paramError) return res400(paramError, res); + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + const {error: paramError, value: key} = HelpValidate.params(req.params); + if (paramError) return res400(paramError, res); - HelpModel.findOneAndDelete(key).log(req).lean().exec((err, data) => { - if (err) return next(err); - if (!data) { - return res.status(404).json({status: 'Not found'}); - } - res.json({status: 'OK'}); - }); + HelpModel.findOneAndDelete(key).log(req).lean().exec((err, data) => { + if (err) return next(err); + if (!data) { + return res.status(404).json({status: 'Not found'}); + } + res.json({status: 'OK'}); + }); }); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/src/routes/material.spec.ts b/src/routes/material.spec.ts index 657cb7c..7aca4a3 100644 --- a/src/routes/material.spec.ts +++ b/src/routes/material.spec.ts @@ -6,1115 +6,1115 @@ import TestHelper from "../test/helper"; describe('/material', () => { - let server; - before(done => TestHelper.before(done)); - beforeEach(done => server = TestHelper.beforeEach(server, done)); - afterEach(done => TestHelper.afterEach(server, done)); - after(done => TestHelper.after(done)); + let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); + after(done => TestHelper.after(done)); - describe('GET /materials', () => { - it('returns all materials', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/materials', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === 'validated').length); - should(res.body).matchEach(material => { - should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers', 'status'); - 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('status', 'validated'); - should(material.properties).have.property('material_template').be.type('string'); - should(material.numbers).be.instanceof(Array); - }); - done(); - }); - }); - it('works with an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/materials', - auth: {key: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === 'validated').length); - should(res.body).matchEach(material => { - should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers', 'status'); - 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('status', 'validated'); - should(material.properties).have.property('material_template').be.type('string'); - should(material.numbers).be.instanceof(Array); - }); - done(); - }); - }); - it('allows filtering by state', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/materials?status[]=new', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === 'new').length); - should(res.body).matchEach(material => { - should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers', 'status'); - 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('status', 'new'); - should(material.properties).have.property('material_template').be.type('string'); - should(material.numbers).be.instanceof(Array); - }); - done(); - }); - }); - it('allows filtering by deleted state for admins', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/materials?status[]=deleted', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === 'deleted').length); - should(res.body).matchEach(material => { - should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers', 'status'); - 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('status', 'deleted'); - should(material.properties).have.property('material_template').be.type('string'); - should(material.numbers).be.instanceof(Array); - }); - done(); - }); - }); - it('rejects filtering by deleted state for a write user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/materials?status[]=deleted', - auth: {basic: 'janedoe'}, - httpStatus: 400, - res: {status: 'Invalid body format', details: '"status[0]" must be one of [validated, new]'} - }); - }); - it('rejects an invalid state name', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/materials?status[]=xxx', - auth: {basic: 'janedoe'}, - httpStatus: 400, - res: {status: 'Invalid body format', details: '"status[0]" must be one of [validated, new]'} - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/materials', - httpStatus: 401 - }); - }); - }); + describe('GET /materials', () => { + it('returns all materials', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/materials', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === 'validated').length); + should(res.body).matchEach(material => { + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers', 'status'); + 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('status', 'validated'); + should(material.properties).have.property('material_template').be.type('string'); + should(material.numbers).be.instanceof(Array); + }); + done(); + }); + }); + it('works with an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/materials', + auth: {key: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === 'validated').length); + should(res.body).matchEach(material => { + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers', 'status'); + 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('status', 'validated'); + should(material.properties).have.property('material_template').be.type('string'); + should(material.numbers).be.instanceof(Array); + }); + done(); + }); + }); + it('allows filtering by state', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/materials?status[]=new', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === 'new').length); + should(res.body).matchEach(material => { + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers', 'status'); + 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('status', 'new'); + should(material.properties).have.property('material_template').be.type('string'); + should(material.numbers).be.instanceof(Array); + }); + done(); + }); + }); + it('allows filtering by deleted state for admins', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/materials?status[]=deleted', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === 'deleted').length); + should(res.body).matchEach(material => { + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers', 'status'); + 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('status', 'deleted'); + should(material.properties).have.property('material_template').be.type('string'); + should(material.numbers).be.instanceof(Array); + }); + done(); + }); + }); + it('rejects filtering by deleted state for a write user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/materials?status[]=deleted', + auth: {basic: 'janedoe'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: '"status[0]" must be one of [validated, new]'} + }); + }); + it('rejects an invalid state name', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/materials?status[]=xxx', + auth: {basic: 'janedoe'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: '"status[0]" must be one of [validated, new]'} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/materials', + httpStatus: 401 + }); + }); + }); - describe('GET /materials/{state}', () => { - 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 ==='new').length); - should(res.body).matchEach(material => { - should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', '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.properties).have.property('material_template').be.type('string'); - should(material.numbers).be.instanceof(Array); - MaterialModel.findById(material._id).lean().exec((err, data) => { - should(data).have.property('status','new'); - 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 ==='deleted').length); - should(res.body).matchEach(material => { - should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', '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.properties).have.property('material_template').be.type('string'); - should(material.numbers).be.instanceof(Array); - MaterialModel.findById(material._id).lean().exec((err, data) => { - should(data).have.property('status','deleted'); - if (--asyncCounter === 0) { - 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 /materials/{state}', () => { + 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 ==='new').length); + should(res.body).matchEach(material => { + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', '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.properties).have.property('material_template').be.type('string'); + should(material.numbers).be.instanceof(Array); + MaterialModel.findById(material._id).lean().exec((err, data) => { + should(data).have.property('status','new'); + 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 ==='deleted').length); + should(res.body).matchEach(material => { + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', '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.properties).have.property('material_template').be.type('string'); + should(material.numbers).be.instanceof(Array); + MaterialModel.findById(material._id).lean().exec((err, data) => { + should(data).have.property('status','deleted'); + if (--asyncCounter === 0) { + 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, { - method: 'get', - url: '/material/100000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423', '5514263422']} - }); - }); - it('returns the right material for an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/material/100000000000000000000003', - auth: {key: 'admin'}, - httpStatus: 200, - res: {_id: '100000000000000000000003', name: 'PA GF 50 black (2706)', supplier: 'Akro-Plastic', group: 'PA66+PA6I/6T', properties: {material_template: '130000000000000000000003', 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', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 0, carbon_fiber: 0}, numbers: []} - }); - }); - it('returns a deleted material for a dev/admin user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/material/100000000000000000000008', - auth: {basic: 'admin'}, - httpStatus: 200, - res: {_id: '100000000000000000000008', name: 'Latamid 66 H 2 G 30', supplier: 'LATI', group: 'PA66', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: ['5513943509']} - }); - }); - it('returns 403 for a write user when requesting a deleted material', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/material/100000000000000000000008', - auth: {basic: 'janedoe'}, - httpStatus: 403 - }); - }); - it('rejects an invalid id', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/material/10000000000000000000000x', - auth: {key: 'admin'}, - httpStatus: 404 - }); - }); - it('rejects an unknown id', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/material/100000000000000000000111', - auth: {key: 'janedoe'}, - httpStatus: 404 - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/material/100000000000000000000001', - httpStatus: 401 - }); - }); - }); + describe('GET /material/{id}', () => { + it('returns the right material', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423', '5514263422']} + }); + }); + it('returns the right material for an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/100000000000000000000003', + auth: {key: 'admin'}, + httpStatus: 200, + res: {_id: '100000000000000000000003', name: 'PA GF 50 black (2706)', supplier: 'Akro-Plastic', group: 'PA66+PA6I/6T', properties: {material_template: '130000000000000000000003', 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', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 0, carbon_fiber: 0}, numbers: []} + }); + }); + it('returns a deleted material for a dev/admin user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/100000000000000000000008', + auth: {basic: 'admin'}, + httpStatus: 200, + res: {_id: '100000000000000000000008', name: 'Latamid 66 H 2 G 30', supplier: 'LATI', group: 'PA66', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: ['5513943509']} + }); + }); + it('returns 403 for a write user when requesting a deleted material', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/100000000000000000000008', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/10000000000000000000000x', + auth: {key: 'admin'}, + httpStatus: 404 + }); + }); + it('rejects an unknown id', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/100000000000000000000111', + auth: {key: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/100000000000000000000001', + httpStatus: 401 + }); + }); + }); - describe('PUT /material/{id}', () => { - it('returns the right material', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {}, - res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423', '5514263422']} - }); - }); - it('keeps unchanged properties', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423', '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', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423', '5514263422']}); - MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { - if (err) return done(err); - should(data).have.property('status','validated'); - MaterialGroupModel.find({name: 'PA46'}).lean().exec((err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]._id.toString()).be.eql('900000000000000000000001'); - MaterialSupplierModel.find({name: 'DSM'}).lean().exec((err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]._id.toString()).be.eql('110000000000000000000001'); - done(); - }); - }); - }); - }); - }); - it('keeps only one unchanged property', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - 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', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423', '5514263422']}); - MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { - if (err) return done(err); - should(data).have.property('status','validated'); - done(); - }); - }); - }); - it('keeps unchanged properties', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423', '5514263422']}); - MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { - if (err) return done(err); - should(data).have.property('status','validated'); - done(); - }); - }); - }); - it('changes the given properties', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 35, carbon_fiber: 0}, numbers: ['5514212901', '5514612901']} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 35, carbon_fiber: 0}, numbers: ['5514212901', '5514612901']}); - MaterialModel.findById('100000000000000000000001').lean().exec((err, data:any) => { - if (err) return done(err); - data._id = data._id.toString(); - data.group_id = data.group_id.toString(); - data.supplier_id = data.supplier_id.toString(); - should(data).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier_id: '110000000000000000000002', group_id: '900000000000000000000002', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 35, carbon_fiber: 0}, numbers: ['5514212901', '5514612901'], status: 'new', __v: 0}); - MaterialGroupModel.find({name: 'PA6/6T'}).lean().exec((err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]._id.toString()).be.eql('900000000000000000000002'); - MaterialSupplierModel.find({name: 'BASF'}).lean().exec((err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]._id.toString()).be.eql('110000000000000000000002'); - done(); - }); - }); - }); - }); - }); - it('creates a changelog', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 35, carbon_fiber: 0}, numbers: ['5514212901', '5514612901']}, - log: { - collection: 'materials', - dataAdd: { - group_id: '900000000000000000000002', - supplier_id: '110000000000000000000002', - status: 'new' - }, - dataIgn: ['supplier', 'group'] - } - }); - }); - it('rejects already existing material names', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {name: 'Ultramid T KR 4355 G7'}, - res: {status: 'Material name already taken'} - }); - }); - it('rejects wrong material properties', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {properties: {material_template: '130000000000000000000003', mineral: 'x', glass_fiber: 0, carbon_fiber: 0}}, - res: {status: 'Invalid body format', details: '"mineral" must be a number'} - }); - }); - it('rejects an invalid id', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/10000000000000000000000x', - auth: {basic: 'admin'}, - httpStatus: 404, - req: {}, - }); - }); - it('rejects not specified properties parameters', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 0, carbon_fiber: 0, x: 55}}, - res: {status: 'Invalid body format', details: '"x" is not allowed'} - }); - }); - it('rejects a properties parameter not in the value range', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000009', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {properties: {material_template: '130000000000000000000002', stickiness: 'xx'}}, - res: {status: 'Invalid body format', details: '"stickiness" must be one of [not so sticky, medium, very sticky]'} - }); - }); - it('rejects a properties parameter below minimum range', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: -5, carbon_fiber: 0}}, - res: {status: 'Invalid body format', details: '"glass_fiber" must be greater than or equal to 0'} - }); - }); - it('rejects a properties parameter above maximum range', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 0, carbon_fiber: 105}}, - res: {status: 'Invalid body format', details: '"carbon_fiber" must be less than or equal to 100'} - }); - }); - it('rejects an invalid material template', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {properties: {material_template: '1300000000000h0000000001', mineral: 0, glass_fiber: 0, carbon_fiber: 0}}, - res: {status: 'Material template not available'} - }); - }); - it('rejects an unknown material template', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {properties: {material_template: '100000000000000000000001', mineral: 0, glass_fiber: 0, carbon_fiber: 0}}, - res: {status: 'Material template not available'} - }); - }); - it('rejects an old version of a material template', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {properties: {material_template: '130000000000000000000001', glass_fiber: 0}}, - res: {status: 'Old template version not allowed'} - }); - }); - it('allows keeping an old version of a material template', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000010', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {properties: {material_template: '130000000000000000000001', glass_fiber: 5}}, - res: {_id: '100000000000000000000010', name: 'Latamid 66 G 40', numbers: ['5513943509'], supplier: 'LATI', group: 'PA66', properties: {material_template: '130000000000000000000001', glass_fiber: 5}} - }); - }); - it('rejects editing a deleted material', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000008', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {} - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000002', - auth: {key: 'admin'}, - httpStatus: 401, - req: {} - }); - }); - it('rejects requests from a read user', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000002', - auth: {basic: 'user'}, - httpStatus: 403, - req: {} - }); - }); - it('returns 404 for an unknown material', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000111', - auth: {basic: 'janedoe'}, - httpStatus: 404, - req: {} - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000001', - httpStatus: 401, - req: {} - }); - }); - }); + describe('PUT /material/{id}', () => { + it('returns the right material', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {}, + res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423', '5514263422']} + }); + }); + it('keeps unchanged properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423', '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', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423', '5514263422']}); + MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { + if (err) return done(err); + should(data).have.property('status','validated'); + MaterialGroupModel.find({name: 'PA46'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]._id.toString()).be.eql('900000000000000000000001'); + MaterialSupplierModel.find({name: 'DSM'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]._id.toString()).be.eql('110000000000000000000001'); + done(); + }); + }); + }); + }); + }); + it('keeps only one unchanged property', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + 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', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423', '5514263422']}); + MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { + if (err) return done(err); + should(data).have.property('status','validated'); + done(); + }); + }); + }); + it('keeps unchanged properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423', '5514263422']}); + MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { + if (err) return done(err); + should(data).have.property('status','validated'); + done(); + }); + }); + }); + it('changes the given properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 35, carbon_fiber: 0}, numbers: ['5514212901', '5514612901']} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 35, carbon_fiber: 0}, numbers: ['5514212901', '5514612901']}); + MaterialModel.findById('100000000000000000000001').lean().exec((err, data:any) => { + if (err) return done(err); + data._id = data._id.toString(); + data.group_id = data.group_id.toString(); + data.supplier_id = data.supplier_id.toString(); + should(data).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier_id: '110000000000000000000002', group_id: '900000000000000000000002', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 35, carbon_fiber: 0}, numbers: ['5514212901', '5514612901'], status: 'new', __v: 0}); + MaterialGroupModel.find({name: 'PA6/6T'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]._id.toString()).be.eql('900000000000000000000002'); + MaterialSupplierModel.find({name: 'BASF'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]._id.toString()).be.eql('110000000000000000000002'); + done(); + }); + }); + }); + }); + }); + it('creates a changelog', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 35, carbon_fiber: 0}, numbers: ['5514212901', '5514612901']}, + log: { + collection: 'materials', + dataAdd: { + group_id: '900000000000000000000002', + supplier_id: '110000000000000000000002', + status: 'new' + }, + dataIgn: ['supplier', 'group'] + } + }); + }); + it('rejects already existing material names', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Ultramid T KR 4355 G7'}, + res: {status: 'Material name already taken'} + }); + }); + it('rejects wrong material properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {properties: {material_template: '130000000000000000000003', mineral: 'x', glass_fiber: 0, carbon_fiber: 0}}, + res: {status: 'Invalid body format', details: '"mineral" must be a number'} + }); + }); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/10000000000000000000000x', + auth: {basic: 'admin'}, + httpStatus: 404, + req: {}, + }); + }); + it('rejects not specified properties parameters', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 0, carbon_fiber: 0, x: 55}}, + res: {status: 'Invalid body format', details: '"x" is not allowed'} + }); + }); + it('rejects a properties parameter not in the value range', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000009', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {properties: {material_template: '130000000000000000000002', stickiness: 'xx'}}, + res: {status: 'Invalid body format', details: '"stickiness" must be one of [not so sticky, medium, very sticky]'} + }); + }); + it('rejects a properties parameter below minimum range', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: -5, carbon_fiber: 0}}, + res: {status: 'Invalid body format', details: '"glass_fiber" must be greater than or equal to 0'} + }); + }); + it('rejects a properties parameter above maximum range', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 0, carbon_fiber: 105}}, + res: {status: 'Invalid body format', details: '"carbon_fiber" must be less than or equal to 100'} + }); + }); + it('rejects an invalid material template', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {properties: {material_template: '1300000000000h0000000001', mineral: 0, glass_fiber: 0, carbon_fiber: 0}}, + res: {status: 'Material template not available'} + }); + }); + it('rejects an unknown material template', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {properties: {material_template: '100000000000000000000001', mineral: 0, glass_fiber: 0, carbon_fiber: 0}}, + res: {status: 'Material template not available'} + }); + }); + it('rejects an old version of a material template', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {properties: {material_template: '130000000000000000000001', glass_fiber: 0}}, + res: {status: 'Old template version not allowed'} + }); + }); + it('allows keeping an old version of a material template', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000010', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {properties: {material_template: '130000000000000000000001', glass_fiber: 5}}, + res: {_id: '100000000000000000000010', name: 'Latamid 66 G 40', numbers: ['5513943509'], supplier: 'LATI', group: 'PA66', properties: {material_template: '130000000000000000000001', glass_fiber: 5}} + }); + }); + it('rejects editing a deleted material', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000008', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000002', + auth: {key: 'admin'}, + httpStatus: 401, + req: {} + }); + }); + it('rejects requests from a read user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000002', + auth: {basic: 'user'}, + httpStatus: 403, + req: {} + }); + }); + it('returns 404 for an unknown material', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000111', + auth: {basic: 'janedoe'}, + httpStatus: 404, + req: {} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + httpStatus: 401, + req: {} + }); + }); + }); - describe('DELETE /material/{id}', () => { - it('sets the status to deleted', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/material/100000000000000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - MaterialModel.findById('100000000000000000000002').lean().exec((err, data: any) => { - if (err) return done(err); - data._id = data._id.toString(); - data.group_id = data.group_id.toString(); - data.supplier_id = data.supplier_id.toString(); - data.properties.material_template = data.properties.material_template.toString(); - should(data).be.eql({_id: '100000000000000000000002', name: 'Ultramid T KR 4355 G7', supplier_id: '110000000000000000000002', group_id: '900000000000000000000002', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 35, carbon_fiber: 0}, numbers: ['5514212901', '5514612901'], status: 'deleted', __v: 0} - ); - done(); - }); - }); - }); - it('creates a changelog', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/material/100000000000000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 200, - log: { - collection: 'materials', - dataAdd: { status: 'deleted'} - } - }); - }); - it('rejects deleting a material referenced by samples', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/material/100000000000000000000004', - auth: {basic: 'janedoe'}, - httpStatus: 400, - res: {status: 'Material still in use'} - }) - }); - it('rejects an invalid id', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/material/10000000000000000000000x', - auth: {basic: 'admin'}, - httpStatus: 404 - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/material/100000000000000000000002', - auth: {key: 'admin'}, - httpStatus: 401 - }); - }); - it('rejects requests from a read user', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/material/100000000000000000000002', - auth: {basic: 'user'}, - httpStatus: 403 - }); - }); - it('returns 404 for an unknown id', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/material/100000000000000000000111', - auth: {basic: 'janedoe'}, - httpStatus: 404 - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/material/100000000000000000000001', - httpStatus: 401 - }); - }); - }); + describe('DELETE /material/{id}', () => { + it('sets the status to deleted', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/100000000000000000000002', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + MaterialModel.findById('100000000000000000000002').lean().exec((err, data: any) => { + if (err) return done(err); + data._id = data._id.toString(); + data.group_id = data.group_id.toString(); + data.supplier_id = data.supplier_id.toString(); + data.properties.material_template = data.properties.material_template.toString(); + should(data).be.eql({_id: '100000000000000000000002', name: 'Ultramid T KR 4355 G7', supplier_id: '110000000000000000000002', group_id: '900000000000000000000002', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 35, carbon_fiber: 0}, numbers: ['5514212901', '5514612901'], status: 'deleted', __v: 0} + ); + done(); + }); + }); + }); + it('creates a changelog', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/100000000000000000000002', + auth: {basic: 'janedoe'}, + httpStatus: 200, + log: { + collection: 'materials', + dataAdd: { status: 'deleted'} + } + }); + }); + it('rejects deleting a material referenced by samples', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/100000000000000000000004', + auth: {basic: 'janedoe'}, + httpStatus: 400, + res: {status: 'Material still in use'} + }) + }); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/10000000000000000000000x', + auth: {basic: 'admin'}, + httpStatus: 404 + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/100000000000000000000002', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('rejects requests from a read user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/100000000000000000000002', + auth: {basic: 'user'}, + httpStatus: 403 + }); + }); + it('returns 404 for an unknown id', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/100000000000000000000111', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/material/100000000000000000000001', + httpStatus: 401 + }); + }); + }); - describe('PUT /material/restore/{id}', () => { - it('sets the status', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/restore/100000000000000000000008', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {} - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'OK'}); - MaterialModel.findById('100000000000000000000008').lean().exec((err, data: any) => { - if (err) return done(err); - should(data).have.property('status','new'); - done(); - }); - }); - }); - it('creates a changelog', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/restore/100000000000000000000008', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {}, - log: { - collection: 'materials', - dataAdd: { - status: 'new' - } - } - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/restore/100000000000000000000008', - auth: {key: 'admin'}, - httpStatus: 401, - req: {} - }); - }); - it('rejects a write user', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/restore/100000000000000000000008', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {} - }); - }); - it('returns 404 for an unknown sample', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/restore/000000000000000000000008', - auth: {basic: 'admin'}, - httpStatus: 404, - req: {} - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/restore/100000000000000000000008', - httpStatus: 401, - req: {} - }); - }); - }); + describe('PUT /material/restore/{id}', () => { + it('sets the status', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/restore/100000000000000000000008', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + MaterialModel.findById('100000000000000000000008').lean().exec((err, data: any) => { + if (err) return done(err); + should(data).have.property('status','new'); + done(); + }); + }); + }); + it('creates a changelog', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/restore/100000000000000000000008', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {}, + log: { + collection: 'materials', + dataAdd: { + status: 'new' + } + } + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/restore/100000000000000000000008', + auth: {key: 'admin'}, + httpStatus: 401, + req: {} + }); + }); + it('rejects a write user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/restore/100000000000000000000008', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {} + }); + }); + it('returns 404 for an unknown sample', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/restore/000000000000000000000008', + auth: {basic: 'admin'}, + httpStatus: 404, + req: {} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/restore/100000000000000000000008', + httpStatus: 401, + req: {} + }); + }); + }); - describe('PUT /material/validate/{id}', () => { - it('sets the status', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/validate/100000000000000000000007', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {} - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'OK'}); - MaterialModel.findById('100000000000000000000007').lean().exec((err, data: any) => { - if (err) return done(err); - should(data).have.property('status','validated'); - done(); - }); - }); - }); - it('creates a changelog', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/validate/100000000000000000000007', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {}, - log: { - collection: 'materials', - dataAdd: { - status: 'validated' - } - } - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/validate/100000000000000000000007', - auth: {key: 'admin'}, - httpStatus: 401, - req: {} - }); - }); - it('rejects a write user', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/validate/100000000000000000000007', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {} - }); - }); - it('returns 404 for an unknown sample', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/validate/000000000000000000000007', - auth: {basic: 'admin'}, - httpStatus: 404, - req: {} - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/validate/100000000000000000000007', - httpStatus: 401, - req: {} - }); - }); - }); + describe('PUT /material/validate/{id}', () => { + it('sets the status', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/validate/100000000000000000000007', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + MaterialModel.findById('100000000000000000000007').lean().exec((err, data: any) => { + if (err) return done(err); + should(data).have.property('status','validated'); + done(); + }); + }); + }); + it('creates a changelog', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/validate/100000000000000000000007', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {}, + log: { + collection: 'materials', + dataAdd: { + status: 'validated' + } + } + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/validate/100000000000000000000007', + auth: {key: 'admin'}, + httpStatus: 401, + req: {} + }); + }); + it('rejects a write user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/validate/100000000000000000000007', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {} + }); + }); + it('returns 404 for an unknown sample', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/validate/000000000000000000000007', + auth: {basic: 'admin'}, + httpStatus: 404, + req: {} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/validate/100000000000000000000007', + httpStatus: 401, + req: {} + }); + }); + }); - describe('POST /material/new', () => { - it('returns the right material', 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', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: ['5515798402']} - }).end((err, res) => { - if (err) return done (err); - should(res.body).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', '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.properties).have.property('material_template', '130000000000000000000003'); - should(res.body.properties).have.property('mineral', 0); - should(res.body.properties).have.property('glass_fiber', 30); - should(res.body.properties).have.property('carbon_fiber', 0); - should(res.body).have.property('numbers', ['5515798402']); - done(); - }); - }); - it('stores the material', 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', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: []} - }).end(err => { - if (err) return done (err); - MaterialModel.find({name: 'Crastin CE 2510'}).lean().exec((err, materialData: any) => { - if (err) return done (err); - should(materialData).have.lengthOf(1); - should(materialData[0]).have.only.keys('_id', 'name', 'supplier_id', 'group_id', 'properties', 'numbers', 'status', '__v'); - should(materialData[0]).have.property('name', 'Crastin CE 2510'); - should(materialData[0].properties).have.property('material_template', '130000000000000000000003'); - should(materialData[0].properties).have.property('mineral', 0); - should(materialData[0].properties).have.property('glass_fiber', 30); - should(materialData[0].properties).have.property('carbon_fiber', 0); - should(materialData[0]).have.property('status','new'); - should(materialData[0].numbers).have.lengthOf(0); - MaterialGroupModel.findById(materialData[0].group_id).lean().exec((err, data) => { - if (err) return done(err); - should(data).have.property('name', 'PBT') - MaterialSupplierModel.findById(materialData[0].supplier_id).lean().exec((err, data) => { - if (err) return done(err); - should(data).have.property('name', 'Du Pont'); - done(); - }); - }); - }); - }); - }); - it('creates a changelog', 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', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: []}, - log: { - collection: 'materials', - dataAdd: {status: 'new'}, - dataIgn: ['group_id', 'supplier_id', 'group', 'supplier'] - } - }); - }); - 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', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423']}, - res: {status: 'Material name already taken'} - }); - }); - it('rejects a missing name', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/material/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: ['5515798402']}, - res: {status: 'Invalid body format', details: '"name" is required'} - }); - }); - it('rejects a missing supplier', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/material/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {name: 'Crastin CE 2510', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: ['5515798402']}, - res: {status: 'Invalid body format', details: '"supplier" is required'} - }); - }); - it('rejects a missing group', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/material/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: ['5515798402']}, - res: {status: 'Invalid body format', details: '"group" is required'} - }); - }); - it('rejects a missing mineral property', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/material/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', glass_fiber: 30, carbon_fiber: 0}, numbers: ['5515798402']}, - res: {status: 'Invalid body format', details: '"mineral" is required'} - }); - }); - it('rejects a missing glass_fiber property', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/material/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, carbon_fiber: 0}, numbers: ['5515798402']}, - res: {status: 'Invalid body format', details: '"glass_fiber" is required'} - }); - }); - it('rejects a missing carbon_fiber property', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/material/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30}, numbers: ['5515798402']}, - res: {status: 'Invalid body format', details: '"carbon_fiber" is required'} - }); - }); - it('rejects a missing numbers array', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/material/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}}, - res: {status: 'Invalid body format', details: '"numbers" is required'} - }); - }); - it('rejects not specified properties parameters', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/material/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, carbon_fiber: 0, glass_fiber: 30, x: 47}, numbers: ['5515798402']}, - res: {status: 'Invalid body format', details: '"x" is not allowed'} - }); - }); - it('rejects a properties parameter not in the value range', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/material/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {name: 'Glue2', supplier: 'BASF', group: 'Glue', properties: {material_template: '130000000000000000000002', stickiness: 'not so much'}, numbers: []}, - res: {status: 'Invalid body format', details: '"stickiness" must be one of [not so sticky, medium, very sticky]'} - }); - }); - it('rejects a properties parameter below minimum range', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/material/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', glass_fiber: -0.3}, numbers: ['5515798402']}, - res: {status: 'Invalid body format', details: '"glass_fiber" must be greater than or equal to 0'} - }); - }); - it('rejects a properties parameter above maximum range', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/material/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', glass_fiber: 100.001}, numbers: ['5515798402']}, - res: {status: 'Invalid body format', details: '"glass_fiber" must be less than or equal to 100'} - }); - }); - it('rejects an invalid material template', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/material/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000h00000000000003', glass_fiber: 30}, numbers: ['5515798402']}, - res: {status: 'Material template not available'} - }); - }); - it('rejects an unknown material template', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/material/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '100000000000000000000003', glass_fiber: 30}, numbers: ['5515798402']}, - res: {status: 'Material template not available'} - }); - }); - it('rejects an old version of a material template', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/material/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000001', glass_fiber: 30}, numbers: ['5515798402']}, - res: {status: 'Old template version not allowed'} - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/material/new', - auth: {key: 'janedoe'}, - httpStatus: 401, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: []} - }); - }); - it('rejects requests from a read user', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/material/new', - auth: {basic: 'user'}, - httpStatus: 403, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: []} - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/material/new', - httpStatus: 401, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: []} - }); - }); - }); + describe('POST /material/new', () => { + it('returns the right material', 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', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: ['5515798402']} + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', '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.properties).have.property('material_template', '130000000000000000000003'); + should(res.body.properties).have.property('mineral', 0); + should(res.body.properties).have.property('glass_fiber', 30); + should(res.body.properties).have.property('carbon_fiber', 0); + should(res.body).have.property('numbers', ['5515798402']); + done(); + }); + }); + it('stores the material', 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', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: []} + }).end(err => { + if (err) return done (err); + MaterialModel.find({name: 'Crastin CE 2510'}).lean().exec((err, materialData: any) => { + if (err) return done (err); + should(materialData).have.lengthOf(1); + should(materialData[0]).have.only.keys('_id', 'name', 'supplier_id', 'group_id', 'properties', 'numbers', 'status', '__v'); + should(materialData[0]).have.property('name', 'Crastin CE 2510'); + should(materialData[0].properties).have.property('material_template', '130000000000000000000003'); + should(materialData[0].properties).have.property('mineral', 0); + should(materialData[0].properties).have.property('glass_fiber', 30); + should(materialData[0].properties).have.property('carbon_fiber', 0); + should(materialData[0]).have.property('status','new'); + should(materialData[0].numbers).have.lengthOf(0); + MaterialGroupModel.findById(materialData[0].group_id).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.property('name', 'PBT') + MaterialSupplierModel.findById(materialData[0].supplier_id).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.property('name', 'Du Pont'); + done(); + }); + }); + }); + }); + }); + it('creates a changelog', 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', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: []}, + log: { + collection: 'materials', + dataAdd: {status: 'new'}, + dataIgn: ['group_id', 'supplier_id', 'group', 'supplier'] + } + }); + }); + 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', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423']}, + res: {status: 'Material name already taken'} + }); + }); + it('rejects a missing name', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: ['5515798402']}, + res: {status: 'Invalid body format', details: '"name" is required'} + }); + }); + it('rejects a missing supplier', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: ['5515798402']}, + res: {status: 'Invalid body format', details: '"supplier" is required'} + }); + }); + it('rejects a missing group', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: ['5515798402']}, + res: {status: 'Invalid body format', details: '"group" is required'} + }); + }); + it('rejects a missing mineral property', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', glass_fiber: 30, carbon_fiber: 0}, numbers: ['5515798402']}, + res: {status: 'Invalid body format', details: '"mineral" is required'} + }); + }); + it('rejects a missing glass_fiber property', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, carbon_fiber: 0}, numbers: ['5515798402']}, + res: {status: 'Invalid body format', details: '"glass_fiber" is required'} + }); + }); + it('rejects a missing carbon_fiber property', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30}, numbers: ['5515798402']}, + res: {status: 'Invalid body format', details: '"carbon_fiber" is required'} + }); + }); + it('rejects a missing numbers array', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}}, + res: {status: 'Invalid body format', details: '"numbers" is required'} + }); + }); + it('rejects not specified properties parameters', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, carbon_fiber: 0, glass_fiber: 30, x: 47}, numbers: ['5515798402']}, + res: {status: 'Invalid body format', details: '"x" is not allowed'} + }); + }); + it('rejects a properties parameter not in the value range', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Glue2', supplier: 'BASF', group: 'Glue', properties: {material_template: '130000000000000000000002', stickiness: 'not so much'}, numbers: []}, + res: {status: 'Invalid body format', details: '"stickiness" must be one of [not so sticky, medium, very sticky]'} + }); + }); + it('rejects a properties parameter below minimum range', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', glass_fiber: -0.3}, numbers: ['5515798402']}, + res: {status: 'Invalid body format', details: '"glass_fiber" must be greater than or equal to 0'} + }); + }); + it('rejects a properties parameter above maximum range', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', glass_fiber: 100.001}, numbers: ['5515798402']}, + res: {status: 'Invalid body format', details: '"glass_fiber" must be less than or equal to 100'} + }); + }); + it('rejects an invalid material template', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000h00000000000003', glass_fiber: 30}, numbers: ['5515798402']}, + res: {status: 'Material template not available'} + }); + }); + it('rejects an unknown material template', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '100000000000000000000003', glass_fiber: 30}, numbers: ['5515798402']}, + res: {status: 'Material template not available'} + }); + }); + it('rejects an old version of a material template', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000001', glass_fiber: 30}, numbers: ['5515798402']}, + res: {status: 'Old template version not allowed'} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {key: 'janedoe'}, + httpStatus: 401, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: []} + }); + }); + it('rejects requests from a read user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'user'}, + httpStatus: 403, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: []} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + httpStatus: 401, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: []} + }); + }); + }); - describe('GET /material/groups', () => { - it('returns all groups', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/material/groups', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.material_groups.length); - should(res.body[0]).be.eql(json.collections.material_groups[0].name); - should(res.body).matchEach(group => { - should(group).be.type('string'); - }); - done(); - }); - }); - it('works with an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/material/groups', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.material_groups.length); - should(res.body[0]).be.eql(json.collections.material_groups[0].name); - should(res.body).matchEach(group => { - should(group).be.type('string'); - }); - done(); - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/material/groups', - httpStatus: 401 - }); - }); - }); + describe('GET /material/groups', () => { + it('returns all groups', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/groups', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.material_groups.length); + should(res.body[0]).be.eql(json.collections.material_groups[0].name); + should(res.body).matchEach(group => { + should(group).be.type('string'); + }); + done(); + }); + }); + it('works with an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/groups', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.material_groups.length); + should(res.body[0]).be.eql(json.collections.material_groups[0].name); + should(res.body).matchEach(group => { + should(group).be.type('string'); + }); + done(); + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/groups', + httpStatus: 401 + }); + }); + }); - describe('GET /material/suppliers', () => { - it('returns all suppliers', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/material/suppliers', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.material_suppliers.length); - should(res.body[0]).be.eql(json.collections.material_suppliers[0].name); - should(res.body).matchEach(supplier => { - should(supplier).be.type('string'); - }); - done(); - }); - }); - it('works with an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/material/suppliers', - auth: {key: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.material_suppliers.length); - should(res.body[0]).be.eql(json.collections.material_suppliers[0].name); - should(res.body).matchEach(supplier => { - should(supplier).be.type('string'); - }); - done(); - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/material/suppliers', - httpStatus: 401 - }); - }); - }); -}); \ No newline at end of file + describe('GET /material/suppliers', () => { + it('returns all suppliers', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/suppliers', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.material_suppliers.length); + should(res.body[0]).be.eql(json.collections.material_suppliers[0].name); + should(res.body).matchEach(supplier => { + should(supplier).be.type('string'); + }); + done(); + }); + }); + it('works with an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/suppliers', + auth: {key: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.material_suppliers.length); + should(res.body[0]).be.eql(json.collections.material_suppliers[0].name); + should(res.body).matchEach(supplier => { + should(supplier).be.type('string'); + }); + done(); + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/material/suppliers', + httpStatus: 401 + }); + }); + }); +}); diff --git a/src/routes/material.ts b/src/routes/material.ts index 3996e30..de97245 100644 --- a/src/routes/material.ts +++ b/src/routes/material.ts @@ -19,169 +19,169 @@ import globals from '../globals'; const router = express.Router(); router.get('/materials', (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; - const {error, value: filters} = - MaterialValidate.query(req.query, ['dev', 'admin'].indexOf(req.authDetails.level) >= 0); - if (error) return res400(error, res); + const {error, value: filters} = + MaterialValidate.query(req.query, ['dev', 'admin'].indexOf(req.authDetails.level) >= 0); + if (error) return res400(error, res); - MaterialModel.find(filters).sort({name: 1}).populate('group_id').populate('supplier_id') - .lean().exec((err, data) => { - if (err) return next(err); + MaterialModel.find(filters).sort({name: 1}).populate('group_id').populate('supplier_id') + .lean().exec((err, data) => { + if (err) return next(err); - // validate all and filter null values from validation errors - res.json(_.compact(data.map(e => MaterialValidate.output(e, true)))); - }); + // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => MaterialValidate.output(e, true)))); + }); }); router.get(`/materials/:state(${globals.status.new}|${globals.status.del})`, (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - MaterialModel.find({status: req.params.state}).populate('group_id').populate('supplier_id') - .lean().exec((err, data) => { - if (err) return next(err); + MaterialModel.find({status: req.params.state}).populate('group_id').populate('supplier_id') + .lean().exec((err, data) => { + if (err) return next(err); - // 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 + res.json(_.compact(data.map(e => MaterialValidate.output(e)))); + }); }); router.get('/material/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; - MaterialModel.findById(req.params.id).populate('group_id').populate('supplier_id').lean().exec((err, data: any) => { - if (err) return next(err); + MaterialModel.findById(req.params.id).populate('group_id').populate('supplier_id').lean().exec((err, data: any) => { + if (err) return next(err); - if (!data) { - return res.status(404).json({status: 'Not found'}); - } + if (!data) { + return res.status(404).json({status: 'Not found'}); + } - // deleted materials only available for dev/admin - if (data.status === globals.status.del && !req.auth(res, ['dev', 'admin'], 'all')) return; - res.json(MaterialValidate.output(data)); - }); + // deleted materials only available for dev/admin + if (data.status === globals.status.del && !req.auth(res, ['dev', 'admin'], 'all')) return; + res.json(MaterialValidate.output(data)); + }); }); router.put('/material/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; - let {error, value: material} = MaterialValidate.input(req.body, 'change'); - if (error) return res400(error, res); + let {error, value: material} = MaterialValidate.input(req.body, 'change'); + if (error) return res400(error, res); - MaterialModel.findById(req.params.id).lean().exec(async (err, materialData: any) => { - if (!materialData) { - return res.status(404).json({status: 'Not found'}); - } - if (materialData.status === 'deleted') { - return res.status(403).json({status: 'Forbidden'}); - } - if (material.hasOwnProperty('name') && material.name !== materialData.name) { - if (!await nameCheck(material, res, next)) return; - } - if (material.hasOwnProperty('group')) { - material = await groupResolve(material, req, next); - if (!material) return; - } - if (material.hasOwnProperty('supplier')) { - material = await supplierResolve(material, req, next); - if (!material) return; - } - if (material.hasOwnProperty('properties')) { - if (!await propertiesCheck(material.properties, 'change', res, next, - materialData.properties.material_template.toString() !== material.properties.material_template)) return; - } + MaterialModel.findById(req.params.id).lean().exec(async (err, materialData: any) => { + if (!materialData) { + return res.status(404).json({status: 'Not found'}); + } + if (materialData.status === 'deleted') { + return res.status(403).json({status: 'Forbidden'}); + } + if (material.hasOwnProperty('name') && material.name !== materialData.name) { + if (!await nameCheck(material, res, next)) return; + } + if (material.hasOwnProperty('group')) { + material = await groupResolve(material, req, next); + if (!material) return; + } + if (material.hasOwnProperty('supplier')) { + material = await supplierResolve(material, req, next); + if (!material) return; + } + if (material.hasOwnProperty('properties')) { + if (!await propertiesCheck(material.properties, 'change', res, next, + materialData.properties.material_template.toString() !== material.properties.material_template)) return; + } - // check for changes - if (!_.isEqual(_.pick(IdValidate.stringify(materialData), _.keys(material)), IdValidate.stringify(material))) { - material.status = globals.status.new; // set status to new - } + // check for changes + if (!_.isEqual(_.pick(IdValidate.stringify(materialData), _.keys(material)), IdValidate.stringify(material))) { + material.status = globals.status.new; // set status to new + } - await MaterialModel.findByIdAndUpdate(req.params.id, material, {new: true}) - .log(req).populate('group_id').populate('supplier_id').lean().exec((err, data) => { - if (err) return next(err); - res.json(MaterialValidate.output(data)); - }); - }); + await MaterialModel.findByIdAndUpdate(req.params.id, material, {new: true}) + .log(req).populate('group_id').populate('supplier_id').lean().exec((err, data) => { + if (err) return next(err); + res.json(MaterialValidate.output(data)); + }); + }); }); router.delete('/material/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; - // check if there are still samples referencing this material - SampleModel.find({'material_id': new mongoose.Types.ObjectId(req.params.id), status: {$ne: globals.status.del}}) - .lean().exec((err, data) => { - if (err) return next(err); - if (data.length) { - return res.status(400).json({status: 'Material still in use'}); - } - MaterialModel.findByIdAndUpdate(req.params.id, {status: globals.status.del}) - .log(req).populate('group_id').populate('supplier_id').lean().exec((err, data) => { - if (err) return next(err); - if (data) { - res.json({status: 'OK'}); - } - else { - res.status(404).json({status: 'Not found'}); - } - }); - }); + // check if there are still samples referencing this material + SampleModel.find({'material_id': new mongoose.Types.ObjectId(req.params.id), status: {$ne: globals.status.del}}) +.lean().exec((err, data) => { + if (err) return next(err); + if (data.length) { + return res.status(400).json({status: 'Material still in use'}); + } + MaterialModel.findByIdAndUpdate(req.params.id, {status: globals.status.del}) + .log(req).populate('group_id').populate('supplier_id').lean().exec((err, data) => { + if (err) return next(err); + if (data) { + res.json({status: 'OK'}); + } + else { + res.status(404).json({status: 'Not found'}); + } + }); +}); }); router.put('/material/restore/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - setStatus(globals.status.new, req, res, next); + setStatus(globals.status.new, req, res, next); }); router.put('/material/validate/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - setStatus(globals.status.val, req, res, next); + setStatus(globals.status.val, req, res, next); }); router.post('/material/new', async (req, res, next) => { - if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; - let {error, value: material} = MaterialValidate.input(req.body, 'new'); - if (error) return res400(error, res); + let {error, value: material} = MaterialValidate.input(req.body, 'new'); + if (error) return res400(error, res); - if (!await nameCheck(material, res, next)) return; - material = await groupResolve(material, req, next); - if (!material) return; - material = await supplierResolve(material, req, next); - if (!material) return; - if (!await propertiesCheck(material.properties, 'new', res, next)) return; + if (!await nameCheck(material, res, next)) return; + material = await groupResolve(material, req, next); + if (!material) return; + material = await supplierResolve(material, req, next); + if (!material) return; + if (!await propertiesCheck(material.properties, 'new', res, next)) return; - material.status = globals.status.new; // set status to new - await new MaterialModel(material).save(async (err, data) => { - if (err) return next(err); - db.log(req, 'materials', {_id: data._id}, data.toObject()); - await data.populate('group_id').populate('supplier_id').execPopulate().catch(err => next(err)); - if (data instanceof Error) return; - res.json(MaterialValidate.output(data.toObject())); - }); + material.status = globals.status.new; // set status to new + await new MaterialModel(material).save(async (err, data) => { + if (err) return next(err); + db.log(req, 'materials', {_id: data._id}, data.toObject()); + await data.populate('group_id').populate('supplier_id').execPopulate().catch(err => next(err)); + if (data instanceof Error) return; + res.json(MaterialValidate.output(data.toObject())); + }); }); router.get('/material/groups', (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; - MaterialGroupModel.find().lean().exec((err, data: any) => { - if (err) return next(err); + MaterialGroupModel.find().lean().exec((err, data: any) => { + if (err) return next(err); - // validate all and filter null values from validation errors - res.json(_.compact(data.map(e => MaterialValidate.outputGroups(e.name)))); - }); + // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => MaterialValidate.outputGroups(e.name)))); + }); }); router.get('/material/suppliers', (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; - MaterialSupplierModel.find().lean().exec((err, data: any) => { - if (err) return next(err); + MaterialSupplierModel.find().lean().exec((err, data: any) => { + if (err) return next(err); - // validate all and filter null values from validation errors - res.json(_.compact(data.map(e => MaterialValidate.outputSuppliers(e.name)))); - }); + // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => MaterialValidate.outputSuppliers(e.name)))); + }); }); @@ -189,81 +189,81 @@ 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)) as any; - if (materialData instanceof Error) return false; - if (materialData) { // could not find material_id - res.status(400).json({status: 'Material name already taken'}); - return false; - } - return true; + 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'}); + return false; +} +return true; } async function groupResolve (material, req, next) { - const groupData = await MaterialGroupModel.findOneAndUpdate( - {name: material.group}, - {name: material.group}, - {upsert: true, new: true} - ).log(req).lean().exec().catch(err => next(err)) as any; - if (groupData instanceof Error) return false; - material.group_id = groupData._id; - delete material.group; - return material; + const groupData = await MaterialGroupModel.findOneAndUpdate( + {name: material.group}, + {name: material.group}, + {upsert: true, new: true} + ).log(req).lean().exec().catch(err => next(err)) as any; + if (groupData instanceof Error) return false; + material.group_id = groupData._id; + delete material.group; + return material; } async function supplierResolve (material, req, next) { - const supplierData = await MaterialSupplierModel.findOneAndUpdate( - {name: material.supplier}, - {name: material.supplier}, - {upsert: true, new: true} - ).log(req).lean().exec().catch(err => next(err)) as any; - if (supplierData instanceof Error) return false; - material.supplier_id = supplierData._id; - delete material.supplier; - return material; + const supplierData = await MaterialSupplierModel.findOneAndUpdate( + {name: material.supplier}, + {name: material.supplier}, + {upsert: true, new: true} + ).log(req).lean().exec().catch(err => next(err)) as any; + if (supplierData instanceof Error) return false; + material.supplier_id = supplierData._id; + delete material.supplier; + return material; } // validate material properties, returns false if invalid, otherwise template data async function propertiesCheck (properties, param, res, next, checkVersion = true) { - if (!properties.material_template || !IdValidate.valid(properties.material_template)) { // template id not found - res.status(400).json({status: 'Material template not available'}); - return false; - } - const materialData = await MaterialTemplateModel.findById(properties.material_template) - .lean().exec().catch(err => next(err)) as any; - if (materialData instanceof Error) return false; - if (!materialData) { // template not found - res.status(400).json({status: 'Material template not available'}); - return false; - } + if (!properties.material_template || !IdValidate.valid(properties.material_template)) { // template id not found + res.status(400).json({status: 'Material template not available'}); + return false; + } + const materialData = await MaterialTemplateModel.findById(properties.material_template) + .lean().exec().catch(err => next(err)) as any; + if (materialData instanceof Error) return false; + if (!materialData) { // template not found + res.status(400).json({status: 'Material template not available'}); + return false; + } - if (checkVersion) { - // get all template versions and check if given is latest - const materialVersions = await MaterialTemplateModel.find({first_id: materialData.first_id}).sort({version: -1}) - .lean().exec().catch(err => next(err)) as any; - if (materialVersions instanceof Error) return false; - if (properties.material_template !== materialVersions[0]._id.toString()) { // template not latest - res.status(400).json({status: 'Old template version not allowed'}); - return false; - } - } + if (checkVersion) { + // get all template versions and check if given is latest + const materialVersions = await MaterialTemplateModel.find({first_id: materialData.first_id}).sort({version: -1}) + .lean().exec().catch(err => next(err)) as any; + if (materialVersions instanceof Error) return false; + if (properties.material_template !== materialVersions[0]._id.toString()) { // template not latest + res.status(400).json({status: 'Old template version not allowed'}); + return false; + } + } - // validate parameters - const {error, value} = ParametersValidate - .input(_.omit(properties, 'material_template'), materialData.parameters, param); - if (error) {res400(error, res); return false;} - Object.keys(value).forEach(key => { - properties[key] = value[key]; - }); - return materialData; + // validate parameters + const {error, value} = ParametersValidate + .input(_.omit(properties, 'material_template'), materialData.parameters, param); + if (error) {res400(error, res); return false;} + Object.keys(value).forEach(key => { + properties[key] = value[key]; + }); + return materialData; } function setStatus (status, req, res, next) { // set measurement status - MaterialModel.findByIdAndUpdate(req.params.id, {status: status}).log(req).lean().exec((err, data) => { - if (err) return next(err); + MaterialModel.findByIdAndUpdate(req.params.id, {status: status}).log(req).lean().exec((err, data) => { + if (err) return next(err); - if (!data) { - return res.status(404).json({status: 'Not found'}); - } - res.json({status: 'OK'}); - }); -} \ No newline at end of file + if (!data) { + return res.status(404).json({status: 'Not found'}); + } + res.json({status: 'OK'}); + }); +} diff --git a/src/routes/measurement.spec.ts b/src/routes/measurement.spec.ts index ec8885f..eb89c19 100644 --- a/src/routes/measurement.spec.ts +++ b/src/routes/measurement.spec.ts @@ -4,850 +4,850 @@ import TestHelper from "../test/helper"; describe('/measurement', () => { - let server; - before(done => TestHelper.before(done)); - beforeEach(done => server = TestHelper.beforeEach(server, done)); - afterEach(done => TestHelper.afterEach(server, done)); - after(done => TestHelper.after(done)); + let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); + after(done => TestHelper.after(done)); - describe('GET /measurement/{id}', () => { - it('returns the right measurement', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/measurement/800000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'} - }); - }); - it('returns the measurement for an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/measurement/800000000000000000000001', - auth: {key: 'admin'}, - httpStatus: 200, - res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'} - }); - }); - it('filters out spectral data for a write user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/measurement/800000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'} - }); - }); - it('returns deleted measurements for a dev/admin user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/measurement/800000000000000000000004', - auth: {basic: 'admin'}, - httpStatus: 200, - res: {_id: '800000000000000000000004', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003'} - }); - }); - it('rejects requests for deleted measurements from a write user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/measurement/800000000000000000000004', - auth: {basic: 'janedoe'}, - httpStatus: 403 - }); - }); - it('rejects an invalid id', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/measurement/8000000000h0000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 404 - }); - }); - it('rejects an unknown id', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/measurement/000000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 404 - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/measurement/800000000000000000000001', - httpStatus: 401 - }); - }); - }); + describe('GET /measurement/{id}', () => { + it('returns the right measurement', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/measurement/800000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'} + }); + }); + it('returns the measurement for an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/measurement/800000000000000000000001', + auth: {key: 'admin'}, + httpStatus: 200, + res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'} + }); + }); + it('filters out spectral data for a write user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/measurement/800000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'} + }); + }); + it('returns deleted measurements for a dev/admin user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/measurement/800000000000000000000004', + auth: {basic: 'admin'}, + httpStatus: 200, + res: {_id: '800000000000000000000004', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003'} + }); + }); + it('rejects requests for deleted measurements from a write user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/measurement/800000000000000000000004', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/measurement/8000000000h0000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects an unknown id', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/measurement/000000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/measurement/800000000000000000000001', + httpStatus: 401 + }); + }); + }); - describe('PUT /measurement/{id}', () => { - it('returns the right measurement', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {}, - res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'} - }); - }); - it('keeps unchanged values', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I', filename: '1_0.DPT'}} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'}); - MeasurementModel.findById('800000000000000000000001').lean().exec((err, data: any) => { - if (err) return done(err); - should(data).have.property('status','validated'); - done(); - }); - }); - }); - it('keeps only one unchanged value', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {values: {'weight %': 0.5}} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({_id: '800000000000000000000002', sample_id: '400000000000000000000002', values: {'weight %': 0.5, 'standard deviation': 0.2}, measurement_template: '300000000000000000000002'}); - MeasurementModel.findById('800000000000000000000002').lean().exec((err, data: any) => { - if (err) return done(err); - should(data).have.property('status','validated'); - done(); - }); - }); - }); - it('changes the given values', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {values: {dpt: [[1,2],[3,4],[5,6]]}} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[1,2],[3,4],[5,6]], device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'}); - MeasurementModel.findById('800000000000000000000001').lean().exec((err, data: any) => { - should(data).have.only.keys('_id', 'sample_id', 'values', 'measurement_template', 'status', '__v'); - should(data.sample_id.toString()).be.eql('400000000000000000000001'); - should(data.measurement_template.toString()).be.eql('300000000000000000000001'); - should(data).have.property('status','new'); - should(data).have.property('values'); - should(data.values).have.property('dpt', [[1,2],[3,4],[5,6]]); - done(); - }); - }); - }); - it('creates a changelog', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {values: {dpt: [[1,2],[3,4],[5,6]], device: 'Alpha I', filename: '1_0.DPT'}}, - log: { - collection: 'measurements', - dataAdd: { - measurement_template: '300000000000000000000001', - sample_id: '400000000000000000000001', - status: 'new' - }, - dataIgn: ['values'] - } - }); - }); - it('allows changing only one value', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {values: {'weight %': 0.9}}, - res: {_id: '800000000000000000000002', sample_id: '400000000000000000000002', values: {'weight %': 0.9, 'standard deviation': 0.2}, measurement_template: '300000000000000000000002'} - }); - }); - it('allows keeping empty values empty', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000005', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {values: {'weight %': 0.9}}, - res: {_id: '800000000000000000000005', sample_id: '400000000000000000000002', values: {'weight %': 0.9, 'standard deviation': null}, measurement_template: '300000000000000000000002'} - }); - }); - it('rejects not specified values', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {values: {'weight %': 0.9, 'standard deviation': 0.3, xx: 44}}, - res: {status: 'Invalid body format', details: '"xx" is not allowed'} - }); - }); - it('rejects a value not in the value range', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000003', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {values: {val1: 4}}, - res: {status: 'Invalid body format', details: '"val1" must be one of [1, 2, 3, null]'} - }); - }); - it('rejects a value below minimum range', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {values: {'weight %': -1, 'standard deviation': 0.3}}, - res: {status: 'Invalid body format', details: '"weight %" must be greater than or equal to 0'} - }); - }); - it('rejects a value above maximum range', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {values: {'weight %': 0.9, 'standard deviation': 3}}, - res: {status: 'Invalid body format', details: '"standard deviation" must be less than or equal to 0.5'} - }); - }); - it('rejects a new measurement template', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {values: {'weight %': 0.9, 'standard deviation': 0.3}, measurement_template: '300000000000000000000001'}, - res: {status: 'Invalid body format', details: '"measurement_template" is not allowed'} - }); - }); - it('rejects a new sample id', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {values: {'weight %': 0.9, 'standard deviation': 0.3}, sample_id: '400000000000000000000002'}, - res: {status: 'Invalid body format', details: '"sample_id" is not allowed'} - }); - }); - it('rejects editing a measurement for a write user who did not create this measurement', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000003', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {values: {val1: 2}} - }); - }); - it('accepts editing a measurement of another user for a dev/admin user', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000002', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {values: {'weight %': 0.9, 'standard deviation': 0.3}}, - res: {_id: '800000000000000000000002', sample_id: '400000000000000000000002', values: {'weight %': 0.9, 'standard deviation': 0.3}, measurement_template: '300000000000000000000002'} - }); - }); - it('rejects an invalid id', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000h00000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 404 - }); - }); - it('rejects an unknown id', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/000000000000000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 404 - }); - }); - it('rejects editing a deleted measurement', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000004', - auth: {basic: 'admin'}, - httpStatus: 403, - req: {} - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000002', - auth: {key: 'janedoe'}, - httpStatus: 401, - req: {values: {'weight %': 0.9, 'standard deviation': 0.3}}, - }); - }); - it('rejects requests from a read user', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000002', - auth: {basic: 'user'}, - httpStatus: 403, - req: {values: {'weight %': 0.9, 'standard deviation': 0.3}}, - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/800000000000000000000002', - httpStatus: 401, - req: {values: {'weight %': 0.9, 'standard deviation': 0.3}}, - }); - }); - }); + describe('PUT /measurement/{id}', () => { + it('returns the right measurement', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {}, + res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'} + }); + }); + it('keeps unchanged values', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I', filename: '1_0.DPT'}} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'}); + MeasurementModel.findById('800000000000000000000001').lean().exec((err, data: any) => { + if (err) return done(err); + should(data).have.property('status','validated'); + done(); + }); + }); + }); + it('keeps only one unchanged value', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000002', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {values: {'weight %': 0.5}} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '800000000000000000000002', sample_id: '400000000000000000000002', values: {'weight %': 0.5, 'standard deviation': 0.2}, measurement_template: '300000000000000000000002'}); + MeasurementModel.findById('800000000000000000000002').lean().exec((err, data: any) => { + if (err) return done(err); + should(data).have.property('status','validated'); + done(); + }); + }); + }); + it('changes the given values', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {values: {dpt: [[1,2],[3,4],[5,6]]}} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[1,2],[3,4],[5,6]], device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'}); + MeasurementModel.findById('800000000000000000000001').lean().exec((err, data: any) => { + should(data).have.only.keys('_id', 'sample_id', 'values', 'measurement_template', 'status', '__v'); + should(data.sample_id.toString()).be.eql('400000000000000000000001'); + should(data.measurement_template.toString()).be.eql('300000000000000000000001'); + should(data).have.property('status','new'); + should(data).have.property('values'); + should(data.values).have.property('dpt', [[1,2],[3,4],[5,6]]); + done(); + }); + }); + }); + it('creates a changelog', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {values: {dpt: [[1,2],[3,4],[5,6]], device: 'Alpha I', filename: '1_0.DPT'}}, + log: { + collection: 'measurements', + dataAdd: { + measurement_template: '300000000000000000000001', + sample_id: '400000000000000000000001', + status: 'new' + }, + dataIgn: ['values'] + } + }); + }); + it('allows changing only one value', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000002', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {values: {'weight %': 0.9}}, + res: {_id: '800000000000000000000002', sample_id: '400000000000000000000002', values: {'weight %': 0.9, 'standard deviation': 0.2}, measurement_template: '300000000000000000000002'} + }); + }); + it('allows keeping empty values empty', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000005', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {values: {'weight %': 0.9}}, + res: {_id: '800000000000000000000005', sample_id: '400000000000000000000002', values: {'weight %': 0.9, 'standard deviation': null}, measurement_template: '300000000000000000000002'} + }); + }); + it('rejects not specified values', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000002', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {values: {'weight %': 0.9, 'standard deviation': 0.3, xx: 44}}, + res: {status: 'Invalid body format', details: '"xx" is not allowed'} + }); + }); + it('rejects a value not in the value range', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000003', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {values: {val1: 4}}, + res: {status: 'Invalid body format', details: '"val1" must be one of [1, 2, 3, null]'} + }); + }); + it('rejects a value below minimum range', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000002', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {values: {'weight %': -1, 'standard deviation': 0.3}}, + res: {status: 'Invalid body format', details: '"weight %" must be greater than or equal to 0'} + }); + }); + it('rejects a value above maximum range', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000002', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {values: {'weight %': 0.9, 'standard deviation': 3}}, + res: {status: 'Invalid body format', details: '"standard deviation" must be less than or equal to 0.5'} + }); + }); + it('rejects a new measurement template', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000002', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {values: {'weight %': 0.9, 'standard deviation': 0.3}, measurement_template: '300000000000000000000001'}, + res: {status: 'Invalid body format', details: '"measurement_template" is not allowed'} + }); + }); + it('rejects a new sample id', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000002', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {values: {'weight %': 0.9, 'standard deviation': 0.3}, sample_id: '400000000000000000000002'}, + res: {status: 'Invalid body format', details: '"sample_id" is not allowed'} + }); + }); + it('rejects editing a measurement for a write user who did not create this measurement', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000003', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {values: {val1: 2}} + }); + }); + it('accepts editing a measurement of another user for a dev/admin user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000002', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {values: {'weight %': 0.9, 'standard deviation': 0.3}}, + res: {_id: '800000000000000000000002', sample_id: '400000000000000000000002', values: {'weight %': 0.9, 'standard deviation': 0.3}, measurement_template: '300000000000000000000002'} + }); + }); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000h00000000000002', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects an unknown id', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/000000000000000000000002', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects editing a deleted measurement', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000004', + auth: {basic: 'admin'}, + httpStatus: 403, + req: {} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000002', + auth: {key: 'janedoe'}, + httpStatus: 401, + req: {values: {'weight %': 0.9, 'standard deviation': 0.3}}, + }); + }); + it('rejects requests from a read user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000002', + auth: {basic: 'user'}, + httpStatus: 403, + req: {values: {'weight %': 0.9, 'standard deviation': 0.3}}, + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/800000000000000000000002', + httpStatus: 401, + req: {values: {'weight %': 0.9, 'standard deviation': 0.3}}, + }); + }); + }); - describe('DELETE /measurement/{id}', () => { - it('sets the status to deleted', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/measurement/800000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - MeasurementModel.findById('800000000000000000000001').lean().exec((err, data) => { - if (err) return done(err); - should(data).have.property('status','deleted'); - done(); - }); - }); - }); - it('creates a changelog', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/measurement/800000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - log: { - collection: 'measurements', - dataAdd: { - status: 'deleted' - } - } - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/measurement/800000000000000000000001', - auth: {key: 'janedoe'}, - httpStatus: 401, - }); - }); - it('rejects requests from a read user', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/measurement/800000000000000000000001', - auth: {basic: 'user'}, - httpStatus: 403, - }); - }); - it('rejects deleting a measurement for a write user who did not create this measurement', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/measurement/800000000000000000000003', - auth: {basic: 'janedoe'}, - httpStatus: 403, - }); - }); - it('accepts deleting a measurement of another user for a dev/admin user', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/measurement/800000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - res: {status: 'OK'} - }); - }); - it('rejects an invalid id', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/measurement/800000000h00000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 404, - }); - }); - it('rejects an unknown id', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/measurement/000000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 404, - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/measurement/800000000000000000000001', - httpStatus: 401, - }); - }); - }); + describe('DELETE /measurement/{id}', () => { + it('sets the status to deleted', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/measurement/800000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + MeasurementModel.findById('800000000000000000000001').lean().exec((err, data) => { + if (err) return done(err); + should(data).have.property('status','deleted'); + done(); + }); + }); + }); + it('creates a changelog', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/measurement/800000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + log: { + collection: 'measurements', + dataAdd: { + status: 'deleted' + } + } + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/measurement/800000000000000000000001', + auth: {key: 'janedoe'}, + httpStatus: 401, + }); + }); + it('rejects requests from a read user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/measurement/800000000000000000000001', + auth: {basic: 'user'}, + httpStatus: 403, + }); + }); + it('rejects deleting a measurement for a write user who did not create this measurement', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/measurement/800000000000000000000003', + auth: {basic: 'janedoe'}, + httpStatus: 403, + }); + }); + it('accepts deleting a measurement of another user for a dev/admin user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/measurement/800000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + res: {status: 'OK'} + }); + }); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/measurement/800000000h00000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 404, + }); + }); + it('rejects an unknown id', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/measurement/000000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 404, + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/measurement/800000000000000000000001', + httpStatus: 401, + }); + }); + }); - describe('GET /measurement/sample/{id}', () => { - it('returns the right measurements', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/measurement/sample/400000000000000000000003', - auth: {basic: 'admin'}, - httpStatus: 200, - res: [ - {_id: '800000000000000000000003', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003', status: 'new'}, - {_id: '800000000000000000000004', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003', status: 'deleted'} - ] - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/measurement/sample/400000000000000000000003', - auth: {key: 'admin'}, - httpStatus: 401 - }); - }); - it('rejects a write user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/measurement/sample/400000000000000000000003', - auth: {basic: 'janedoe'}, - httpStatus: 403 - }); - }); - it('rejects an invalid id', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/measurement/sample/4000000000h0000000000003', - auth: {basic: 'admin'}, - httpStatus: 404 - }); - }); - it('rejects an unknown id', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/measurement/sample/000000000000000000000003', - auth: {basic: 'admin'}, - httpStatus: 404 - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/measurement/sample/400000000000000000000003', - httpStatus: 401 - }); - }); - }); + describe('GET /measurement/sample/{id}', () => { + it('returns the right measurements', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/measurement/sample/400000000000000000000003', + auth: {basic: 'admin'}, + httpStatus: 200, + res: [ + {_id: '800000000000000000000003', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003', status: 'new'}, + {_id: '800000000000000000000004', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003', status: 'deleted'} + ] + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/measurement/sample/400000000000000000000003', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('rejects a write user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/measurement/sample/400000000000000000000003', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/measurement/sample/4000000000h0000000000003', + auth: {basic: 'admin'}, + httpStatus: 404 + }); + }); + it('rejects an unknown id', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/measurement/sample/000000000000000000000003', + auth: {basic: 'admin'}, + httpStatus: 404 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/measurement/sample/400000000000000000000003', + httpStatus: 401 + }); + }); + }); - describe('PUT /measurement/restore/{id}', () => { - it('sets the status', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/restore/800000000000000000000004', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {} - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'OK'}); - MeasurementModel.findById('800000000000000000000004').lean().exec((err, data: any) => { - if (err) return done(err); - should(data).have.property('status','new'); - done(); - }); - }); - }); - it('creates a changelog', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/restore/800000000000000000000004', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {}, - log: { - collection: 'measurements', - dataAdd: { - status: 'new' - } - } - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/restore/800000000000000000000004', - auth: {key: 'admin'}, - httpStatus: 401, - req: {} - }); - }); - it('rejects a write user', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/restore/800000000000000000000004', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {} - }); - }); - it('returns 404 for an unknown sample', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/restore/000000000000000000000004', - auth: {basic: 'admin'}, - httpStatus: 404, - req: {} - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/restore/800000000000000000000004', - httpStatus: 401, - req: {} - }); - }); - }); + describe('PUT /measurement/restore/{id}', () => { + it('sets the status', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/restore/800000000000000000000004', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + MeasurementModel.findById('800000000000000000000004').lean().exec((err, data: any) => { + if (err) return done(err); + should(data).have.property('status','new'); + done(); + }); + }); + }); + it('creates a changelog', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/restore/800000000000000000000004', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {}, + log: { + collection: 'measurements', + dataAdd: { + status: 'new' + } + } + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/restore/800000000000000000000004', + auth: {key: 'admin'}, + httpStatus: 401, + req: {} + }); + }); + it('rejects a write user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/restore/800000000000000000000004', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {} + }); + }); + it('returns 404 for an unknown sample', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/restore/000000000000000000000004', + auth: {basic: 'admin'}, + httpStatus: 404, + req: {} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/restore/800000000000000000000004', + httpStatus: 401, + req: {} + }); + }); + }); - describe('PUT /measurement/validate/{id}', () => { - it('sets the status', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/validate/800000000000000000000003', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {} - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'OK'}); - MeasurementModel.findById('800000000000000000000003').lean().exec((err, data: any) => { - if (err) return done(err); - should(data).have.property('status','validated'); - done(); - }); - }); - }); - it('creates a changelog', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/validate/800000000000000000000003', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {}, - log: { - collection: 'measurements', - dataAdd: { - status: 'validated' - } - } - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/validate/800000000000000000000003', - auth: {key: 'admin'}, - httpStatus: 401, - req: {} - }); - }); - it('rejects a write user', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/validate/800000000000000000000003', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {} - }); - }); - it('returns 404 for an unknown sample', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/validate/000000000000000000000003', - auth: {basic: 'admin'}, - httpStatus: 404, - req: {} - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/measurement/validate/800000000000000000000003', - httpStatus: 401, - req: {} - }); - }); - }); + describe('PUT /measurement/validate/{id}', () => { + it('sets the status', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/validate/800000000000000000000003', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + MeasurementModel.findById('800000000000000000000003').lean().exec((err, data: any) => { + if (err) return done(err); + should(data).have.property('status','validated'); + done(); + }); + }); + }); + it('creates a changelog', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/validate/800000000000000000000003', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {}, + log: { + collection: 'measurements', + dataAdd: { + status: 'validated' + } + } + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/validate/800000000000000000000003', + auth: {key: 'admin'}, + httpStatus: 401, + req: {} + }); + }); + it('rejects a write user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/validate/800000000000000000000003', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {} + }); + }); + it('returns 404 for an unknown sample', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/validate/000000000000000000000003', + auth: {basic: 'admin'}, + httpStatus: 404, + req: {} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/measurement/validate/800000000000000000000003', + httpStatus: 401, + req: {} + }); + }); + }); - describe('POST /measurement/new', () => { - it('returns the right measurement', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} - }).end((err, res) => { - if (err) return done(err); - should(res.body).have.only.keys('_id', 'sample_id', 'values', 'measurement_template'); - should(res.body).have.property('_id').be.type('string'); - should(res.body).have.property('sample_id', '400000000000000000000001'); - should(res.body).have.property('measurement_template', '300000000000000000000002'); - should(res.body).have.property('values'); - should(res.body.values).have.property('weight %', 0.8); - should(res.body.values).have.property('standard deviation', 0.1); - done(); - }); - }); - it('stores the measurement', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} - }).end((err, res) => { - if (err) return done(err); - MeasurementModel.findById(res.body._id).lean().exec((err, data: any) => { - if (err) return done(err); - should(data).have.only.keys('_id', 'sample_id', 'values', 'measurement_template', 'status', '__v'); - should(data.sample_id.toString()).be.eql('400000000000000000000001'); - should(data.measurement_template.toString()).be.eql('300000000000000000000002'); - should(data).have.property('status', 'new'); - should(data).have.property('values'); - should(data.values).have.property('weight %', 0.8); - should(data.values).have.property('standard deviation', 0.1); - done(); - }); - }); - }); - it('creates a changelog', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}, - log: { - collection: 'measurements', - dataAdd: { - status: 'new' - } - } - }); - }); - it('rejects an invalid sample id', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000h00000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}, - res: {status: 'Invalid body format', details: 'Invalid object id'} - }); - }); - it('rejects a sample id not available', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '000000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}, - res: {status: 'Sample id not available'} - }); - }); - it('rejects an invalid measurement_template id', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '30000000000h000000000002'}, - res: {status: 'Invalid body format', details: 'Invalid object id'} - }); - }); - it('rejects a measurement_template not available', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '000000000000000000000002'}, - res: {status: 'Measurement template not available'} - }); - }); - it('rejects not specified values', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1, xx: 44}, measurement_template: '300000000000000000000002'}, - res: {status: 'Invalid body format', details: '"xx" is not allowed'} - }); - }); - it('accepts missing values', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8}, measurement_template: '300000000000000000000002'} - }).end((err, res) => { - if (err) return done(err); - should(res.body).have.only.keys('_id', 'sample_id', 'values', 'measurement_template'); - should(res.body).have.property('_id').be.type('string'); - should(res.body).have.property('sample_id', '400000000000000000000001'); - should(res.body).have.property('measurement_template', '300000000000000000000002'); - should(res.body).have.property('values'); - should(res.body.values).have.property('weight %', 0.8); - should(res.body.values).have.property('standard deviation', null); - done(); - }); - }); - it('rejects no values', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000000000000001', values: {}, measurement_template: '300000000000000000000002'}, - res: {status: 'At least one value is required'} - }); - }); - it('rejects a value not in the value range', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000000000000001', values: {val2: 5}, measurement_template: '300000000000000000000004'}, - res: {status: 'Invalid body format', details: '"val2" must be one of [1, 2, 3, 4, null]'} - }); - }); - it('rejects a value below minimum range', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000000000000001', values: {'weight %': -1, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}, - res: {status: 'Invalid body format', details: '"weight %" must be greater than or equal to 0'} - }); - }); - it('rejects a value above maximum range', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 2}, measurement_template: '300000000000000000000002'}, - res: {status: 'Invalid body format', details: '"standard deviation" must be less than or equal to 0.5'} - }); - }); - it('rejects a missing sample id', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}, - res: {status: 'Invalid body format', details: '"sample_id" is required'} - }); - }); - it('rejects a missing measurement_template', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}}, - res: {status: 'Invalid body format', details: '"measurement_template" is required'} - }); - }); - it('rejects adding a measurement to the sample of another user for a write user', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {sample_id: '400000000000000000000003', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} - }); - }); - it('accepts adding a measurement to the sample of another user for a dev/admin user', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} - }).end((err, res) => { - if (err) return done(err); - should(res.body).have.only.keys('_id', 'sample_id', 'values', 'measurement_template'); - should(res.body).have.property('_id').be.type('string'); - should(res.body).have.property('sample_id', '400000000000000000000001'); - should(res.body).have.property('measurement_template', '300000000000000000000002'); - should(res.body).have.property('values'); - should(res.body.values).have.property('weight %', 0.8); - should(res.body.values).have.property('standard deviation', 0.1); - done(); - }); - }); - it('rejects an old version of a measurement template', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000000000000001', values: {val1: 2}, measurement_template: '300000000000000000000003'}, - res: {status: 'Old template version not allowed'} - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {key: 'janedoe'}, - httpStatus: 401, - req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} - }); - }); - it('rejects requests from a read user', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - auth: {basic: 'user'}, - httpStatus: 403, - req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/measurement/new', - httpStatus: 401, - req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} - }); - }); - }); -}); \ No newline at end of file + describe('POST /measurement/new', () => { + it('returns the right measurement', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} + }).end((err, res) => { + if (err) return done(err); + should(res.body).have.only.keys('_id', 'sample_id', 'values', 'measurement_template'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('sample_id', '400000000000000000000001'); + should(res.body).have.property('measurement_template', '300000000000000000000002'); + should(res.body).have.property('values'); + should(res.body.values).have.property('weight %', 0.8); + should(res.body.values).have.property('standard deviation', 0.1); + done(); + }); + }); + it('stores the measurement', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} + }).end((err, res) => { + if (err) return done(err); + MeasurementModel.findById(res.body._id).lean().exec((err, data: any) => { + if (err) return done(err); + should(data).have.only.keys('_id', 'sample_id', 'values', 'measurement_template', 'status', '__v'); + should(data.sample_id.toString()).be.eql('400000000000000000000001'); + should(data.measurement_template.toString()).be.eql('300000000000000000000002'); + should(data).have.property('status', 'new'); + should(data).have.property('values'); + should(data.values).have.property('weight %', 0.8); + should(data.values).have.property('standard deviation', 0.1); + done(); + }); + }); + }); + it('creates a changelog', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}, + log: { + collection: 'measurements', + dataAdd: { + status: 'new' + } + } + }); + }); + it('rejects an invalid sample id', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {sample_id: '400000000000h00000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}, + res: {status: 'Invalid body format', details: 'Invalid object id'} + }); + }); + it('rejects a sample id not available', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {sample_id: '000000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}, + res: {status: 'Sample id not available'} + }); + }); + it('rejects an invalid measurement_template id', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '30000000000h000000000002'}, + res: {status: 'Invalid body format', details: 'Invalid object id'} + }); + }); + it('rejects a measurement_template not available', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '000000000000000000000002'}, + res: {status: 'Measurement template not available'} + }); + }); + it('rejects not specified values', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1, xx: 44}, measurement_template: '300000000000000000000002'}, + res: {status: 'Invalid body format', details: '"xx" is not allowed'} + }); + }); + it('accepts missing values', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8}, measurement_template: '300000000000000000000002'} + }).end((err, res) => { + if (err) return done(err); + should(res.body).have.only.keys('_id', 'sample_id', 'values', 'measurement_template'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('sample_id', '400000000000000000000001'); + should(res.body).have.property('measurement_template', '300000000000000000000002'); + should(res.body).have.property('values'); + should(res.body.values).have.property('weight %', 0.8); + should(res.body.values).have.property('standard deviation', null); + done(); + }); + }); + it('rejects no values', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {sample_id: '400000000000000000000001', values: {}, measurement_template: '300000000000000000000002'}, + res: {status: 'At least one value is required'} + }); + }); + it('rejects a value not in the value range', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {sample_id: '400000000000000000000001', values: {val2: 5}, measurement_template: '300000000000000000000004'}, + res: {status: 'Invalid body format', details: '"val2" must be one of [1, 2, 3, 4, null]'} + }); + }); + it('rejects a value below minimum range', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {sample_id: '400000000000000000000001', values: {'weight %': -1, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}, + res: {status: 'Invalid body format', details: '"weight %" must be greater than or equal to 0'} + }); + }); + it('rejects a value above maximum range', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 2}, measurement_template: '300000000000000000000002'}, + res: {status: 'Invalid body format', details: '"standard deviation" must be less than or equal to 0.5'} + }); + }); + it('rejects a missing sample id', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}, + res: {status: 'Invalid body format', details: '"sample_id" is required'} + }); + }); + it('rejects a missing measurement_template', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}}, + res: {status: 'Invalid body format', details: '"measurement_template" is required'} + }); + }); + it('rejects adding a measurement to the sample of another user for a write user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {sample_id: '400000000000000000000003', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} + }); + }); + it('accepts adding a measurement to the sample of another user for a dev/admin user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} + }).end((err, res) => { + if (err) return done(err); + should(res.body).have.only.keys('_id', 'sample_id', 'values', 'measurement_template'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('sample_id', '400000000000000000000001'); + should(res.body).have.property('measurement_template', '300000000000000000000002'); + should(res.body).have.property('values'); + should(res.body.values).have.property('weight %', 0.8); + should(res.body.values).have.property('standard deviation', 0.1); + done(); + }); + }); + it('rejects an old version of a measurement template', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {sample_id: '400000000000000000000001', values: {val1: 2}, measurement_template: '300000000000000000000003'}, + res: {status: 'Old template version not allowed'} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {key: 'janedoe'}, + httpStatus: 401, + req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} + }); + }); + it('rejects requests from a read user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + auth: {basic: 'user'}, + httpStatus: 403, + req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/measurement/new', + httpStatus: 401, + req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} + }); + }); + }); +}); diff --git a/src/routes/measurement.ts b/src/routes/measurement.ts index 701cf8a..863a067 100644 --- a/src/routes/measurement.ts +++ b/src/routes/measurement.ts @@ -16,114 +16,114 @@ import mongoose from "mongoose"; const router = express.Router(); router.get('/measurement/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; - MeasurementModel.findById(req.params.id).lean().exec((err, data: any) => { - if (err) return next(err); - if (!data) { - return res.status(404).json({status: 'Not found'}); - } - // deleted measurements only available for dev/admin - if (data.status === globals.status.del && !req.auth(res, ['dev', 'admin'], 'all')) return; + MeasurementModel.findById(req.params.id).lean().exec((err, data: any) => { + if (err) return next(err); + if (!data) { + return res.status(404).json({status: 'Not found'}); + } + // deleted measurements only available for dev/admin + if (data.status === globals.status.del && !req.auth(res, ['dev', 'admin'], 'all')) return; - res.json(MeasurementValidate.output(data, req)); - }); + res.json(MeasurementValidate.output(data, req)); + }); }); router.put('/measurement/' + IdValidate.parameter(), async (req, res, next) => { - if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; - const {error, value: measurement} = MeasurementValidate.input(req.body, 'change'); - if (error) return res400(error, res); + const {error, value: measurement} = MeasurementValidate.input(req.body, 'change'); + if (error) return res400(error, res); - const data = await MeasurementModel.findById(req.params.id).lean().exec().catch(err => {next(err);}) as any; - if (data instanceof Error) return; - if (!data) { - return res.status(404).json({status: 'Not found'}); - } - if (data.status === 'deleted') { - return res.status(403).json({status: 'Forbidden'}); - } + const data = await MeasurementModel.findById(req.params.id).lean().exec().catch(err => {next(err);}) as any; + if (data instanceof Error) return; + if (!data) { + return res.status(404).json({status: 'Not found'}); + } + if (data.status === 'deleted') { + return res.status(403).json({status: 'Forbidden'}); + } - // add properties needed for sampleIdCheck - measurement.measurement_template = data.measurement_template; - measurement.sample_id = data.sample_id; - if (!await sampleIdCheck(measurement, req, res, next)) return; + // add properties needed for sampleIdCheck + measurement.measurement_template = data.measurement_template; +measurement.sample_id = data.sample_id; +if (!await sampleIdCheck(measurement, req, res, next)) return; - // check for changes - if (measurement.values) { // fill not changed values from database - measurement.values = _.assign({}, data.values, measurement.values); - if (!_.isEqual(measurement.values, data.values)) { - measurement.status = globals.status.new; // set status to new - } - } +// check for changes +if (measurement.values) { // fill not changed values from database + measurement.values = _.assign({}, data.values, measurement.values); + if (!_.isEqual(measurement.values, data.values)) { + measurement.status = globals.status.new; // set status to new + } +} - if (!await templateCheck(measurement, 'change', res, next)) return; - await MeasurementModel.findByIdAndUpdate(req.params.id, measurement, {new: true}) - .log(req).lean().exec((err, data) => { - if (err) return next(err); - res.json(MeasurementValidate.output(data, req)); - }); +if (!await templateCheck(measurement, 'change', res, next)) return; +await MeasurementModel.findByIdAndUpdate(req.params.id, measurement, {new: true}) +.log(req).lean().exec((err, data) => { + if (err) return next(err); + res.json(MeasurementValidate.output(data, req)); +}); }); router.delete('/measurement/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; - MeasurementModel.findById(req.params.id).lean().exec(async (err, data) => { - if (err) return next(err); - if (!data) { - return res.status(404).json({status: 'Not found'}); - } - if (!await sampleIdCheck(data, req, res, next)) return; - await MeasurementModel.findByIdAndUpdate(req.params.id, {status: globals.status.del}) - .log(req).lean().exec(err => { - if (err) return next(err); - return res.json({status: 'OK'}); - }); - }); + MeasurementModel.findById(req.params.id).lean().exec(async (err, data) => { + if (err) return next(err); + if (!data) { + return res.status(404).json({status: 'Not found'}); + } + if (!await sampleIdCheck(data, req, res, next)) return; + await MeasurementModel.findByIdAndUpdate(req.params.id, {status: globals.status.del}) + .log(req).lean().exec(err => { + if (err) return next(err); + return res.json({status: 'OK'}); + }); + }); }); router.get('/measurement/sample/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - MeasurementModel.find({sample_id: mongoose.Types.ObjectId(req.params.id)}).lean().exec((err, data: any) => { - if (err) return next(err); - if (!data.length) { - return res.status(404).json({status: 'Not found'}); - } + MeasurementModel.find({sample_id: mongoose.Types.ObjectId(req.params.id)}).lean().exec((err, data: any) => { + if (err) return next(err); + if (!data.length) { + return res.status(404).json({status: 'Not found'}); + } - res.json(_.compact(data.map(e => MeasurementValidate.output(e, req, true)))); - }); + res.json(_.compact(data.map(e => MeasurementValidate.output(e, req, true)))); + }); }); router.put('/measurement/restore/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - setStatus(globals.status.new, req, res, next); + setStatus(globals.status.new, req, res, next); }); router.put('/measurement/validate/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - setStatus(globals.status.val, req, res, next); + setStatus(globals.status.val, req, res, next); }); router.post('/measurement/new', async (req, res, next) => { - if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; - const {error, value: measurement} = MeasurementValidate.input(req.body, 'new'); - if (error) return res400(error, res); + const {error, value: measurement} = MeasurementValidate.input(req.body, 'new'); + if (error) return res400(error, res); - if (!await sampleIdCheck(measurement, req, res, next)) return; - measurement.values = await templateCheck(measurement, 'new', res, next); - if (!measurement.values) return; + if (!await sampleIdCheck(measurement, req, res, next)) return; + measurement.values = await templateCheck(measurement, 'new', res, next); + if (!measurement.values) return; - measurement.status = globals.status.new; - await new MeasurementModel(measurement).save((err, data) => { - if (err) return next(err); - db.log(req, 'measurements', {_id: data._id}, data.toObject()); - res.json(MeasurementValidate.output(data.toObject(), req)); - }); + measurement.status = globals.status.new; + await new MeasurementModel(measurement).save((err, data) => { + if (err) return next(err); + db.log(req, 'measurements', {_id: data._id}, data.toObject()); + res.json(MeasurementValidate.output(data.toObject(), req)); + }); }); @@ -132,61 +132,61 @@ module.exports = router; // validate sample_id, returns false if invalid or user has no access for this sample async function sampleIdCheck (measurement, req, res, next) { - const sampleData = await SampleModel.findById(measurement.sample_id) - .lean().exec().catch(err => {next(err); return false;}) as any; - if (!sampleData) { // sample_id not found - res.status(400).json({status: 'Sample id not available'}); - return false - } - // sample does not belong to user - return !(sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic')); + const sampleData = await SampleModel.findById(measurement.sample_id) + .lean().exec().catch(err => {next(err); return false;}) as any; + if (!sampleData) { // sample_id not found + res.status(400).json({status: 'Sample id not available'}); + return false + } + // sample does not belong to user + return !(sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic')); } // validate measurement_template and values, returns values, true if values are {} or false if invalid, // param for 'new'/'change' async function templateCheck (measurement, param, res, next) { - 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'}); - return false - } + 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'}); + return false + } - // fill not given values for new measurements - if (param === 'new') { - // get all template versions and check if given is latest - const templateVersions = await MeasurementTemplateModel.find({first_id: templateData.first_id}).sort({version: -1}) - .lean().exec().catch(err => next(err)) as any; - if (templateVersions instanceof Error) return false; - if (measurement.measurement_template !== templateVersions[0]._id.toString()) { // template not latest - res.status(400).json({status: 'Old template version not allowed'}); - return false; - } + // fill not given values for new measurements + if (param === 'new') { + // get all template versions and check if given is latest + const templateVersions = await MeasurementTemplateModel.find({first_id: templateData.first_id}).sort({version: -1}) + .lean().exec().catch(err => next(err)) as any; + if (templateVersions instanceof Error) return false; + if (measurement.measurement_template !== templateVersions[0]._id.toString()) { // template not latest + res.status(400).json({status: 'Old template version not allowed'}); + return false; + } - if (Object.keys(measurement.values).length === 0) { - res.status(400).json({status: 'At least one value is required'}); - return false - } - const fillValues = {}; // initialize not given values with null - templateData.parameters.forEach(parameter => { - fillValues[parameter.name] = null; - }); - measurement.values = _.assign({}, fillValues, measurement.values); - } + if (Object.keys(measurement.values).length === 0) { + res.status(400).json({status: 'At least one value is required'}); + return false + } + const fillValues = {}; // initialize not given values with null + templateData.parameters.forEach(parameter => { + fillValues[parameter.name] = null; + }); + measurement.values = _.assign({}, fillValues, measurement.values); + } - // validate values - const {error, value} = ParametersValidate.input(measurement.values, templateData.parameters, 'null'); - if (error) {res400(error, res); return false;} - return value || true; + // validate values + const {error, value} = ParametersValidate.input(measurement.values, templateData.parameters, 'null'); + if (error) {res400(error, res); return false;} + return value || true; } function setStatus (status, req, res, next) { // set measurement status - MeasurementModel.findByIdAndUpdate(req.params.id, {status: status}).log(req).lean().exec((err, data) => { - if (err) return next(err); + MeasurementModel.findByIdAndUpdate(req.params.id, {status: status}).log(req).lean().exec((err, data) => { + if (err) return next(err); - if (!data) { - return res.status(404).json({status: 'Not found'}); - } - res.json({status: 'OK'}); - }); -} \ No newline at end of file + if (!data) { + return res.status(404).json({status: 'Not found'}); + } + res.json({status: 'OK'}); + }); +} diff --git a/src/routes/model.spec.ts b/src/routes/model.spec.ts index 88a963e..66820e9 100644 --- a/src/routes/model.spec.ts +++ b/src/routes/model.spec.ts @@ -7,542 +7,542 @@ import mongoose from 'mongoose'; describe('/model', () => { - let server; - before(done => TestHelper.before(done)); - beforeEach(done => server = TestHelper.beforeEach(server, done)); - afterEach(done => TestHelper.afterEach(server, done)); - after(done => TestHelper.after(done)); + let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); + after(done => TestHelper.after(done)); - describe('GET /model/groups', () => { - it('returns all groups for an admin user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/model/groups', - auth: {basic: 'admin'}, - httpStatus: 200, - }).end((err, res) => { - if (err) return done (err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.models.length); - should(res.body).matchEach(group => { - should(group).have.only.keys('group', 'models'); - should(group).have.property('group').be.type('string'); - should(group.models).matchEach(model => { - should(model).have.only.keys('_id', 'name', 'url'); - should(model).have.property('_id').be.type('string'); - should(model).have.property('name').be.type('string'); - should(model).have.property('url').be.type('string'); - }); - }); - done(); - }); - }); - it('returns all allowed groups for a predict user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/model/groups', - auth: {basic: 'customer'}, - httpStatus: 200, - }).end((err, res) => { - if (err) return done (err); - should(res.body).have.lengthOf(1); - should(res.body).matchEach(group => { - should(group).have.only.keys('group', 'models'); - should(group).have.property('group').be.type('string'); - should(group).have.property('models', [{_id: '120000000000000000000001', name: 'Model A', url: 'http://model-a.com'}]); - }); - done(); - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/model/groups', - auth: {key: 'janedoe'}, - httpStatus: 401, - }); - }); - it('rejects an unauthorized request', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/model/groups', - httpStatus: 401, - }); - }); - }); + describe('GET /model/groups', () => { + it('returns all groups for an admin user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/groups', + auth: {basic: 'admin'}, + httpStatus: 200, + }).end((err, res) => { + if (err) return done (err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.models.length); + should(res.body).matchEach(group => { + should(group).have.only.keys('group', 'models'); + should(group).have.property('group').be.type('string'); + should(group.models).matchEach(model => { + should(model).have.only.keys('_id', 'name', 'url'); + should(model).have.property('_id').be.type('string'); + should(model).have.property('name').be.type('string'); + should(model).have.property('url').be.type('string'); + }); + }); + done(); + }); + }); + it('returns all allowed groups for a predict user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/groups', + auth: {basic: 'customer'}, + httpStatus: 200, + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.lengthOf(1); + should(res.body).matchEach(group => { + should(group).have.only.keys('group', 'models'); + should(group).have.property('group').be.type('string'); + should(group).have.property('models', [{_id: '120000000000000000000001', name: 'Model A', url: 'http://model-a.com'}]); + }); + done(); + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/groups', + auth: {key: 'janedoe'}, + httpStatus: 401, + }); + }); + it('rejects an unauthorized request', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/groups', + httpStatus: 401, + }); + }); + }); - describe('POST /model/{group}', () => { - it('adds a new model', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/model/VN', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {name: 'Model C', url: 'http://model-c.com'} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - ModelModel.findOne({group: 'VN'}).lean().exec((err, res) => { - if (err) return done(err); - const model = res.models.find(e => e.name === 'Model C'); - should(model).have.property('url', 'http://model-c.com'); - done(); - }); - }); - }); - it('adds a new group', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/model/classification', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {name: 'Model 0.1', url: 'http://model-0-1.com'} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - ModelModel.findOne({group: 'classification'}).lean().exec((err, res) => { - if (err) return done(err); - should(res).have.only.keys('_id', 'group', 'models', '__v'); - should(res).have.property('group', 'classification'); - should(res.models[0]).have.only.keys('_id', 'name', 'url'); - should(res.models[0]).have.property('name', 'Model 0.1'); - should(res.models[0]).have.property('url', 'http://model-0-1.com'); - done(); - }); - }); - }); - it('replaces a model', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/model/VN', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {name: 'Model A', url: 'http://model-a-new.com'} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - ModelModel.findOne({group: 'VN'}).lean().exec((err, res) => { - if (err) return done(err); - const model = res.models.find(e => e.name === 'Model A'); - should(model).have.property('url', 'http://model-a-new.com'); - done(); - }); - }); - }); - it('rejects an empty name', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/model/VN', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {name: '', url: 'http://model-c.com'}, - res:{status: 'Invalid body format', details: '"name" is not allowed to be empty'} - }); - }); - it('rejects a missing name', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/model/VN', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {url: 'http://model-c.com'}, - res:{status: 'Invalid body format', details: '"name" is required'} - }); - }); - it('rejects an invalid URL', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/model/VN', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {name: 'Model C', url: 'model-c'}, - res:{status: 'Invalid body format', details: '"url" must be a valid uri'} - }); - }); - it('rejects a missing URL', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/model/VN', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {name: 'Model C'}, - res:{status: 'Invalid body format', details: '"url" is required'} - }); - }); - it('rejects a write user', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/model/VN', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {name: 'Model C', url: 'http://model-c.com'} - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/model/VN', - auth: {key: 'admin'}, - httpStatus: 401, - req: {name: 'Model C', url: 'http://model-c.com'} - }); - }); - it('rejects an unauthorized request', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/model/VN', - httpStatus: 401, - req: {name: 'Model C', url: 'http://model-c.com'} - }); - }); - }); + describe('POST /model/{group}', () => { + it('adds a new model', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/VN', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'Model C', url: 'http://model-c.com'} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + ModelModel.findOne({group: 'VN'}).lean().exec((err, res) => { + if (err) return done(err); + const model = res.models.find(e => e.name === 'Model C'); + should(model).have.property('url', 'http://model-c.com'); + done(); + }); + }); + }); + it('adds a new group', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/classification', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'Model 0.1', url: 'http://model-0-1.com'} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + ModelModel.findOne({group: 'classification'}).lean().exec((err, res) => { + if (err) return done(err); + should(res).have.only.keys('_id', 'group', 'models', '__v'); + should(res).have.property('group', 'classification'); + should(res.models[0]).have.only.keys('_id', 'name', 'url'); + should(res.models[0]).have.property('name', 'Model 0.1'); + should(res.models[0]).have.property('url', 'http://model-0-1.com'); + done(); + }); + }); + }); + it('replaces a model', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/VN', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'Model A', url: 'http://model-a-new.com'} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + ModelModel.findOne({group: 'VN'}).lean().exec((err, res) => { + if (err) return done(err); + const model = res.models.find(e => e.name === 'Model A'); + should(model).have.property('url', 'http://model-a-new.com'); + done(); + }); + }); + }); + it('rejects an empty name', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/VN', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: '', url: 'http://model-c.com'}, + res:{status: 'Invalid body format', details: '"name" is not allowed to be empty'} + }); + }); + it('rejects a missing name', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/VN', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {url: 'http://model-c.com'}, + res:{status: 'Invalid body format', details: '"name" is required'} + }); + }); + it('rejects an invalid URL', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/VN', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'Model C', url: 'model-c'}, + res:{status: 'Invalid body format', details: '"url" must be a valid uri'} + }); + }); + it('rejects a missing URL', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/VN', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'Model C'}, + res:{status: 'Invalid body format', details: '"url" is required'} + }); + }); + it('rejects a write user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/VN', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {name: 'Model C', url: 'http://model-c.com'} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/VN', + auth: {key: 'admin'}, + httpStatus: 401, + req: {name: 'Model C', url: 'http://model-c.com'} + }); + }); + it('rejects an unauthorized request', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/VN', + httpStatus: 401, + req: {name: 'Model C', url: 'http://model-c.com'} + }); + }); + }); - describe('DELETE /model/{group}/{name}', () => { - it('deletes the model', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/model/VN/Model%20A', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - ModelModel.findOne({group: 'VN'}).lean().exec((err, res) => { - if (err) return done(err); - should(res).have.only.keys('_id', 'group', 'models'); - should(res).have.property('group', 'VN'); - should(res.models[0]).have.only.keys('_id', 'name', 'url'); - should(res.models[0]).have.property('name', 'Model B'); - should(res.models[0]).have.property('url', 'http://model-b.com'); - done(); - }); - }); - }); - it('deletes the group, if empty afterwards', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/model/Moisture/Model%201', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - ModelModel.find({group: 'Moisture'}).lean().exec((err, res) => { - if (err) return done(err); - should(res).have.lengthOf(0); - done(); - }); - }); - }); - it ('removes the model_id from all user.models', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/model/VN/Model%20A', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - UserModel.find({models: mongoose.Types.ObjectId("120000000000000000000001")}).lean().exec((err, res) => { - if (err) return done(err); - should(res).have.lengthOf(0); - done(); - }); - }); - }); - it('returns 404 for an unknown group', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/model/xxx/Model%201', - auth: {basic: 'admin'}, - httpStatus: 404 - }); - }); - it('returns 404 for an unknown model', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/model/VN/xxx', - auth: {basic: 'admin'}, - httpStatus: 404 - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/model/VN/Model%20A', - auth: {key: 'admin'}, - httpStatus: 401 - }); - }); - it('rejects a write user', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/model/VN/Model%20A', - auth: {basic: 'janedoe'}, - httpStatus: 403 - }); - }); - it('rejects an unauthorized request', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/model/VN/Model%20A', - httpStatus: 401 - }); - }); - }); + describe('DELETE /model/{group}/{name}', () => { + it('deletes the model', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/model/VN/Model%20A', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + ModelModel.findOne({group: 'VN'}).lean().exec((err, res) => { + if (err) return done(err); + should(res).have.only.keys('_id', 'group', 'models'); + should(res).have.property('group', 'VN'); + should(res.models[0]).have.only.keys('_id', 'name', 'url'); + should(res.models[0]).have.property('name', 'Model B'); + should(res.models[0]).have.property('url', 'http://model-b.com'); + done(); + }); + }); + }); + it('deletes the group, if empty afterwards', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/model/Moisture/Model%201', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + ModelModel.find({group: 'Moisture'}).lean().exec((err, res) => { + if (err) return done(err); + should(res).have.lengthOf(0); + done(); + }); + }); + }); + it ('removes the model_id from all user.models', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/model/VN/Model%20A', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + UserModel.find({models: mongoose.Types.ObjectId("120000000000000000000001")}).lean().exec((err, res) => { + if (err) return done(err); + should(res).have.lengthOf(0); + done(); + }); + }); + }); + it('returns 404 for an unknown group', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/model/xxx/Model%201', + auth: {basic: 'admin'}, + httpStatus: 404 + }); + }); + it('returns 404 for an unknown model', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/model/VN/xxx', + auth: {basic: 'admin'}, + httpStatus: 404 + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/model/VN/Model%20A', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('rejects a write user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/model/VN/Model%20A', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('rejects an unauthorized request', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/model/VN/Model%20A', + httpStatus: 401 + }); + }); + }); - describe('GET /model/files', () => { - it('rejects a write user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/model/files', - auth: {basic: 'janedoe'}, - httpStatus: 403, - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/model/files', - auth: {key: 'admin'}, - httpStatus: 401, - }); - }); - it('rejects an unauthorized request', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/model/files', - httpStatus: 401, - }); - }); - }); + describe('GET /model/files', () => { + it('rejects a write user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/files', + auth: {basic: 'janedoe'}, + httpStatus: 403, + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/files', + auth: {key: 'admin'}, + httpStatus: 401, + }); + }); + it('rejects an unauthorized request', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/files', + httpStatus: 401, + }); + }); + }); - describe('GET /model/file/{name}', (() => { - it('returns the binary data', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/model/file/modela', - auth: {basic: 'admin'}, - httpStatus: 200, - contentType: 'application/octet-stream; charset=utf-8', - }).end((err, res) => { - if (err) return done (err); - should(res.body.toString()).be.eql('binary data'); - done(); - }); - }); - it('returns the binary data for an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/model/file/modela', - auth: {key: 'admin'}, - httpStatus: 200, - contentType: 'application/octet-stream; charset=utf-8', - }).end((err, res) => { - if (err) return done (err); - should(res.body.toString()).be.eql('binary data'); - done(); - }); - }); - it('returns 404 for an unknown name', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/model/file/modelx', - auth: {basic: 'admin'}, - httpStatus: 404 - }) - }); - it('rejects requests from a write user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/model/file/modela', - auth: {basic: 'janedoe'}, - httpStatus: 403 - }) - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/model/file/modela', - httpStatus: 401 - }) - }); - })); + describe('GET /model/file/{name}', (() => { + it('returns the binary data', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/file/modela', + auth: {basic: 'admin'}, + httpStatus: 200, + contentType: 'application/octet-stream; charset=utf-8', + }).end((err, res) => { + if (err) return done (err); + should(res.body.toString()).be.eql('binary data'); + done(); + }); + }); + it('returns the binary data for an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/file/modela', + auth: {key: 'admin'}, + httpStatus: 200, + contentType: 'application/octet-stream; charset=utf-8', + }).end((err, res) => { + if (err) return done (err); + should(res.body.toString()).be.eql('binary data'); + done(); + }); + }); + it('returns 404 for an unknown name', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/file/modelx', + auth: {basic: 'admin'}, + httpStatus: 404 + }) + }); + it('rejects requests from a write user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/file/modela', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }) + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/file/modela', + httpStatus: 401 + }) + }); + })); - describe('POST /model/file/{name}', () => { - it('stores the data', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/model/file/modelb', - auth: {basic: 'admin'}, - httpStatus: 200, - reqContentType: 'application/octet-stream', - req: 'another binary data' - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'OK'}); - ModelFileModel.find({name: 'modelb'}).lean().exec((err, data) => { - if (err) return done (err); - should(data).have.lengthOf(1); - should(data[0]).have.only.keys('_id', 'name', 'data', '__v'); - should(data[0]).have.property('name', 'modelb'); - should(data[0].data.buffer.toString()).be.eql('another binary data'); - done(); - }); - }); - }); - it('stores the data with an API key', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/model/file/modelb', - auth: {key: 'admin'}, - httpStatus: 200, - reqContentType: 'application/octet-stream', - req: 'another binary data' - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'OK'}); - ModelFileModel.find({name: 'modelb'}).lean().exec((err, data) => { - if (err) return done (err); - should(data).have.lengthOf(1); - should(data[0]).have.only.keys('_id', 'name', 'data', '__v'); - should(data[0]).have.property('name', 'modelb'); - should(data[0].data.buffer.toString()).be.eql('another binary data'); - done(); - }); - }); - }); - it('overwrites existing data', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/model/file/modela', - auth: {basic: 'admin'}, - httpStatus: 200, - reqContentType: 'application/octet-stream', - req: 'another binary data' - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'OK'}); - ModelFileModel.find({name: 'modela'}).lean().exec((err, data) => { - if (err) return done (err); - should(data).have.lengthOf(1); - should(data[0]).have.only.keys('_id', 'name', 'data', '__v'); - should(data[0]).have.property('name', 'modela'); - should(data[0].data.buffer.toString()).be.eql('another binary data'); - done(); - }); - }); - }); - it('rejects requests from a write user', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/model/file/modelb', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: 'another binary data' - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/model/file/modelb', - httpStatus: 401, - req: 'another binary data' - }); - }); - }); + describe('POST /model/file/{name}', () => { + it('stores the data', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/file/modelb', + auth: {basic: 'admin'}, + httpStatus: 200, + reqContentType: 'application/octet-stream', + req: 'another binary data' + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + ModelFileModel.find({name: 'modelb'}).lean().exec((err, data) => { + if (err) return done (err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'data', '__v'); + should(data[0]).have.property('name', 'modelb'); + should(data[0].data.buffer.toString()).be.eql('another binary data'); + done(); + }); + }); + }); + it('stores the data with an API key', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/file/modelb', + auth: {key: 'admin'}, + httpStatus: 200, + reqContentType: 'application/octet-stream', + req: 'another binary data' + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + ModelFileModel.find({name: 'modelb'}).lean().exec((err, data) => { + if (err) return done (err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'data', '__v'); + should(data[0]).have.property('name', 'modelb'); + should(data[0].data.buffer.toString()).be.eql('another binary data'); + done(); + }); + }); + }); + it('overwrites existing data', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/file/modela', + auth: {basic: 'admin'}, + httpStatus: 200, + reqContentType: 'application/octet-stream', + req: 'another binary data' + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + ModelFileModel.find({name: 'modela'}).lean().exec((err, data) => { + if (err) return done (err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'data', '__v'); + should(data[0]).have.property('name', 'modela'); + should(data[0].data.buffer.toString()).be.eql('another binary data'); + done(); + }); + }); + }); + it('rejects requests from a write user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/file/modelb', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: 'another binary data' + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/model/file/modelb', + httpStatus: 401, + req: 'another binary data' + }); + }); + }); - describe('DELETE /model/file/{name}', () => { - it('deletes the data', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/model/file/modela', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - ModelFileModel.find({name: 'modela'}).lean().exec((err, data) => { - if (err) return done(err); - should(data).have.lengthOf(0); - done(); - }); - }); - }); - it('returns 404 for an unknown name', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/model/file/modelx', - auth: {basic: 'admin'}, - httpStatus: 404 - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/model/file/modela', - auth: {key: 'admin'}, - httpStatus: 401 - }); - }); - it('rejects a write user', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/model/file/modela', - auth: {basic: 'janedoe'}, - httpStatus: 403 - }); - }); - it('rejects an unauthorized request', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/model/file/modela', - httpStatus: 401 - }); - }); - }); + describe('DELETE /model/file/{name}', () => { + it('deletes the data', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/model/file/modela', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + ModelFileModel.find({name: 'modela'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(0); + done(); + }); + }); + }); + it('returns 404 for an unknown name', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/model/file/modelx', + auth: {basic: 'admin'}, + httpStatus: 404 + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/model/file/modela', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('rejects a write user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/model/file/modela', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('rejects an unauthorized request', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/model/file/modela', + httpStatus: 401 + }); + }); + }); - describe('GET /model/authorized/{url}', () => { - it('returns OK for every model for admins', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/model/authorized/xx', - auth: {basic: 'admin'}, - httpStatus: 200, - res: {status: 'OK'} - }); - }); - it('returns OK for a specified model for a predict user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/model/authorized/http%3A%2F%2Fmodel-a.com', - auth: {basic: 'customer'}, - httpStatus: 200, - res: {status: 'OK'} - }); - }); - it('rejects a model not specified for a predict user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/model/authorized/http%3A%2F%2Fmodel-b.com', - auth: {basic: 'customer'}, - httpStatus: 403 - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/model/authorized/xx', - auth: {key: 'admin'}, - httpStatus: 401 - }); - }); - it('rejects an unauthorized request', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/model/authorized/http%3A%2F%2Fmodel-b.com', - httpStatus: 401 - }); - }); - }); -}); \ No newline at end of file + describe('GET /model/authorized/{url}', () => { + it('returns OK for every model for admins', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/authorized/xx', + auth: {basic: 'admin'}, + httpStatus: 200, + res: {status: 'OK'} + }); + }); + it('returns OK for a specified model for a predict user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/authorized/http%3A%2F%2Fmodel-a.com', + auth: {basic: 'customer'}, + httpStatus: 200, + res: {status: 'OK'} + }); + }); + it('rejects a model not specified for a predict user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/authorized/http%3A%2F%2Fmodel-b.com', + auth: {basic: 'customer'}, + httpStatus: 403 + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/authorized/xx', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('rejects an unauthorized request', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/model/authorized/http%3A%2F%2Fmodel-b.com', + httpStatus: 401 + }); + }); + }); +}); diff --git a/src/routes/model.ts b/src/routes/model.ts index 70bf5ac..89a3f85 100644 --- a/src/routes/model.ts +++ b/src/routes/model.ts @@ -13,162 +13,162 @@ import mongoose from "mongoose"; const router = express.Router(); router.get('/model/groups', (req, res, next) => { - if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return; - let conditions: any = [{}, {}]; - if (['dev', 'admin'].indexOf(req.authDetails.level) < 0) { // if not dev or admin, user has to possess model rights - conditions = [ - {'models._id': {$in: req.authDetails.models.map(e => mongoose.Types.ObjectId(e))}}, - {group: true, 'models.$': true} - ] - } - ModelModel.find(...conditions).lean().exec((err, data) => { - if (err) return next(err); + let conditions: any = [{}, {}]; + if (['dev', 'admin'].indexOf(req.authDetails.level) < 0) { // if not dev or admin, user has to possess model rights + conditions = [ + {'models._id': {$in: req.authDetails.models.map(e => mongoose.Types.ObjectId(e))}}, + {group: true, 'models.$': true} + ] + } + ModelModel.find(...conditions).lean().exec((err, data) => { + if (err) return next(err); - // validate all and filter null values from validation errors - res.json(_.compact(data.map(e => ModelValidate.output(e)))); - }); + // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => ModelValidate.output(e)))); + }); }); router.post('/model/:group', (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - const {error, value: model} = ModelValidate.input(req.body); - if (error) return res400(error, res); + const {error, value: model} = ModelValidate.input(req.body); + if (error) return res400(error, res); - ModelModel.findOne({group: req.params.group}).lean().exec((err, data) => { - if (err) return next(err); + ModelModel.findOne({group: req.params.group}).lean().exec((err, data) => { + if (err) return next(err); - if (data) { // group exists - if (data.models.find(e => e.name === model.name)) { // name exists, overwrite - ModelModel.findOneAndUpdate( - {$and: [{group: req.params.group}, {'models.name': model.name}]}, - {'models.$': model}, - {upsert: true}).log(req).lean().exec(err => { - if (err) return next(err); - res.json({status: 'OK'}) - }); - } - else { // create new - ModelModel.findOneAndUpdate( - {group: req.params.group}, - {$push: {models: model as never}} - ).log(req).lean().exec(err => { - if (err) return next(err); - res.json({status: 'OK'}); - }); - } - } - else { // create new group - new ModelModel({group: req.params.group, models: [model]}).save((err, data) => { - if (err) return next(err); - db.log(req, 'models', {_id: data._id}, data.toObject()); - res.json({status: 'OK'}); - }); - } - }); + if (data) { // group exists + if (data.models.find(e => e.name === model.name)) { // name exists, overwrite + ModelModel.findOneAndUpdate( + {$and: [{group: req.params.group}, {'models.name': model.name}]}, + {'models.$': model}, + {upsert: true}).log(req).lean().exec(err => { + if (err) return next(err); + res.json({status: 'OK'}) + }); + } + else { // create new + ModelModel.findOneAndUpdate( + {group: req.params.group}, + {$push: {models: model as never}} + ).log(req).lean().exec(err => { + if (err) return next(err); + res.json({status: 'OK'}); + }); + } + } + else { // create new group + new ModelModel({group: req.params.group, models: [model]}).save((err, data) => { + if (err) return next(err); + db.log(req, 'models', {_id: data._id}, data.toObject()); + res.json({status: 'OK'}); + }); + } + }); }); router.delete('/model/:group(((?!file)[^\\/]+?))/:name', (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - ModelModel.findOne({group: req.params.group}).lean().exec((err, data) => { - if (err) return next(err); + ModelModel.findOne({group: req.params.group}).lean().exec((err, data) => { + if (err) return next(err); - if (!data || !data.models.find(e => e.name === req.params.name)) { - return res.status(404).json({status: 'Not found'}); - } - // delete all references in user.models - UserModel.updateMany({}, {$pull: {models: data.models.find(e => e.name === req.params.name)._id as never}}, - { multi: true }).log(req).lean().exec(err => { - if (err) return next(err); - if (data.models.length > 1) { // only remove model - ModelModel.findOneAndUpdate( - {group: req.params.group}, - {$pull: {models: data.models.find(e => e.name === req.params.name) as never}} - ).log(req).lean().exec(err => { - if (err) return next(err); - res.json({status: 'OK'}) - }); - } - else { // remove document - ModelModel.findOneAndDelete({group: req.params.group}).log(req).lean().exec(err => { - if (err) return next(err); - res.json({status: 'OK'}) - }); - } - }); - }); + if (!data || !data.models.find(e => e.name === req.params.name)) { + return res.status(404).json({status: 'Not found'}); + } + // delete all references in user.models + UserModel.updateMany({}, {$pull: {models: data.models.find(e => e.name === req.params.name)._id as never}}, + { multi: true }).log(req).lean().exec(err => { + if (err) return next(err); + if (data.models.length > 1) { // only remove model + ModelModel.findOneAndUpdate( + {group: req.params.group}, + {$pull: {models: data.models.find(e => e.name === req.params.name) as never}} + ).log(req).lean().exec(err => { + if (err) return next(err); + res.json({status: 'OK'}) + }); + } + else { // remove document + ModelModel.findOneAndDelete({group: req.params.group}).log(req).lean().exec(err => { + if (err) return next(err); + res.json({status: 'OK'}) + }); + } + }); + }); }); router.get('/model/files', (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - ModelFileModel.find().exec((err, data) => { - if (err) return next(err); - res.json(data.map(e => ModelValidate.fileOutput(e))); - }); + ModelFileModel.find().exec((err, data) => { + if (err) return next(err); + res.json(data.map(e => ModelValidate.fileOutput(e))); + }); }); router.get('/model/file/:name', (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'all')) return; + if (!req.auth(res, ['dev', 'admin'], 'all')) return; - ModelFileModel.findOne({name: req.params.name}).lean().exec((err, data) => { - if (err) return next(err); - if (data) { - res.set('Content-Type', 'application/octet-stream'); - res.send(data.data.buffer); - } - else { - res.status(404).json({status: 'Not found'}); - } - }); + ModelFileModel.findOne({name: req.params.name}).lean().exec((err, data) => { + if (err) return next(err); + if (data) { + res.set('Content-Type', 'application/octet-stream'); + res.send(data.data.buffer); + } + else { + res.status(404).json({status: 'Not found'}); + } + }); }); router.post('/model/file/:name', bodyParser.raw({limit: '50mb'}), (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'all')) return; + if (!req.auth(res, ['dev', 'admin'], 'all')) return; - ModelFileModel.replaceOne({name: req.params.name}, {name: req.params.name, data: req.body}).setOptions({upsert: true}) - .lean().exec(err => { - if (err) return next(err); - res.json({status: 'OK'}); - }); + ModelFileModel.replaceOne({name: req.params.name}, {name: req.params.name, data: req.body}).setOptions({upsert: true}) + .lean().exec(err => { + if (err) return next(err); + res.json({status: 'OK'}); + }); }); router.delete('/model/file/:name', (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - ModelFileModel.findOneAndDelete({name: req.params.name}).lean().exec((err, data) => { - if (err) return next(err); - if (data) { - res.json({status: 'OK'}); - } - else { - res.status(404).json({status: 'Not found'}); - } - }); + ModelFileModel.findOneAndDelete({name: req.params.name}).lean().exec((err, data) => { + if (err) return next(err); + if (data) { + res.json({status: 'OK'}); + } + else { + res.status(404).json({status: 'Not found'}); + } + }); }); router.get('/model/authorized/:url', (req, res, next) => { - if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return; - if (['dev', 'admin'].indexOf(req.authDetails.level) < 0) { // if not dev or admin, user has to possess model rights - ModelModel.findOne({models: { $elemMatch: { - url: decodeURIComponent(req.params.url), - '_id': {$in: req.authDetails.models.map(e => mongoose.Types.ObjectId(e))} - }}}).lean().exec((err, data) => { - if (err) return next(err); - if (data) { - res.json({status: 'OK'}); - } - else { - res.status(403).json({status: 'Forbidden'}); - } - }); - } - else { - res.json({status: 'OK'}); - } + if (['dev', 'admin'].indexOf(req.authDetails.level) < 0) { // if not dev or admin, user has to possess model rights + ModelModel.findOne({models: { $elemMatch: { + url: decodeURIComponent(req.params.url), + '_id': {$in: req.authDetails.models.map(e => mongoose.Types.ObjectId(e))} + }}}).lean().exec((err, data) => { + if (err) return next(err); + if (data) { + res.json({status: 'OK'}); + } + else { + res.status(403).json({status: 'Forbidden'}); + } + }); + } + else { + res.json({status: 'OK'}); + } }); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/src/routes/root.spec.ts b/src/routes/root.spec.ts index 337fc77..d38546c 100644 --- a/src/routes/root.spec.ts +++ b/src/routes/root.spec.ts @@ -4,254 +4,254 @@ import db from '../db'; describe('/', () => { - let server; - before(done => TestHelper.before(done)); - beforeEach(done => server = TestHelper.beforeEach(server, done)); - afterEach(done => TestHelper.afterEach(server, done)); - after(done => TestHelper.after(done)); + let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); + after(done => TestHelper.after(done)); - describe('GET /', () => { - it('returns the root message', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/', - httpStatus: 200, - res: {status: 'API server up and running!'} - }); - }); - }); + describe('GET /', () => { + it('returns the root message', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/', + httpStatus: 200, + res: {status: 'API server up and running!'} + }); + }); + }); - describe('GET /changelog/{timestamp}/{page}/{pagesize}', () => { - it('returns the first page', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/changelog/120000030000000000000000/0/2', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).have.lengthOf(2); - should(res.body[0].date).be.eql('1979-07-28T06:04:51.000Z'); - should(res.body[1].date).be.eql('1979-07-28T06:04:50.000Z'); - should(res.body).matchEach(log => { - should(log).have.only.keys('_id', 'date', 'action', 'collection', 'conditions', 'data'); - should(log).have.property('_id').be.type('string'); - should(log).have.property('action', 'PUT /sample/400000000000000000000001'); - should(log).have.property('collection', 'samples'); - should(log).have.property('conditions', {_id: '400000000000000000000001'}); - should(log).have.property('data', {type: 'processed', status: 0}); - }); - done(); - }); - }); - it('returns another page', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/changelog/120000030000000000000000/1/2', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).have.lengthOf(1); - should(res.body[0].date).be.eql('1979-07-28T06:04:49.000Z'); - should(res.body).matchEach(log => { - should(log).have.only.keys('_id', 'date', 'action', 'collection', 'conditions', 'data'); - should(log).have.property('_id').be.type('string'); - should(log).have.property('action', 'PUT /sample/400000000000000000000001'); - should(log).have.property('collection', 'samples'); - should(log).have.property('conditions', {_id: '400000000000000000000001'}); - should(log).have.property('data', {type: 'processed', status: 0}); - done(); - }); - }); - }); - it('returns an empty array for a page with no results', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/changelog/120000030000000000000000/10/2', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).have.lengthOf(0); - done(); - }); - }); - it('rejects invalid ids', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/changelog/12000003000000h000000000/10/2', - auth: {basic: 'admin'}, - httpStatus: 400, - res: {status: 'Invalid body format', details: 'Invalid object id'} - }); - }); - it('rejects negative page numbers', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/changelog/120000030000000000000000/-10/2', - auth: {basic: 'admin'}, - httpStatus: 400, - res: {status: 'Invalid body format', details: '"page" must be greater than or equal to 0'} - }); - }); - it('rejects negative pagesizes', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/changelog/120000030000000000000000/10/-2', - auth: {basic: 'admin'}, - httpStatus: 400, - res: {status: 'Invalid body format', details: '"pagesize" must be greater than or equal to 0'} - }); - }); - it('rejects request from a write user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/changelog/120000030000000000000000/10/2', - auth: {basic: 'janedoe'}, - httpStatus: 403 - }); - }); - it('rejects requests from an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/changelog/120000030000000000000000/10/2', - auth: {key: 'admin'}, - httpStatus: 401 - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/changelog/120000030000000000000000/10/2', - httpStatus: 401 - }); - }); - }); + describe('GET /changelog/{timestamp}/{page}/{pagesize}', () => { + it('returns the first page', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/changelog/120000030000000000000000/0/2', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).have.lengthOf(2); + should(res.body[0].date).be.eql('1979-07-28T06:04:51.000Z'); + should(res.body[1].date).be.eql('1979-07-28T06:04:50.000Z'); + should(res.body).matchEach(log => { + should(log).have.only.keys('_id', 'date', 'action', 'collection', 'conditions', 'data'); + should(log).have.property('_id').be.type('string'); + should(log).have.property('action', 'PUT /sample/400000000000000000000001'); + should(log).have.property('collection', 'samples'); + should(log).have.property('conditions', {_id: '400000000000000000000001'}); + should(log).have.property('data', {type: 'processed', status: 0}); + }); + done(); + }); + }); + it('returns another page', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/changelog/120000030000000000000000/1/2', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).have.lengthOf(1); + should(res.body[0].date).be.eql('1979-07-28T06:04:49.000Z'); + should(res.body).matchEach(log => { + should(log).have.only.keys('_id', 'date', 'action', 'collection', 'conditions', 'data'); + should(log).have.property('_id').be.type('string'); + should(log).have.property('action', 'PUT /sample/400000000000000000000001'); + should(log).have.property('collection', 'samples'); + should(log).have.property('conditions', {_id: '400000000000000000000001'}); + should(log).have.property('data', {type: 'processed', status: 0}); + done(); + }); + }); + }); + it('returns an empty array for a page with no results', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/changelog/120000030000000000000000/10/2', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).have.lengthOf(0); + done(); + }); + }); + it('rejects invalid ids', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/changelog/12000003000000h000000000/10/2', + auth: {basic: 'admin'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: 'Invalid object id'} + }); + }); + it('rejects negative page numbers', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/changelog/120000030000000000000000/-10/2', + auth: {basic: 'admin'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: '"page" must be greater than or equal to 0'} + }); + }); + it('rejects negative pagesizes', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/changelog/120000030000000000000000/10/-2', + auth: {basic: 'admin'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: '"pagesize" must be greater than or equal to 0'} + }); + }); + it('rejects request from a write user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/changelog/120000030000000000000000/10/2', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('rejects requests from an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/changelog/120000030000000000000000/10/2', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/changelog/120000030000000000000000/10/2', + httpStatus: 401 + }); + }); + }); - describe('Unknown routes', () => { - it('return a 404 message', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/unknownroute', - httpStatus: 404 - }); - }); - }); + describe('Unknown routes', () => { + it('return a 404 message', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/unknownroute', + httpStatus: 404 + }); + }); + }); - describe('An unauthorized request', () => { - it('returns a 401 message', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/authorized', - httpStatus: 401 - }); - }); - it('does not work with correct username', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/authorized', - auth: {basic: {name: 'admin', pass: 'Abc123!!'}}, - httpStatus: 401 - }); - }); - it('does not work with incorrect username', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/authorized', - auth: {basic: {name: 'adminxx', pass: 'Abc123!!'}}, - httpStatus: 401 - }); - }); - it('does not work with a deleted user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/authorized', - auth: {basic: {name: 'customerold', pass: 'Xyz890*)'}}, - httpStatus: 401 - }); - }); - }); + describe('An unauthorized request', () => { + it('returns a 401 message', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/authorized', + httpStatus: 401 + }); + }); + it('does not work with correct username', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/authorized', + auth: {basic: {name: 'admin', pass: 'Abc123!!'}}, + httpStatus: 401 + }); + }); + it('does not work with incorrect username', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/authorized', + auth: {basic: {name: 'adminxx', pass: 'Abc123!!'}}, + httpStatus: 401 + }); + }); + it('does not work with a deleted user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/authorized', + auth: {basic: {name: 'customerold', pass: 'Xyz890*)'}}, + httpStatus: 401 + }); + }); + }); - describe('An authorized request', () => { - it('works with an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/authorized', - auth: {key: 'admin'}, - httpStatus: 200, - res: {status: 'Authorization successful', method: 'key', level: 'admin', user_id: '000000000000000000000003'} - }); - }); - it('works with basic auth', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/authorized', - auth: {basic: 'admin'}, - httpStatus: 200, - res: {status: 'Authorization successful', method: 'basic', level: 'admin', user_id: '000000000000000000000003'} - }); - }); - }); + describe('An authorized request', () => { + it('works with an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/authorized', + auth: {key: 'admin'}, + httpStatus: 200, + res: {status: 'Authorization successful', method: 'key', level: 'admin', user_id: '000000000000000000000003'} + }); + }); + it('works with basic auth', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/authorized', + auth: {basic: 'admin'}, + httpStatus: 200, + res: {status: 'Authorization successful', method: 'basic', level: 'admin', user_id: '000000000000000000000003'} + }); + }); + }); - describe('An invalid JSON body', () => { - it('is rejected', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/', - httpStatus: 400, - reqType: 'json', - req: '{"xxx"}', - res: {status: 'Invalid JSON body'} - }); + describe('An invalid JSON body', () => { + it('is rejected', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/', + httpStatus: 400, + reqType: 'json', + req: '{"xxx"}', + res: {status: 'Invalid JSON body'} + }); - }); - }); + }); + }); - // describe('A not connected database', () => { // RUN AS LAST OR RECONNECT DATABASE!! - // it('resolves to an 500 error', done => { - // db.disconnect(() => { - // TestHelper.request(server, done, { - // method: 'get', - // url: '/', - // httpStatus: 500 - // }); - // }); - // }); - // }); + // describe('A not connected database', () => { // RUN AS LAST OR RECONNECT DATABASE!! + // it('resolves to an 500 error', done => { + // db.disconnect(() => { + // TestHelper.request(server, done, { + // method: 'get', + // url: '/', + // httpStatus: 500 + // }); + // }); + // }); + // }); }); describe('The /api/{url} redirect', () => { - let server; - let counter = 0; // count number of current test method - before(done => { - process.env.port = '2999'; - db.connect('test', done); - }); - beforeEach(done => { - process.env.NODE_ENV = counter === 1 ? 'production' : 'test'; - counter ++; - server = TestHelper.beforeEach(server, done); - }); - afterEach(done => TestHelper.afterEach(server, done)); - after(done => TestHelper.after(done)); + let server; + let counter = 0; // count number of current test method + before(done => { + process.env.port = '2999'; + db.connect('test', done); + }); + beforeEach(done => { + process.env.NODE_ENV = counter === 1 ? 'production' : 'test'; + counter ++; + server = TestHelper.beforeEach(server, done); + }); + afterEach(done => TestHelper.afterEach(server, done)); + after(done => TestHelper.after(done)); - it('returns the right method', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/api/authorized', - auth: {basic: 'admin'}, - httpStatus: 200, - res: {status: 'Authorization successful', method: 'basic', level: 'admin', user_id: '000000000000000000000003'} - }); - }); - // it('is disabled in production', done => { - // TestHelper.request(server, done, { - // method: 'get', - // url: '/api/authorized', - // auth: {basic: 'admin'}, - // httpStatus: 404 - // }); - // }); -}); \ No newline at end of file + it('returns the right method', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/api/authorized', + auth: {basic: 'admin'}, + httpStatus: 200, + res: {status: 'Authorization successful', method: 'basic', level: 'admin', user_id: '000000000000000000000003'} + }); + }); + // it('is disabled in production', done => { + // TestHelper.request(server, done, { + // method: 'get', + // url: '/api/authorized', + // auth: {basic: 'admin'}, + // httpStatus: 404 + // }); + // }); +}); diff --git a/src/routes/root.ts b/src/routes/root.ts index 165da7e..86b76af 100644 --- a/src/routes/root.ts +++ b/src/routes/root.ts @@ -9,37 +9,37 @@ import _ from 'lodash'; const router = express.Router(); router.get('/', (req, res) => { - res.json({status: 'API server up and running!'}); + res.json({status: 'API server up and running!'}); }); router.get('/authorized', (req, res) => { - if (!req.auth(res, Object.values(globals.levels))) return; - res.json({ - status: 'Authorization successful', - method: req.authDetails.method, - level: req.authDetails.level, - user_id: req.authDetails.id - }); + if (!req.auth(res, Object.values(globals.levels))) return; + res.json({ + status: 'Authorization successful', + method: req.authDetails.method, + level: req.authDetails.level, + user_id: req.authDetails.id + }); }); router.get('/changelog/:id/:page?/:pagesize?', (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - const {error, value: options} = RootValidate.changelogParams({ - id: req.params.id, - page: req.params.page, - pagesize: req.params.pagesize - }); - if (error) return res400(error, res); + const {error, value: options} = RootValidate.changelogParams({ + id: req.params.id, + page: req.params.page, + pagesize: req.params.pagesize + }); + if (error) return res400(error, res); - ChangelogModel.find({_id: {$lte: mongoose.Types.ObjectId(options.id)}}) - .sort({_id: -1}).skip(options.page * options.pagesize).limit(options.pagesize) - .lean().exec((err, data) => { - if (err) return next(err); + ChangelogModel.find({_id: {$lte: mongoose.Types.ObjectId(options.id)}}) + .sort({_id: -1}).skip(options.page * options.pagesize).limit(options.pagesize) + .lean().exec((err, data) => { + if (err) return next(err); - // validate all and filter null values from validation errors - res.json(_.compact(data.map(e => RootValidate.changelogOutput(e)))); - }); + // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => RootValidate.changelogOutput(e)))); + }); }); module.exports = router; diff --git a/src/routes/sample.spec.ts b/src/routes/sample.spec.ts index 9dec0fa..38ccfe3 100644 --- a/src/routes/sample.spec.ts +++ b/src/routes/sample.spec.ts @@ -8,2286 +8,2286 @@ import mongoose from 'mongoose'; describe('/sample', () => { - let server; - before(done => TestHelper.before(done)); - beforeEach(done => server = TestHelper.beforeEach(server, done)); - afterEach(done => TestHelper.afterEach(server, done)); - after(done => TestHelper.after(done)); + let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); + after(done => TestHelper.after(done)); - describe('GET /samples', () => { - it('returns all samples', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status ==='validated').length); - should(res.body).matchEach(sample => { - should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'condition', 'material_id', 'note_id', 'user_id', 'added'); - 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('condition').be.type('object'); - should(sample.condition).have.property('condition_template').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'); - should(sample).have.property('added').be.type('string'); - }); - done(); - }); - }); - it('returns deleted samples for admin', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=deleted&fields[]=number&fields=status', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status ==='deleted').length); - should(res.body).matchEach(sample => { - should(sample).have.property('status', 'deleted').be.type('string'); - }); - done(); - }); - }); - it('works with an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples', - auth: {key: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status ==='validated').length); - should(res.body).matchEach(sample => { - should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'condition', 'material_id', 'note_id', 'user_id', 'added'); - 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('condition').be.type('object'); - should(sample.condition).have.property('condition_template').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'); - should(sample).have.property('added').be.type('string'); - }); - done(); - }); - }); - it('allows filtering by state', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status ==='new').length); - should(res.body).matchEach(sample => { - should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'condition', 'material_id', 'note_id', 'user_id', 'added'); - 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('condition').be.type('object'); - should(sample).have.property('material_id').be.type('string'); - should(sample).have.property('note_id'); - should(sample).have.property('user_id').be.type('string'); - should(sample).have.property('added').be.type('string'); - }); - done(); - }); - }); - it('uses the given page size', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&page-size=3', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).have.lengthOf(3); - done(); - }); - }); - it('returns results starting from first-id', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&from-id=400000000000000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body[0]).have.property('_id', '400000000000000000000002'); - should(res.body[1]).have.property('_id', '400000000000000000000003'); - done(); - }); - }); - it('returns the right page number', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&to-page=2&page-size=2', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body[0]).have.property('_id', '400000000000000000000006'); - done(); - }); - }); - it('works with negative page numbers', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&to-page=-1&page-size=2&from-id=400000000000000000000004', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body[0]).have.property('_id', '400000000000000000000002'); - should(res.body[1]).have.property('_id', '400000000000000000000003'); - done(); - }); - }); - it('returns an empty array for a page number out of range', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&to-page=100&page-size=2', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).have.lengthOf(0); - should(res.body).be.eql([]); - done(); - }); - }); - it('returns an empty array for a page number out of negative range', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&to-page=-100&page-size=3&from-id=400000000000000000000004', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).have.lengthOf(0); - should(res.body).be.eql([]); - done(); - }); - }); - it('sorts the samples ascending', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&sort=color-asc', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body[0]).have.property('color', 'black'); - should(res.body[res.body.length - 1]).have.property('color', 'natural'); - done(); - }); - }); - it('sorts the samples descending', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&sort=number-desc', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body[0]).have.property('number', 'Rng36'); - should(res.body[1]).have.property('number', '34'); - should(res.body[res.body.length - 1]).have.property('number', '1'); - done(); - }); - }); - it('sorts the samples correctly in combination with paging', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&sort=color-asc&page-size=2&from-id=400000000000000000000006', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body[0]).have.property('_id', '400000000000000000000006'); - should(res.body[1]).have.property('_id', '400000000000000000000007'); - done(); - }); - }); - it('sorts the samples correctly in combination with going pages backward', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&sort=color-desc&page-size=2&from-id=400000000000000000000004&to-page=-1', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body[0]).have.property('_id', '400000000000000000000007'); - should(res.body[1]).have.property('_id', '400000000000000000000006'); - done(); - }); - }); - it('sorts the samples correctly for material keys', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&sort=material.name-desc', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body[0]).have.property('_id', '400000000000000000000002'); - should(res.body[1]).have.property('_id', '400000000000000000000006'); - should(res.body[2]).have.property('_id', '400000000000000000000001'); - done(); - }); - }); - it('unwinds only once when filtering for spectrum measurements and including the dpt field', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=measurements.spectrum.device&fields[]=measurements.spectrum.filename&fields=measurements.spectrum.dpt&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22measurements.spectrum.device%22%2C%22values%22%3A%5B%22Alpha%20II%22%5D%7D', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.measurements.filter(e => e.values.device === 'Alpha II' && e.status !== 'deleted').length); - should(res.body).matchEach(sample => { - should(sample).have.only.keys('number', 'spectrum'); - should(sample.spectrum).have.only.keys('device', 'dpt', 'filename'); - should(sample.spectrum).have.property('device', 'Alpha II'); - }); - done(); - }); - }) - it('adds the status if specified', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=status', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body.find(e => e.number === '1')).have.property('status', 'validated'); - should(res.body.find(e => e.number === 'Rng36')).have.property('status', 'new'); - done(); - }); - }); - it('adds the specified measurements', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=measurements.kf.weight%20%25&fields[]=measurements.kf.standard%20deviation', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body.find(e => e.number === '1')).have.property('kf', {'weight %': null, 'standard deviation': null}); - should(res.body.find(e => e.number === 'Rng36')).have.property('kf', {'weight %': 0.6, 'standard deviation': null}); - done(); - }); - }); - it('multiplies the sample information for each spectrum', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=measurements.spectrum.dpt', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body.filter(e => e.spectrum.dpt)).have.lengthOf(3); - should(res.body[0].spectrum).have.property('dpt', [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]); - should(res.body[1].spectrum).have.property('dpt', [[3996.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]); - done(); - }); - }); - it('filters a sample property', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=type&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22type%22%2C%22values%22%3A%5B%22processed%22%5D%7D', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.samples.filter(e => e.type === 'processed').length); - should(res.body).matchEach(sample => { - should(sample).have.property('type', 'processed'); - }); - done(); - }); - }); - it('filters a material property', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=material.name&filters[]=%7B%22mode%22%3A%22in%22%2C%22field%22%3A%22material.name%22%2C%22values%22%3A%5B%22Schulamid%2066%20GF%2025%20H%22%2C%22Stanyl%20TW%20200%20F8%22%5D%7D', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.samples.filter(e => e.material_id == '100000000000000000000004' || e.material_id == '100000000000000000000001').length); - should(res.body).matchEach(sample => { - should(sample.material.name).be.equalOneOf('Schulamid 66 GF 25 H', 'Stanyl TW 200 F8'); - }); - done(); - }); - }); - it('filters by measurement value', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=material.name&fields[]=measurements.kf.weight%20%25&filters[]=%7B%22mode%22%3A%22gt%22%2C%22field%22%3A%22measurements.kf.weight%20%25%22%2C%22values%22%3A%5B0.5%5D%7D', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.measurements.filter(e => e.measurement_template == '300000000000000000000002' && e.values['weight %'] > 0.5).length); - should(res.body).matchEach(sample => { - should(sample.kf['weight %']).be.above(0.5); - }); - done(); - }); - }); - it('filters by measurement value not in the fields', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=material.name&filters[]=%7B%22mode%22%3A%22gt%22%2C%22field%22%3A%22measurements.kf.weight%20%25%22%2C%22values%22%3A%5B0.5%5D%7D', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.measurements.filter(e => e.measurement_template == '300000000000000000000002' && e.values['weight %'] > 0.5).length); - should(res.body[0]).have.property('number', 'Rng36'); - done(); - }); - }); - it('filters by a measurement properties property', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=material.name&fields[]=material.properties.glass_fiber&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22material.properties.glass_fiber%22%2C%22values%22%3A%5B%2225%22%5D%7D', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).have.lengthOf(2); - should(res.body).matchEach(sample => { - should(sample.material.properties.glass_fiber).be.eql(25); - }); - done(); - }); - }); - it('filters and sorts by a measurement properties property', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&sort=material.properties.glass_fiber-desc&fields[]=number&fields[]=material.name&fields[]=material.properties.glass_fiber&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22material.properties.glass_fiber%22%2C%22values%22%3A%5B%2225%22%5D%7D', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).have.lengthOf(2); - should(res.body[0].number).be.eql('Rng36'); - should(res.body[1].number).be.eql('1'); - should(res.body).matchEach(sample => { - should(sample.material.properties.glass_fiber).be.eql(25); - }); - done(); - }); - }); - it('filters multiple properties', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=batch&filters[]=%7B%22mode%22%3A%22lte%22%2C%22field%22%3A%22number%22%2C%22values%22%3A%5B%22Rng33%22%5D%7D&filters[]=%7B%22mode%22%3A%22nin%22%2C%22field%22%3A%22batch%22%2C%22values%22%3A%5B%221704-005%22%5D%7D', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).have.lengthOf(4); - should(res.body[0]).be.eql({number: '1', batch: ''}); - done(); - }); - }); - it('filters for empty comments', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=notes.comment&filters[]=%7B%22mode%22%3A%22in%22%2C%22field%22%3A%22notes.comment%22%2C%22values%22%3A%5Bnull%2C%22%22%5D%7D', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf( - json.collections.samples - .filter(e => e.status !== 'deleted') - .filter(e => e.note_id === null || json.collections.notes.find(el => el._id.toString() == e.note_id.toString()).comment === '') - .length - ); - should(res.body).matchEach(sample => { - should(sample.notes.comment).be.equalOneOf(null, ''); - }); - done(); - }); - }); - it('filters for empty conditions', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=condition&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22condition%22%2C%22values%22%3A%5B%7B%7D%5D%7D', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf( - json.collections.samples - .filter(e => e.status !== 'deleted') - .filter(e => Object.keys(e.condition).length === 0) - .length - ); - should(res.body).matchEach(sample => { - should(sample.condition).be.eql({}); - }); - done(); - }); - }); - it('filters for samples without measurements', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=_id&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22measurements%22%2C%22values%22%3A%5Bnull%5D%7D', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf( - json.collections.samples - .filter(e => e.status !== 'deleted') - .filter(e => !json.collections.measurements.find(el => el.sample_id.toString() === e._id.toString())) - .length - ); - should(res.body).matchEach(sample => { - should(json.collections.measurements.find(el => el.sample_id.toString() === sample._id)).be.eql(undefined); - }); - done(); - }); - }); - it('returns comment fields', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=notes.comment', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status !== 'deleted').length); - should(res.body).matchEach(sample => { - should(sample).have.only.keys('number', 'notes'); - should(sample.notes).have.only.keys('comment'); - }); - done(); - }); - }); - it('rejects returning spectral data for a write user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=measurements.spectrum.dpt', - auth: {basic: 'janedoe'}, - httpStatus: 403 - }); - }); - it('rejects an invalid JSON string as a filters parameter', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=material.glass_fiber&fields[]=batch&filters[]=xx', - auth: {basic: 'janedoe'}, - httpStatus: 400, - res: {status: 'Invalid body format', details: 'Invalid JSON string for filter parameter'} - }); - }); - it('rejects an invalid filter mode', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=batch&filters[]=%7B%22mode%22%3A%22xx%22%2C%22field%22%3A%22batch%22%2C%22values%22%3A%5B%221704-005%22%5D%7D', - auth: {basic: 'janedoe'}, - httpStatus: 400, - res: {status: 'Invalid body format', details: '"filters[0].mode" must be one of [eq, ne, lt, lte, gt, gte, in, nin, stringin]'} - }); - }); - it('rejects an filter field not existing', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=material.glass_fiber&fields[]=batch&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22xx%22%2C%22values%22%3A%5B%221704-005%22%5D%7D', - auth: {basic: 'janedoe'}, - httpStatus: 400, - res: {status: 'Invalid body format', details: 'Invalid JSON string for filter parameter'} - }); - }); - it('rejects unknown measurement names', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=measurements.xx', - auth: {basic: 'janedoe'}, - httpStatus: 400, - res: {status: 'Invalid body format', details: 'Measurement key not found'} - }); - }); - it('returns a correct csv file for admins if specified', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&page-size=2&output=csv', - contentType: /text\/csv/, - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.text).be.eql('"_id","number","type","color","batch","condition.material","condition.weeks","condition.condition_template","material_id","note_id","user_id","added"\r\n' + - '"400000000000000000000001","1","as-delivered/raw","black","","copper",3,"200000000000000000000001","100000000000000000000004",,"000000000000000000000002","2004-01-10T13:37:04.000Z"\r\n' + - '"400000000000000000000002","21","as-delivered/raw","natural","1560237365","copper",3,"200000000000000000000001","100000000000000000000001","500000000000000000000001","000000000000000000000002","2004-01-10T13:37:04.000Z"'); - done(); - }); - }); - it('rejects returning a csv file for a write user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&page-size=2&output=csv', - auth: {basic: 'janedoe'}, - httpStatus: 403 - }); - }); - it('returns the object flattened if specified', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=measurements.spectrum.device&fields[]=measurements.spectrum.dpt&page-size=1&output=flatten', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body[0]).have.only.keys('number', 'spectrum.device', 'spectrum.dpt.labels', 'spectrum.dpt.values'); - should(res.body[0]).have.property('number', '1'); - should(res.body[0]).have.property('spectrum.device', 'Alpha I'); - should(res.body[0]).have.property('spectrum.dpt.labels', [3997.12558, 3995.08519, 3993.0448]); - should(res.body[0]).have.property('spectrum.dpt.values', [98.00555, 98.03253, 98.02657]); - done(); - }); - }); - it('returns only the fields specified', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&page-size=1&fields[]=number&fields[]=condition&fields[]=color&fields[]=material.name&fields[]=material.supplier', - auth: {basic: 'janedoe'}, - httpStatus: 200, - res: [{number: '1', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, color: 'black', material: {name: 'Schulamid 66 GF 25 H', supplier: 'Schulmann'}}] - }); - }); - it('returns specified material properties fields', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=material.properties.glass_fiber&fields[]=material.name', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).matchEach(sample => { - const materialId = json.collections.samples.find(e => e.number === sample.number).material_id; - const material = json.collections.materials.find(e => e._id.toString() == materialId); - should(sample).have.only.keys('number', 'material'); - should(sample.material.name).be.eql(material.name); - should(sample.material.properties.glass_fiber).be.eql(material.properties.glass_fiber); - }); - done() - }); - }); - it('rejects a from-id not in the database', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?from-id=5ea0450ed851c30a90e70894&sort=color-asc', - auth: {basic: 'admin'}, - httpStatus: 400, - res: {status: 'Invalid body format', details: 'from-id not found'} - }); - }); - it('rejects an invalid fields parameter', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&page-size=1&fields=number', - auth: {basic: 'janedoe'}, - httpStatus: 400, - res: {status: 'Invalid body format', details: '"fields" must be an array'} - }); - }); - it('rejects an unknown field name', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=new&status[]=validated&page-size=1&fields[]=xx', - auth: {basic: 'janedoe'}, - httpStatus: 400, - res: {status: 'Invalid body format', details: 'Invalid field name'} - }); - }); - it('rejects a negative page size', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?page-size=-3', - auth: {basic: 'janedoe'}, - httpStatus: 400, - res: {status: 'Invalid body format', details: '"page-size" must be greater than or equal to 1'} - }); - }); - it('rejects an invalid from-id', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?from-id=40000000000h000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 400, - res: {status: 'Invalid body format', details: 'Invalid object id'} - }); - }); - it('rejects a to-page without page-size', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?to-page=3', - auth: {basic: 'janedoe'}, - httpStatus: 400, - res: {status: 'Invalid body format', details: '"to-page" missing required peer "page-size"'} - }); - }); - it('rejects an invalid state name', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples?status[]=xxx', - auth: {basic: 'janedoe'}, - httpStatus: 400, - res: {status: 'Invalid body format', details: '"status[0]" must be one of [validated, new]'} - }); - }); - it('rejects requests from a predict user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples', - auth: {basic: 'customer'}, - httpStatus: 403 - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples', - httpStatus: 401 - }); - }); - }); + describe('GET /samples', () => { + it('returns all samples', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status ==='validated').length); + should(res.body).matchEach(sample => { + should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'condition', 'material_id', 'note_id', 'user_id', 'added'); + 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('condition').be.type('object'); + should(sample.condition).have.property('condition_template').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'); + should(sample).have.property('added').be.type('string'); + }); + done(); + }); + }); + it('returns deleted samples for admin', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=deleted&fields[]=number&fields=status', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status ==='deleted').length); + should(res.body).matchEach(sample => { + should(sample).have.property('status', 'deleted').be.type('string'); + }); + done(); + }); + }); + it('works with an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples', + auth: {key: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status ==='validated').length); + should(res.body).matchEach(sample => { + should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'condition', 'material_id', 'note_id', 'user_id', 'added'); + 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('condition').be.type('object'); + should(sample.condition).have.property('condition_template').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'); + should(sample).have.property('added').be.type('string'); + }); + done(); + }); + }); + it('allows filtering by state', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status ==='new').length); + should(res.body).matchEach(sample => { + should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'condition', 'material_id', 'note_id', 'user_id', 'added'); + 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('condition').be.type('object'); + should(sample).have.property('material_id').be.type('string'); + should(sample).have.property('note_id'); + should(sample).have.property('user_id').be.type('string'); + should(sample).have.property('added').be.type('string'); + }); + done(); + }); + }); + it('uses the given page size', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&page-size=3', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).have.lengthOf(3); + done(); + }); + }); + it('returns results starting from first-id', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&from-id=400000000000000000000002', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body[0]).have.property('_id', '400000000000000000000002'); + should(res.body[1]).have.property('_id', '400000000000000000000003'); + done(); + }); + }); + it('returns the right page number', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&to-page=2&page-size=2', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body[0]).have.property('_id', '400000000000000000000006'); + done(); + }); + }); + it('works with negative page numbers', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&to-page=-1&page-size=2&from-id=400000000000000000000004', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body[0]).have.property('_id', '400000000000000000000002'); + should(res.body[1]).have.property('_id', '400000000000000000000003'); + done(); + }); + }); + it('returns an empty array for a page number out of range', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&to-page=100&page-size=2', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).have.lengthOf(0); + should(res.body).be.eql([]); + done(); + }); + }); + it('returns an empty array for a page number out of negative range', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&to-page=-100&page-size=3&from-id=400000000000000000000004', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).have.lengthOf(0); + should(res.body).be.eql([]); + done(); + }); + }); + it('sorts the samples ascending', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&sort=color-asc', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body[0]).have.property('color', 'black'); + should(res.body[res.body.length - 1]).have.property('color', 'natural'); + done(); + }); + }); + it('sorts the samples descending', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&sort=number-desc', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body[0]).have.property('number', 'Rng36'); + should(res.body[1]).have.property('number', '34'); + should(res.body[res.body.length - 1]).have.property('number', '1'); + done(); + }); + }); + it('sorts the samples correctly in combination with paging', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&sort=color-asc&page-size=2&from-id=400000000000000000000006', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body[0]).have.property('_id', '400000000000000000000006'); + should(res.body[1]).have.property('_id', '400000000000000000000007'); + done(); + }); + }); + it('sorts the samples correctly in combination with going pages backward', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&sort=color-desc&page-size=2&from-id=400000000000000000000004&to-page=-1', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body[0]).have.property('_id', '400000000000000000000007'); + should(res.body[1]).have.property('_id', '400000000000000000000006'); + done(); + }); + }); + it('sorts the samples correctly for material keys', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&sort=material.name-desc', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body[0]).have.property('_id', '400000000000000000000002'); + should(res.body[1]).have.property('_id', '400000000000000000000006'); + should(res.body[2]).have.property('_id', '400000000000000000000001'); + done(); + }); + }); + it('unwinds only once when filtering for spectrum measurements and including the dpt field', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=measurements.spectrum.device&fields[]=measurements.spectrum.filename&fields=measurements.spectrum.dpt&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22measurements.spectrum.device%22%2C%22values%22%3A%5B%22Alpha%20II%22%5D%7D', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.measurements.filter(e => e.values.device === 'Alpha II' && e.status !== 'deleted').length); + should(res.body).matchEach(sample => { + should(sample).have.only.keys('number', 'spectrum'); + should(sample.spectrum).have.only.keys('device', 'dpt', 'filename'); + should(sample.spectrum).have.property('device', 'Alpha II'); + }); + done(); + }); + }) + it('adds the status if specified', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=status', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body.find(e => e.number === '1')).have.property('status', 'validated'); + should(res.body.find(e => e.number === 'Rng36')).have.property('status', 'new'); + done(); + }); + }); + it('adds the specified measurements', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=measurements.kf.weight%20%25&fields[]=measurements.kf.standard%20deviation', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body.find(e => e.number === '1')).have.property('kf', {'weight %': null, 'standard deviation': null}); + should(res.body.find(e => e.number === 'Rng36')).have.property('kf', {'weight %': 0.6, 'standard deviation': null}); + done(); + }); + }); + it('multiplies the sample information for each spectrum', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=measurements.spectrum.dpt', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body.filter(e => e.spectrum.dpt)).have.lengthOf(3); + should(res.body[0].spectrum).have.property('dpt', [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]); + should(res.body[1].spectrum).have.property('dpt', [[3996.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]); + done(); + }); + }); + it('filters a sample property', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=type&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22type%22%2C%22values%22%3A%5B%22processed%22%5D%7D', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.samples.filter(e => e.type === 'processed').length); + should(res.body).matchEach(sample => { + should(sample).have.property('type', 'processed'); + }); + done(); + }); + }); + it('filters a material property', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=material.name&filters[]=%7B%22mode%22%3A%22in%22%2C%22field%22%3A%22material.name%22%2C%22values%22%3A%5B%22Schulamid%2066%20GF%2025%20H%22%2C%22Stanyl%20TW%20200%20F8%22%5D%7D', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.samples.filter(e => e.material_id == '100000000000000000000004' || e.material_id == '100000000000000000000001').length); + should(res.body).matchEach(sample => { + should(sample.material.name).be.equalOneOf('Schulamid 66 GF 25 H', 'Stanyl TW 200 F8'); + }); + done(); + }); + }); + it('filters by measurement value', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=material.name&fields[]=measurements.kf.weight%20%25&filters[]=%7B%22mode%22%3A%22gt%22%2C%22field%22%3A%22measurements.kf.weight%20%25%22%2C%22values%22%3A%5B0.5%5D%7D', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.measurements.filter(e => e.measurement_template == '300000000000000000000002' && e.values['weight %'] > 0.5).length); + should(res.body).matchEach(sample => { + should(sample.kf['weight %']).be.above(0.5); + }); + done(); + }); + }); + it('filters by measurement value not in the fields', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=material.name&filters[]=%7B%22mode%22%3A%22gt%22%2C%22field%22%3A%22measurements.kf.weight%20%25%22%2C%22values%22%3A%5B0.5%5D%7D', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.measurements.filter(e => e.measurement_template == '300000000000000000000002' && e.values['weight %'] > 0.5).length); + should(res.body[0]).have.property('number', 'Rng36'); + done(); + }); + }); + it('filters by a measurement properties property', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=material.name&fields[]=material.properties.glass_fiber&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22material.properties.glass_fiber%22%2C%22values%22%3A%5B%2225%22%5D%7D', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).have.lengthOf(2); + should(res.body).matchEach(sample => { + should(sample.material.properties.glass_fiber).be.eql(25); + }); + done(); + }); + }); + it('filters and sorts by a measurement properties property', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&sort=material.properties.glass_fiber-desc&fields[]=number&fields[]=material.name&fields[]=material.properties.glass_fiber&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22material.properties.glass_fiber%22%2C%22values%22%3A%5B%2225%22%5D%7D', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).have.lengthOf(2); + should(res.body[0].number).be.eql('Rng36'); + should(res.body[1].number).be.eql('1'); + should(res.body).matchEach(sample => { + should(sample.material.properties.glass_fiber).be.eql(25); + }); + done(); + }); + }); + it('filters multiple properties', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=batch&filters[]=%7B%22mode%22%3A%22lte%22%2C%22field%22%3A%22number%22%2C%22values%22%3A%5B%22Rng33%22%5D%7D&filters[]=%7B%22mode%22%3A%22nin%22%2C%22field%22%3A%22batch%22%2C%22values%22%3A%5B%221704-005%22%5D%7D', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).have.lengthOf(4); + should(res.body[0]).be.eql({number: '1', batch: ''}); + done(); + }); + }); + it('filters for empty comments', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=notes.comment&filters[]=%7B%22mode%22%3A%22in%22%2C%22field%22%3A%22notes.comment%22%2C%22values%22%3A%5Bnull%2C%22%22%5D%7D', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf( + json.collections.samples + .filter(e => e.status !== 'deleted') + .filter(e => e.note_id === null || json.collections.notes.find(el => el._id.toString() == e.note_id.toString()).comment === '') + .length + ); + should(res.body).matchEach(sample => { + should(sample.notes.comment).be.equalOneOf(null, ''); + }); + done(); + }); + }); + it('filters for empty conditions', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=condition&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22condition%22%2C%22values%22%3A%5B%7B%7D%5D%7D', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf( + json.collections.samples + .filter(e => e.status !== 'deleted') + .filter(e => Object.keys(e.condition).length === 0) + .length + ); + should(res.body).matchEach(sample => { + should(sample.condition).be.eql({}); + }); + done(); + }); + }); + it('filters for samples without measurements', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=_id&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22measurements%22%2C%22values%22%3A%5Bnull%5D%7D', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf( + json.collections.samples + .filter(e => e.status !== 'deleted') + .filter(e => !json.collections.measurements.find(el => el.sample_id.toString() === e._id.toString())) + .length + ); + should(res.body).matchEach(sample => { + should(json.collections.measurements.find(el => el.sample_id.toString() === sample._id)).be.eql(undefined); + }); + done(); + }); + }); + it('returns comment fields', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=notes.comment', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status !== 'deleted').length); + should(res.body).matchEach(sample => { + should(sample).have.only.keys('number', 'notes'); + should(sample.notes).have.only.keys('comment'); + }); + done(); + }); + }); + it('rejects returning spectral data for a write user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=measurements.spectrum.dpt', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('rejects an invalid JSON string as a filters parameter', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=material.glass_fiber&fields[]=batch&filters[]=xx', + auth: {basic: 'janedoe'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: 'Invalid JSON string for filter parameter'} + }); + }); + it('rejects an invalid filter mode', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=batch&filters[]=%7B%22mode%22%3A%22xx%22%2C%22field%22%3A%22batch%22%2C%22values%22%3A%5B%221704-005%22%5D%7D', + auth: {basic: 'janedoe'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: '"filters[0].mode" must be one of [eq, ne, lt, lte, gt, gte, in, nin, stringin]'} + }); + }); + it('rejects an filter field not existing', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=material.glass_fiber&fields[]=batch&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22xx%22%2C%22values%22%3A%5B%221704-005%22%5D%7D', + auth: {basic: 'janedoe'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: 'Invalid JSON string for filter parameter'} + }); + }); + it('rejects unknown measurement names', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=measurements.xx', + auth: {basic: 'janedoe'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: 'Measurement key not found'} + }); + }); + it('returns a correct csv file for admins if specified', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&page-size=2&output=csv', + contentType: /text\/csv/, + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.text).be.eql('"_id","number","type","color","batch","condition.material","condition.weeks","condition.condition_template","material_id","note_id","user_id","added"\r\n' + + '"400000000000000000000001","1","as-delivered/raw","black","","copper",3,"200000000000000000000001","100000000000000000000004",,"000000000000000000000002","2004-01-10T13:37:04.000Z"\r\n' + + '"400000000000000000000002","21","as-delivered/raw","natural","1560237365","copper",3,"200000000000000000000001","100000000000000000000001","500000000000000000000001","000000000000000000000002","2004-01-10T13:37:04.000Z"'); + done(); + }); + }); + it('rejects returning a csv file for a write user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&page-size=2&output=csv', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('returns the object flattened if specified', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=measurements.spectrum.device&fields[]=measurements.spectrum.dpt&page-size=1&output=flatten', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body[0]).have.only.keys('number', 'spectrum.device', 'spectrum.dpt.labels', 'spectrum.dpt.values'); + should(res.body[0]).have.property('number', '1'); + should(res.body[0]).have.property('spectrum.device', 'Alpha I'); + should(res.body[0]).have.property('spectrum.dpt.labels', [3997.12558, 3995.08519, 3993.0448]); + should(res.body[0]).have.property('spectrum.dpt.values', [98.00555, 98.03253, 98.02657]); + done(); + }); + }); + it('returns only the fields specified', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&page-size=1&fields[]=number&fields[]=condition&fields[]=color&fields[]=material.name&fields[]=material.supplier', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: [{number: '1', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, color: 'black', material: {name: 'Schulamid 66 GF 25 H', supplier: 'Schulmann'}}] + }); + }); + it('returns specified material properties fields', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=material.properties.glass_fiber&fields[]=material.name', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).matchEach(sample => { + const materialId = json.collections.samples.find(e => e.number === sample.number).material_id; + const material = json.collections.materials.find(e => e._id.toString() == materialId); + should(sample).have.only.keys('number', 'material'); + should(sample.material.name).be.eql(material.name); + should(sample.material.properties.glass_fiber).be.eql(material.properties.glass_fiber); + }); + done() + }); + }); + it('rejects a from-id not in the database', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?from-id=5ea0450ed851c30a90e70894&sort=color-asc', + auth: {basic: 'admin'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: 'from-id not found'} + }); + }); + it('rejects an invalid fields parameter', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&page-size=1&fields=number', + auth: {basic: 'janedoe'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: '"fields" must be an array'} + }); + }); + it('rejects an unknown field name', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=new&status[]=validated&page-size=1&fields[]=xx', + auth: {basic: 'janedoe'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: 'Invalid field name'} + }); + }); + it('rejects a negative page size', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?page-size=-3', + auth: {basic: 'janedoe'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: '"page-size" must be greater than or equal to 1'} + }); + }); + it('rejects an invalid from-id', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?from-id=40000000000h000000000002', + auth: {basic: 'janedoe'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: 'Invalid object id'} + }); + }); + it('rejects a to-page without page-size', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?to-page=3', + auth: {basic: 'janedoe'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: '"to-page" missing required peer "page-size"'} + }); + }); + it('rejects an invalid state name', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status[]=xxx', + auth: {basic: 'janedoe'}, + httpStatus: 400, + res: {status: 'Invalid body format', details: '"status[0]" must be one of [validated, new]'} + }); + }); + it('rejects requests from a predict user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples', + auth: {basic: 'customer'}, + httpStatus: 403 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples', + httpStatus: 401 + }); + }); + }); - describe('GET /samples/{state}', () => { - 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 ==='new').length); - should(res.body).matchEach(sample => { - should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'condition', 'material_id', 'note_id', 'user_id', 'status', 'added'); - 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('condition').be.type('object'); - if (Object.keys(sample.condition).length > 0) { - should(sample.condition).have.property('condition_template').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'); - should(sample).have.property('added').be.type('string'); - should(sample).have.property('status').be.type('string'); - SampleModel.findById(sample._id).lean().exec((err, data) => { - should(data).have.property('status','new'); - if (--asyncCounter === 0) { - 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 === 'deleted').length); - should(res.body).matchEach(sample => { - should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'condition', 'material_id', 'note_id', 'user_id', 'status', 'added'); - 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('condition').be.type('object'); - should(sample.condition).have.property('condition_template').be.type('string'); - should(sample.condition).have.property('condition_template').be.type('string'); - should(sample.condition).have.property('condition_template').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'); - should(sample).have.property('added').be.type('string'); - should(sample).have.property('status').be.type('string'); - SampleModel.findById(sample._id).lean().exec((err, data) => { - should(data).have.property('status','deleted'); - if (--asyncCounter === 0) { - 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('GET /samples/{state}', () => { + 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 ==='new').length); + should(res.body).matchEach(sample => { + should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'condition', 'material_id', 'note_id', 'user_id', 'status', 'added'); + 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('condition').be.type('object'); + if (Object.keys(sample.condition).length > 0) { + should(sample.condition).have.property('condition_template').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'); + should(sample).have.property('added').be.type('string'); + should(sample).have.property('status').be.type('string'); + SampleModel.findById(sample._id).lean().exec((err, data) => { + should(data).have.property('status','new'); + if (--asyncCounter === 0) { + 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 === 'deleted').length); + should(res.body).matchEach(sample => { + should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'condition', 'material_id', 'note_id', 'user_id', 'status', 'added'); + 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('condition').be.type('object'); + should(sample.condition).have.property('condition_template').be.type('string'); + should(sample.condition).have.property('condition_template').be.type('string'); + should(sample.condition).have.property('condition_template').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'); + should(sample).have.property('added').be.type('string'); + should(sample).have.property('status').be.type('string'); + SampleModel.findById(sample._id).lean().exec((err, data) => { + should(data).have.property('status','deleted'); + if (--asyncCounter === 0) { + 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('GET /samples/count', () => { - it('returns the correct number of samples', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples/count', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body.count).be.eql(json.collections.samples.length); - done(); - }); - }); - it('works with an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples/count', - auth: {key: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body.count).be.eql(json.collections.samples.length); - done(); - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/samples/count', - httpStatus: 401 - }); - }); - }); + describe('GET /samples/count', () => { + it('returns the correct number of samples', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples/count', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body.count).be.eql(json.collections.samples.length); + done(); + }); + }); + it('works with an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples/count', + auth: {key: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body.count).be.eql(json.collections.samples.length); + done(); + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples/count', + httpStatus: 401 + }); + }); + }); - describe('GET /sample/{id}', () => { - it('returns the right sample', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/400000000000000000000003', - auth: {basic: 'janedoe'}, - httpStatus: 200, - res: {_id: '400000000000000000000003', number: '33', type: 'processed', color: 'black', batch: '1704-005', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 33, carbon_fiber: 0}, numbers: ['5514262406']}, notes: {comment: '', sample_references: [{sample_id: '400000000000000000000004', relation: 'as-delivered/raw to sample'}], custom_fields: {'not allowed for new applications': true}}, measurements: [{_id: '800000000000000000000003', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003'}], status: 'new', user: 'admin'} - }); - }); - it('works with an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/400000000000000000000003', - auth: {key: 'janedoe'}, - httpStatus: 200, - res: {_id: '400000000000000000000003', number: '33', type: 'processed', color: 'black', batch: '1704-005', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 33, carbon_fiber: 0}, numbers: ['5514262406']}, notes: {comment: '', sample_references: [{sample_id: '400000000000000000000004', relation: 'as-delivered/raw to sample'}], custom_fields: {'not allowed for new applications': true}}, measurements: [{_id: '800000000000000000000003', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003'}], status: 'new', user: 'admin'} - }); - }); - it ('filters out spectral data for a write user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - res: {_id: '400000000000000000000001', number: '1', type: 'as-delivered/raw', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {numbers: ['5513933405'], _id: '100000000000000000000004', name: 'Schulamid 66 GF 25 H', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 25, carbon_fiber: 0}, group: 'PA66', supplier: 'Schulmann'}, user: 'janedoe', notes: {}, measurements: [{_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000007', sample_id: '400000000000000000000001', values: {device: 'Alpha II', filename: '1_1.DPT'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000009', sample_id: '400000000000000000000001', values: {device: 'Alpha II', filename: '1_2.DPT'}, measurement_template: '300000000000000000000001'}], status: 'validated'} - }); - }); - it ('returns spectral data for an admin user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/400000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - res: {_id: '400000000000000000000001', number: '1', type: 'as-delivered/raw', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {numbers: ['5513933405'], _id: '100000000000000000000004', name: 'Schulamid 66 GF 25 H', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 25, carbon_fiber: 0}, group: 'PA66', supplier: 'Schulmann'}, user: 'janedoe', notes: {}, measurements: [{_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[ 3997.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]],device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000007', sample_id: '400000000000000000000001', values: {dpt: [[ 3996.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]], device: 'Alpha II', filename: '1_1.DPT'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000009', sample_id: '400000000000000000000001', values: {dpt: [[ 3996.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]], device: 'Alpha II', filename: '1_2.DPT'}, measurement_template: '300000000000000000000001'}], status: 'validated'} - }); - }); - it('returns a deleted sample for a dev/admin user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/400000000000000000000005', - auth: {basic: 'admin'}, - httpStatus: 200, - res: {_id: '400000000000000000000005', number: 'Rng33', type: 'as-delivered/raw', color: 'black', batch: '1653000308', condition: {condition_template: '200000000000000000000003'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 33, carbon_fiber: 0}, numbers: ['5514262406']}, notes: {}, measurements: [], status: 'deleted', user: 'admin'} - }); - }); - it('returns 403 for a write user when requesting a deleted sample', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/400000000000000000000005', - auth: {basic: 'janedoe'}, - httpStatus: 403 - }); - }); - it('returns 404 for an unknown sample', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/000000000000000000000005', - auth: {basic: 'janedoe'}, - httpStatus: 404 - }); - }); - it('rejects an invalid id', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/400000000h00000000000005', - auth: {basic: 'janedoe'}, - httpStatus: 404 - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/400000000000000000000005', - httpStatus: 401 - }); - }); - }); + describe('GET /sample/{id}', () => { + it('returns the right sample', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/400000000000000000000003', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {_id: '400000000000000000000003', number: '33', type: 'processed', color: 'black', batch: '1704-005', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 33, carbon_fiber: 0}, numbers: ['5514262406']}, notes: {comment: '', sample_references: [{sample_id: '400000000000000000000004', relation: 'as-delivered/raw to sample'}], custom_fields: {'not allowed for new applications': true}}, measurements: [{_id: '800000000000000000000003', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003'}], status: 'new', user: 'admin'} + }); + }); + it('works with an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/400000000000000000000003', + auth: {key: 'janedoe'}, + httpStatus: 200, + res: {_id: '400000000000000000000003', number: '33', type: 'processed', color: 'black', batch: '1704-005', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 33, carbon_fiber: 0}, numbers: ['5514262406']}, notes: {comment: '', sample_references: [{sample_id: '400000000000000000000004', relation: 'as-delivered/raw to sample'}], custom_fields: {'not allowed for new applications': true}}, measurements: [{_id: '800000000000000000000003', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003'}], status: 'new', user: 'admin'} + }); + }); + it ('filters out spectral data for a write user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {_id: '400000000000000000000001', number: '1', type: 'as-delivered/raw', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {numbers: ['5513933405'], _id: '100000000000000000000004', name: 'Schulamid 66 GF 25 H', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 25, carbon_fiber: 0}, group: 'PA66', supplier: 'Schulmann'}, user: 'janedoe', notes: {}, measurements: [{_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000007', sample_id: '400000000000000000000001', values: {device: 'Alpha II', filename: '1_1.DPT'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000009', sample_id: '400000000000000000000001', values: {device: 'Alpha II', filename: '1_2.DPT'}, measurement_template: '300000000000000000000001'}], status: 'validated'} + }); + }); + it ('returns spectral data for an admin user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/400000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + res: {_id: '400000000000000000000001', number: '1', type: 'as-delivered/raw', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {numbers: ['5513933405'], _id: '100000000000000000000004', name: 'Schulamid 66 GF 25 H', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 25, carbon_fiber: 0}, group: 'PA66', supplier: 'Schulmann'}, user: 'janedoe', notes: {}, measurements: [{_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[ 3997.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]],device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000007', sample_id: '400000000000000000000001', values: {dpt: [[ 3996.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]], device: 'Alpha II', filename: '1_1.DPT'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000009', sample_id: '400000000000000000000001', values: {dpt: [[ 3996.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]], device: 'Alpha II', filename: '1_2.DPT'}, measurement_template: '300000000000000000000001'}], status: 'validated'} + }); + }); + it('returns a deleted sample for a dev/admin user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/400000000000000000000005', + auth: {basic: 'admin'}, + httpStatus: 200, + res: {_id: '400000000000000000000005', number: 'Rng33', type: 'as-delivered/raw', color: 'black', batch: '1653000308', condition: {condition_template: '200000000000000000000003'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 33, carbon_fiber: 0}, numbers: ['5514262406']}, notes: {}, measurements: [], status: 'deleted', user: 'admin'} + }); + }); + it('returns 403 for a write user when requesting a deleted sample', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/400000000000000000000005', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('returns 404 for an unknown sample', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/000000000000000000000005', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/400000000h00000000000005', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/400000000000000000000005', + httpStatus: 401 + }); + }); + }); - describe('PUT /sample/{id}', () => { - it('returns the right sample', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {}, - res: {_id: '400000000000000000000001', number: '1', type: 'as-delivered/raw', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002', status: 'validated', added: '2004-01-10T13:37:04.000Z'} - }); - }); - it('keeps unchanged properties', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {type: 'as-delivered/raw', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000004', notes: {}} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({_id: '400000000000000000000001', number: '1', type: 'as-delivered/raw', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002', status: 'validated', added: '2004-01-10T13:37:04.000Z'}); - SampleModel.findById('400000000000000000000001').lean().exec((err, data: any) => { - if (err) return done (err); - should(data).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'condition', 'material_id', 'note_id', 'user_id', 'status', '__v'); - should(data).have.property('_id'); - should(data).have.property('number', '1'); - should(data).have.property('color', 'black'); - should(data).have.property('type', 'as-delivered/raw'); - should(data).have.property('batch', ''); - should(data).have.property('condition', {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}); - should(data.material_id.toString()).be.eql('100000000000000000000004'); - should(data.user_id.toString()).be.eql('000000000000000000000002'); - should(data).have.property('status','validated'); - should(data).have.property('note_id', null); - done(); - }); - }); - }); - it('keeps only one unchanged parameter', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {type: 'as-delivered/raw'} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({_id: '400000000000000000000001', number: '1', type: 'as-delivered/raw', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002', status: 'validated', added: '2004-01-10T13:37:04.000Z'}); - SampleModel.findById('400000000000000000000001').lean().exec((err, data: any) => { - if (err) return done (err); - should(data).have.property('status','validated'); - done(); - }); - }); - }); - it('keeps an unchanged condition', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({_id: '400000000000000000000001', number: '1', type: 'as-delivered/raw', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002', status: 'validated', added: '2004-01-10T13:37:04.000Z'}); - SampleModel.findById('400000000000000000000001').lean().exec((err, data: any) => { - if (err) return done (err); - should(data).have.property('status','validated'); - done(); - }); - }); - }); - it('keeps unchanged notes', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {notes: {comment: 'Stoff gesperrt', sample_references: []}} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({_id: '400000000000000000000002', number: '21', type: 'as-delivered/raw', color: 'natural', batch: '1560237365', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', note_id: '500000000000000000000001', user_id: '000000000000000000000002', status: 'validated', added: '2004-01-10T13:37:04.000Z'}); - SampleModel.findById('400000000000000000000002').lean().exec((err, data: any) => { - if (err) return done (err); - should(data).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'condition', 'material_id', 'note_id', 'user_id', 'status', '__v'); - should(data).have.property('_id'); - should(data).have.property('number', '21'); - should(data).have.property('color', 'natural'); - should(data).have.property('type', 'as-delivered/raw'); - should(data).have.property('batch', '1560237365'); - should(data.condition).have.property('material', 'copper'); - should(data.condition).have.property('weeks', 3); - should(data.condition.condition_template.toString()).be.eql('200000000000000000000001'); - should(data.material_id.toString()).be.eql('100000000000000000000001'); - should(data.user_id.toString()).be.eql('000000000000000000000002'); - should(data).have.property('status','validated'); - should(data.note_id.toString()).be.eql('500000000000000000000001'); - done(); - }); - }); - }); - it('changes the given properties', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {type: 'processed', color: 'signalviolet', batch: '114531', condition: {condition_template: '200000000000000000000003'}, material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_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', 'condition', 'material_id', 'note_id', 'user_id', 'status', '__v'); - should(data).have.property('_id'); - should(data).have.property('number', '1'); - should(data).have.property('color', 'signalviolet'); - should(data).have.property('type', 'processed'); - should(data).have.property('batch', '114531'); - should(data).have.property('condition', {condition_template: '200000000000000000000003'}); - should(data.material_id.toString()).be.eql('100000000000000000000002'); - should(data.user_id.toString()).be.eql('000000000000000000000002'); - should(data).have.property('status','new'); - should(data).have.property('note_id'); - NoteModel.findById(data.note_id).lean().exec((err, data: any) => { - if (err) return done (err); - should(data).have.property('_id'); - should(data).have.property('comment', 'Testcomment'); - should(data).have.property('sample_references'); - should(data.sample_references).have.lengthOf(1); - should(data.sample_references[0].sample_id.toString()).be.eql('400000000000000000000003'); - should(data.sample_references[0]).have.property('relation', 'part to this sample'); - done(); - }); - }); - }); - }); - it('creates a changelog', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {type: 'processed', color: 'signalviolet', batch: '114531', condition: {condition_template: '200000000000000000000003'}, material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - log: { - collection: 'samples', - dataAdd: { - status: 'new' - }, - dataIgn: ['notes', 'note_id'] - } - }); - }); - it('adjusts the note_fields correctly', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000003', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {notes: {comment: 'Testcomment', sample_references: [], custom_fields: {field1: 'value 1'}}} - }).end(err => { - if (err) return done(err); - NoteFieldModel.findOne({name: 'not allowed for new applications'}).lean().exec((err, 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); - should(data).have.property('qty', 1); - done(); - }); - }); - }); - }); - it('deletes old note_fields', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000004', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {notes: {comment: 'Testcomment', sample_references: []}} - }).end(err => { - if (err) return done (err); - NoteFieldModel.findOne({name: 'another_field'}).lean().exec((err, data) => { - if (err) return done (err); - should(data).be.null(); - done(); - }); - }); - }); - it('keeps untouched notes', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {type: 'processed'} - }).end((err, res) => { - if (err) return done (err); - NoteModel.findById(res.body.note_id).lean().exec((err, data) => { - if (err) return done (err); - should(data).not.be.null(); - should(data).have.property('comment', 'Stoff gesperrt'); - should(data).have.property('sample_references').have.lengthOf(0); - done(); - }); - }); - }); - it('deletes old notes', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000004', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {notes: {comment: 'Testcomment', sample_references: []}} - }).end(err => { - if (err) return done (err); - NoteModel.findById('500000000000000000000003').lean().exec((err, data) => { - if (err) return done (err); - should(data).be.null(); - done(); - }); - }); - }); - it('rejects an unknown material id', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {type: 'processed', color: 'signalviolet', batch: '114531', material_id: '000000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Material not available'} - }); - }); - it('rejects a sample number', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {number: 25, type: 'processed', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Invalid body format', details: '"number" is not allowed'} - }); - }); - it('rejects an invalid sample reference', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {type: 'processed', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '000000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Sample reference not available'} - }); - }); - it('rejects an invalid material id', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {type: 'processed', color: 'signalviolet', batch: '114531', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Invalid body format', details: 'Invalid object id'} - }); - }); - it('rejects an invalid id', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/10000000000h000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 404, - req: {type: 'processed', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - }); - }); - it('rejects not specified condition parameters', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {condition: {material: 'copper', weeks: 3, xxx: 44, condition_template: '200000000000000000000001'}}, - res: {status: 'Invalid body format', details: '"xxx" is not allowed'} - }); - }); - it('rejects a condition parameter not in the value range', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {condition: {material: 'xx', weeks: 3, condition_template: '200000000000000000000001'}}, - res: {status: 'Invalid body format', details: '"material" must be one of [copper, hot air]'} - }); - }); - it('rejects a condition parameter below minimum range', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {condition: {material: 'copper', weeks: 0, condition_template: '200000000000000000000001'}}, - res: {status: 'Invalid body format', details: '"weeks" must be greater than or equal to 1'} - }); - }); - it('rejects a condition parameter above maximum range', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {condition: {material: 'copper', weeks: 10.5, condition_template: '200000000000000000000001'}}, - res: {status: 'Invalid body format', details: '"weeks" must be less than or equal to 10'} - }); - }); - it('rejects a missing condition parameter marked as required', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {condition: {material: 'copper', condition_template: '200000000000000000000001'}}, - res: {status: 'Invalid body format', details: '"weeks" is required'} - }); - }); - it('accepts a missing condition parameter not marked as required', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {condition: {weeks: 10, condition_template: '200000000000000000000001'}} - }).end(err => { - if (err) return done(err); - done(); - }); - }); - it('rejects an invalid condition template', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {condition: {material: 'copper', weeks: 3, condition_template: '200000000000h00000000001'}}, - res: {status: 'Condition template not available'} - }); - }); - it('rejects an unknown condition template', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {condition: {material: 'copper', weeks: 3, condition_template: '000000000000000000000001'}}, - res: {status: 'Condition template not available'} - }); - }); - it('rejects a not accepted type', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {type: 'xx'}, - res: {status: 'Invalid body format', details: '"type" must be one of [as-delivered/raw, processed]'} - }); - }); - it('allows keeping an empty condition empty', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000006', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {condition: {}}, - res: {_id: '400000000000000000000006', number: 'Rng36', type: 'as-delivered/raw', color: 'black', batch: '', condition: {}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002', status: 'new', added: '2004-01-10T13:37:04.000Z'} - }); - }); - it('rejects an old version of a condition template', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {condition: {p1: 36, condition_template: '200000000000000000000004'}}, - res: {status: 'Old template version not allowed'} - }); - }); - it('allows keeping an old version of a condition template', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000004', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {condition: {p1: 36, condition_template: '200000000000000000000004'}}, - res: {_id: '400000000000000000000004', number: '32', type: 'as-delivered/raw', color: 'black', batch: '1653000308', condition: {p1: 36, condition_template: '200000000000000000000004'}, material_id: '100000000000000000000005', note_id: '500000000000000000000003', user_id: '000000000000000000000003', status: 'new', added: '2004-01-10T13:37:04.000Z'} - }); - }); - it('rejects changing back to an empty condition', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {condition: {}}, - res: {status: 'Condition template not available'} - }); - }); - it('rejects editing a deleted sample', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000005', - auth: {basic: 'admin'}, - httpStatus: 403, - req: {} - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {key: 'janedoe'}, - httpStatus: 401, - req: {type: 'processed', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - }); - }); - it('rejects changes for samples from another user for a write user', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000003', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {} - }); - }); - it('accepts changes for samples from another user for a dev/admin user', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {}, - res: {_id: '400000000000000000000001', number: '1', type: 'as-delivered/raw', color: 'black', batch: '', condition: {condition_template: '200000000000000000000001', material: 'copper', weeks: 3}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002', status: 'validated', added: '2004-01-10T13:37:04.000Z'} - }); - }); - it('rejects requests from a read user', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'user'}, - httpStatus: 403, - req: {type: 'processed', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - }); - }); - it('returns 404 for an unknown sample', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/000000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 404, - req: {type: 'processed', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}} - }); - }) - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - httpStatus: 401, - req: {type: 'processed', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - }); - }); - }); + describe('PUT /sample/{id}', () => { + it('returns the right sample', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {}, + res: {_id: '400000000000000000000001', number: '1', type: 'as-delivered/raw', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002', status: 'validated', added: '2004-01-10T13:37:04.000Z'} + }); + }); + it('keeps unchanged properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {type: 'as-delivered/raw', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000004', notes: {}} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '400000000000000000000001', number: '1', type: 'as-delivered/raw', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002', status: 'validated', added: '2004-01-10T13:37:04.000Z'}); + SampleModel.findById('400000000000000000000001').lean().exec((err, data: any) => { + if (err) return done (err); + should(data).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'condition', 'material_id', 'note_id', 'user_id', 'status', '__v'); + should(data).have.property('_id'); + should(data).have.property('number', '1'); + should(data).have.property('color', 'black'); + should(data).have.property('type', 'as-delivered/raw'); + should(data).have.property('batch', ''); + should(data).have.property('condition', {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}); + should(data.material_id.toString()).be.eql('100000000000000000000004'); + should(data.user_id.toString()).be.eql('000000000000000000000002'); + should(data).have.property('status','validated'); + should(data).have.property('note_id', null); + done(); + }); + }); + }); + it('keeps only one unchanged parameter', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {type: 'as-delivered/raw'} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '400000000000000000000001', number: '1', type: 'as-delivered/raw', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002', status: 'validated', added: '2004-01-10T13:37:04.000Z'}); + SampleModel.findById('400000000000000000000001').lean().exec((err, data: any) => { + if (err) return done (err); + should(data).have.property('status','validated'); + done(); + }); + }); + }); + it('keeps an unchanged condition', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '400000000000000000000001', number: '1', type: 'as-delivered/raw', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002', status: 'validated', added: '2004-01-10T13:37:04.000Z'}); + SampleModel.findById('400000000000000000000001').lean().exec((err, data: any) => { + if (err) return done (err); + should(data).have.property('status','validated'); + done(); + }); + }); + }); + it('keeps unchanged notes', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000002', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {notes: {comment: 'Stoff gesperrt', sample_references: []}} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '400000000000000000000002', number: '21', type: 'as-delivered/raw', color: 'natural', batch: '1560237365', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', note_id: '500000000000000000000001', user_id: '000000000000000000000002', status: 'validated', added: '2004-01-10T13:37:04.000Z'}); + SampleModel.findById('400000000000000000000002').lean().exec((err, data: any) => { + if (err) return done (err); + should(data).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'condition', 'material_id', 'note_id', 'user_id', 'status', '__v'); + should(data).have.property('_id'); + should(data).have.property('number', '21'); + should(data).have.property('color', 'natural'); + should(data).have.property('type', 'as-delivered/raw'); + should(data).have.property('batch', '1560237365'); + should(data.condition).have.property('material', 'copper'); + should(data.condition).have.property('weeks', 3); + should(data.condition.condition_template.toString()).be.eql('200000000000000000000001'); + should(data.material_id.toString()).be.eql('100000000000000000000001'); + should(data.user_id.toString()).be.eql('000000000000000000000002'); + should(data).have.property('status','validated'); + should(data.note_id.toString()).be.eql('500000000000000000000001'); + done(); + }); + }); + }); + it('changes the given properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {type: 'processed', color: 'signalviolet', batch: '114531', condition: {condition_template: '200000000000000000000003'}, material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_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', 'condition', 'material_id', 'note_id', 'user_id', 'status', '__v'); + should(data).have.property('_id'); + should(data).have.property('number', '1'); + should(data).have.property('color', 'signalviolet'); + should(data).have.property('type', 'processed'); + should(data).have.property('batch', '114531'); + should(data).have.property('condition', {condition_template: '200000000000000000000003'}); + should(data.material_id.toString()).be.eql('100000000000000000000002'); + should(data.user_id.toString()).be.eql('000000000000000000000002'); + should(data).have.property('status','new'); + should(data).have.property('note_id'); + NoteModel.findById(data.note_id).lean().exec((err, data: any) => { + if (err) return done (err); + should(data).have.property('_id'); + should(data).have.property('comment', 'Testcomment'); + should(data).have.property('sample_references'); + should(data.sample_references).have.lengthOf(1); + should(data.sample_references[0].sample_id.toString()).be.eql('400000000000000000000003'); + should(data.sample_references[0]).have.property('relation', 'part to this sample'); + done(); + }); + }); + }); + }); + it('creates a changelog', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {type: 'processed', color: 'signalviolet', batch: '114531', condition: {condition_template: '200000000000000000000003'}, material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + log: { + collection: 'samples', + dataAdd: { + status: 'new' + }, + dataIgn: ['notes', 'note_id'] + } + }); + }); + it('adjusts the note_fields correctly', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000003', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {notes: {comment: 'Testcomment', sample_references: [], custom_fields: {field1: 'value 1'}}} + }).end(err => { + if (err) return done(err); + NoteFieldModel.findOne({name: 'not allowed for new applications'}).lean().exec((err, 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); + should(data).have.property('qty', 1); + done(); + }); + }); + }); + }); + it('deletes old note_fields', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000004', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {notes: {comment: 'Testcomment', sample_references: []}} + }).end(err => { + if (err) return done (err); + NoteFieldModel.findOne({name: 'another_field'}).lean().exec((err, data) => { + if (err) return done (err); + should(data).be.null(); + done(); + }); + }); + }); + it('keeps untouched notes', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000002', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {type: 'processed'} + }).end((err, res) => { + if (err) return done (err); + NoteModel.findById(res.body.note_id).lean().exec((err, data) => { + if (err) return done (err); + should(data).not.be.null(); + should(data).have.property('comment', 'Stoff gesperrt'); + should(data).have.property('sample_references').have.lengthOf(0); + done(); + }); + }); + }); + it('deletes old notes', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000004', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {notes: {comment: 'Testcomment', sample_references: []}} + }).end(err => { + if (err) return done (err); + NoteModel.findById('500000000000000000000003').lean().exec((err, data) => { + if (err) return done (err); + should(data).be.null(); + done(); + }); + }); + }); + it('rejects an unknown material id', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {type: 'processed', color: 'signalviolet', batch: '114531', material_id: '000000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Material not available'} + }); + }); + it('rejects a sample number', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: 25, type: 'processed', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format', details: '"number" is not allowed'} + }); + }); + it('rejects an invalid sample reference', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {type: 'processed', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '000000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Sample reference not available'} + }); + }); + it('rejects an invalid material id', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {type: 'processed', color: 'signalviolet', batch: '114531', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format', details: 'Invalid object id'} + }); + }); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/10000000000h000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 404, + req: {type: 'processed', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + }); + }); + it('rejects not specified condition parameters', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {condition: {material: 'copper', weeks: 3, xxx: 44, condition_template: '200000000000000000000001'}}, + res: {status: 'Invalid body format', details: '"xxx" is not allowed'} + }); + }); + it('rejects a condition parameter not in the value range', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {condition: {material: 'xx', weeks: 3, condition_template: '200000000000000000000001'}}, + res: {status: 'Invalid body format', details: '"material" must be one of [copper, hot air]'} + }); + }); + it('rejects a condition parameter below minimum range', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {condition: {material: 'copper', weeks: 0, condition_template: '200000000000000000000001'}}, + res: {status: 'Invalid body format', details: '"weeks" must be greater than or equal to 1'} + }); + }); + it('rejects a condition parameter above maximum range', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {condition: {material: 'copper', weeks: 10.5, condition_template: '200000000000000000000001'}}, + res: {status: 'Invalid body format', details: '"weeks" must be less than or equal to 10'} + }); + }); + it('rejects a missing condition parameter marked as required', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {condition: {material: 'copper', condition_template: '200000000000000000000001'}}, + res: {status: 'Invalid body format', details: '"weeks" is required'} + }); + }); + it('accepts a missing condition parameter not marked as required', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {condition: {weeks: 10, condition_template: '200000000000000000000001'}} + }).end(err => { + if (err) return done(err); + done(); + }); + }); + it('rejects an invalid condition template', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {condition: {material: 'copper', weeks: 3, condition_template: '200000000000h00000000001'}}, + res: {status: 'Condition template not available'} + }); + }); + it('rejects an unknown condition template', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {condition: {material: 'copper', weeks: 3, condition_template: '000000000000000000000001'}}, + res: {status: 'Condition template not available'} + }); + }); + it('rejects a not accepted type', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {type: 'xx'}, + res: {status: 'Invalid body format', details: '"type" must be one of [as-delivered/raw, processed]'} + }); + }); + it('allows keeping an empty condition empty', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000006', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {condition: {}}, + res: {_id: '400000000000000000000006', number: 'Rng36', type: 'as-delivered/raw', color: 'black', batch: '', condition: {}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002', status: 'new', added: '2004-01-10T13:37:04.000Z'} + }); + }); + it('rejects an old version of a condition template', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {condition: {p1: 36, condition_template: '200000000000000000000004'}}, + res: {status: 'Old template version not allowed'} + }); + }); + it('allows keeping an old version of a condition template', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000004', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {condition: {p1: 36, condition_template: '200000000000000000000004'}}, + res: {_id: '400000000000000000000004', number: '32', type: 'as-delivered/raw', color: 'black', batch: '1653000308', condition: {p1: 36, condition_template: '200000000000000000000004'}, material_id: '100000000000000000000005', note_id: '500000000000000000000003', user_id: '000000000000000000000003', status: 'new', added: '2004-01-10T13:37:04.000Z'} + }); + }); + it('rejects changing back to an empty condition', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {condition: {}}, + res: {status: 'Condition template not available'} + }); + }); + it('rejects editing a deleted sample', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000005', + auth: {basic: 'admin'}, + httpStatus: 403, + req: {} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {key: 'janedoe'}, + httpStatus: 401, + req: {type: 'processed', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + }); + }); + it('rejects changes for samples from another user for a write user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000003', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {} + }); + }); + it('accepts changes for samples from another user for a dev/admin user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {}, + res: {_id: '400000000000000000000001', number: '1', type: 'as-delivered/raw', color: 'black', batch: '', condition: {condition_template: '200000000000000000000001', material: 'copper', weeks: 3}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002', status: 'validated', added: '2004-01-10T13:37:04.000Z'} + }); + }); + it('rejects requests from a read user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + auth: {basic: 'user'}, + httpStatus: 403, + req: {type: 'processed', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + }); + }); + it('returns 404 for an unknown sample', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/000000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 404, + req: {type: 'processed', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}} + }); + }) + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/400000000000000000000001', + httpStatus: 401, + req: {type: 'processed', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + }); + }); + }); - describe('DELETE /sample/{id}', () => { - it('sets the status to deleted', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - SampleModel.findById('400000000000000000000001').lean().exec((err, data: any) => { - if (err) return done(err); - should(data).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'condition', 'material_id', 'note_id', 'user_id', 'status', '__v'); - should(data).have.property('_id'); - should(data).have.property('number', '1'); - should(data).have.property('color', 'black'); - should(data).have.property('type', 'as-delivered/raw'); - should(data).have.property('batch', ''); - should(data.condition).have.property('material', 'copper'); - should(data.condition).have.property('weeks', 3); - should(data.condition.condition_template.toString()).be.eql('200000000000000000000001'); - should(data.material_id.toString()).be.eql('100000000000000000000004'); - should(data.user_id.toString()).be.eql('000000000000000000000002'); - should(data).have.property('status','deleted'); - should(data).have.property('note_id', null); - done(); - }); - }); - }); - it('creates a changelog', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - log: { - collection: 'samples', - skip: 1, - dataAdd: {status: 'deleted'} - } - }); - }); - it('keeps the notes of the sample', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/sample/400000000000000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - NoteModel.findById('500000000000000000000001').lean().exec((err, data) => { - if (err) return done(err); - should(data).have.only.keys('_id', 'comment', 'sample_references', '__v'); - should(data).have.property('comment', 'Stoff gesperrt'); - should(data).have.property('sample_references').with.lengthOf(0); - done(); - }); - }); - }); - it('adjusts the note_fields correctly', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/sample/400000000000000000000004', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - NoteFieldModel.findOne({name: 'not allowed for new applications'}).lean().exec((err, data) => { - if (err) return done(err); - should(data).have.property('qty', 1); - NoteFieldModel.findOne({name: 'another_field'}).lean().exec((err, data) => { - if (err) return done(err); - should(data).be.null(); - done(); - }); - }); - }); - }); - it('keeps references to this sample', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/sample/400000000000000000000003', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - setTimeout(() => { // background action takes some time before we can check - NoteModel.findById('500000000000000000000003').lean().exec((err, data: any) => { - if (err) return done(err); - should(data).have.property('sample_references').with.lengthOf(1); - should(data.sample_references[0].sample_id.toString()).be.eql('400000000000000000000003'); - should(data.sample_references[0]).have.property('relation', 'part to sample'); - done(); - }); - }, 100); + describe('DELETE /sample/{id}', () => { + it('sets the status to deleted', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + SampleModel.findById('400000000000000000000001').lean().exec((err, data: any) => { + if (err) return done(err); + should(data).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'condition', 'material_id', 'note_id', 'user_id', 'status', '__v'); + should(data).have.property('_id'); + should(data).have.property('number', '1'); + should(data).have.property('color', 'black'); + should(data).have.property('type', 'as-delivered/raw'); + should(data).have.property('batch', ''); + should(data.condition).have.property('material', 'copper'); + should(data.condition).have.property('weeks', 3); + should(data.condition.condition_template.toString()).be.eql('200000000000000000000001'); + should(data.material_id.toString()).be.eql('100000000000000000000004'); + should(data.user_id.toString()).be.eql('000000000000000000000002'); + should(data).have.property('status','deleted'); + should(data).have.property('note_id', null); + done(); + }); + }); + }); + it('creates a changelog', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + log: { + collection: 'samples', + skip: 1, + dataAdd: {status: 'deleted'} + } + }); + }); + it('keeps the notes of the sample', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/sample/400000000000000000000002', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + NoteModel.findById('500000000000000000000001').lean().exec((err, data) => { + if (err) return done(err); + should(data).have.only.keys('_id', 'comment', 'sample_references', '__v'); + should(data).have.property('comment', 'Stoff gesperrt'); + should(data).have.property('sample_references').with.lengthOf(0); + done(); + }); + }); + }); + it('adjusts the note_fields correctly', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/sample/400000000000000000000004', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + NoteFieldModel.findOne({name: 'not allowed for new applications'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.property('qty', 1); + NoteFieldModel.findOne({name: 'another_field'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).be.null(); + done(); + }); + }); + }); + }); + it('keeps references to this sample', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/sample/400000000000000000000003', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + setTimeout(() => { // background action takes some time before we can check + NoteModel.findById('500000000000000000000003').lean().exec((err, data: any) => { + if (err) return done(err); + should(data).have.property('sample_references').with.lengthOf(1); + should(data.sample_references[0].sample_id.toString()).be.eql('400000000000000000000003'); + should(data.sample_references[0]).have.property('relation', 'part to sample'); + done(); + }); + }, 100); - }); - }); - it('lets admin/dev users delete samples of other users', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/sample/400000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - SampleModel.findById('400000000000000000000001').lean().exec((err, data) => { - if (err) return done(err); - should(data).have.property('status','deleted'); - done(); - }); - }); - }); - it('deletes associated measurements', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - MeasurementModel.find({sample_id: mongoose.Types.ObjectId('400000000000000000000001')}).lean().exec((err, data: any) => { - if (err) return done(err); - should(data).matchEach(sample => { - should(sample).have.property('status', 'deleted'); - }); - done(); - }); - }); - }); - it('rejects deleting samples of other users for write users', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/sample/400000000000000000000004', - auth: {basic: 'janedoe'}, - httpStatus: 403 - }); - }); - it('rejects an invalid id', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/sample/400000000000h00000000004', - auth: {basic: 'janedoe'}, - httpStatus: 404 - }); - }); - it('rejects requests from a read user', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/sample/400000000000000000000004', - auth: {basic: 'user'}, - httpStatus: 403 - }); - }); - it('returns 404 for an unknown id', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/sample/000000000000000000000004', - auth: {basic: 'janedoe'}, - httpStatus: 404 - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/sample/400000000000000000000001', - auth: {key: 'janedoe'}, - httpStatus: 401 - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/sample/400000000000000000000001', - httpStatus: 401 - }); - }); - }); + }); + }); + it('lets admin/dev users delete samples of other users', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/sample/400000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + SampleModel.findById('400000000000000000000001').lean().exec((err, data) => { + if (err) return done(err); + should(data).have.property('status','deleted'); + done(); + }); + }); + }); + it('deletes associated measurements', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/sample/400000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + MeasurementModel.find({sample_id: mongoose.Types.ObjectId('400000000000000000000001')}).lean().exec((err, data: any) => { + if (err) return done(err); + should(data).matchEach(sample => { + should(sample).have.property('status', 'deleted'); + }); + done(); + }); + }); + }); + it('rejects deleting samples of other users for write users', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/sample/400000000000000000000004', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/sample/400000000000h00000000004', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects requests from a read user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/sample/400000000000000000000004', + auth: {basic: 'user'}, + httpStatus: 403 + }); + }); + it('returns 404 for an unknown id', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/sample/000000000000000000000004', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/sample/400000000000000000000001', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/sample/400000000000000000000001', + httpStatus: 401 + }); + }); + }); - describe('GET /sample/number/{number}', () => { - it('returns the right sample', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/number/33', - auth: {basic: 'janedoe'}, - httpStatus: 200, - res: {_id: '400000000000000000000003', number: '33', type: 'processed', color: 'black', batch: '1704-005', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 33, carbon_fiber: 0}, numbers: ['5514262406']}, notes: {comment: '', sample_references: [{sample_id: '400000000000000000000004', relation: 'as-delivered/raw to sample'}], custom_fields: {'not allowed for new applications': true}}, measurements: [{_id: '800000000000000000000003', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003'}], status: 'new', user: 'admin'} - }); - }); - it('works with an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/number/33', - auth: {key: 'janedoe'}, - httpStatus: 200, - res: {_id: '400000000000000000000003', number: '33', type: 'processed', color: 'black', batch: '1704-005', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 33, carbon_fiber: 0}, numbers: ['5514262406']}, notes: {comment: '', sample_references: [{sample_id: '400000000000000000000004', relation: 'as-delivered/raw to sample'}], custom_fields: {'not allowed for new applications': true}}, measurements: [{_id: '800000000000000000000003', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003'}], status: 'new', user: 'admin'} - }); - }); - it('returns a deleted sample for a dev/admin user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/number/Rng33', - auth: {basic: 'admin'}, - httpStatus: 200, - res: {_id: '400000000000000000000005', number: 'Rng33', type: 'as-delivered/raw', color: 'black', batch: '1653000308', condition: {condition_template: '200000000000000000000003'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 33, carbon_fiber: 0}, numbers: ['5514262406']}, notes: {}, measurements: [], status: 'deleted', user: 'admin'} - }); - }); - it ('filters out spectral data for a write user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/number/1', - auth: {basic: 'janedoe'}, - httpStatus: 200, - res: {_id: '400000000000000000000001', number: '1', type: 'as-delivered/raw', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {numbers: ['5513933405'], _id: '100000000000000000000004', name: 'Schulamid 66 GF 25 H', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 25, carbon_fiber: 0}, group: 'PA66', supplier: 'Schulmann'}, user: 'janedoe', notes: {}, measurements: [{_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000007', sample_id: '400000000000000000000001', values: {device: 'Alpha II', filename: '1_1.DPT'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000009', sample_id: '400000000000000000000001', values: {device: 'Alpha II', filename: '1_2.DPT'}, measurement_template: '300000000000000000000001'}], status: 'validated'} - }); - }); - it ('returns spectral data for an admin user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/number/1', - auth: {basic: 'admin'}, - httpStatus: 200, - res: {_id: '400000000000000000000001', number: '1', type: 'as-delivered/raw', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {numbers: ['5513933405'], _id: '100000000000000000000004', name: 'Schulamid 66 GF 25 H', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 25, carbon_fiber: 0}, group: 'PA66', supplier: 'Schulmann'}, user: 'janedoe', notes: {}, measurements: [{_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[ 3997.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]],device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000007', sample_id: '400000000000000000000001', values: {dpt: [[ 3996.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]], device: 'Alpha II', filename: '1_1.DPT'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000009', sample_id: '400000000000000000000001', values: {dpt: [[ 3996.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]], device: 'Alpha II', filename: '1_2.DPT'}, measurement_template: '300000000000000000000001'}], status: 'validated'} - }); - }); - it('returns 403 for a write user when requesting a deleted sample', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/number/Rng33', - auth: {basic: 'janedoe'}, - httpStatus: 403 - }); - }); - it('returns 404 for an unknown sample', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/number/Rng883', - auth: {basic: 'janedoe'}, - httpStatus: 404 - }); - }); - it('rejects an invalid id', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/number/xx-xx', - auth: {basic: 'janedoe'}, - httpStatus: 404 - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/number/33', - httpStatus: 401 - }); - }); - }); + describe('GET /sample/number/{number}', () => { + it('returns the right sample', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/number/33', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {_id: '400000000000000000000003', number: '33', type: 'processed', color: 'black', batch: '1704-005', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 33, carbon_fiber: 0}, numbers: ['5514262406']}, notes: {comment: '', sample_references: [{sample_id: '400000000000000000000004', relation: 'as-delivered/raw to sample'}], custom_fields: {'not allowed for new applications': true}}, measurements: [{_id: '800000000000000000000003', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003'}], status: 'new', user: 'admin'} + }); + }); + it('works with an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/number/33', + auth: {key: 'janedoe'}, + httpStatus: 200, + res: {_id: '400000000000000000000003', number: '33', type: 'processed', color: 'black', batch: '1704-005', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 33, carbon_fiber: 0}, numbers: ['5514262406']}, notes: {comment: '', sample_references: [{sample_id: '400000000000000000000004', relation: 'as-delivered/raw to sample'}], custom_fields: {'not allowed for new applications': true}}, measurements: [{_id: '800000000000000000000003', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003'}], status: 'new', user: 'admin'} + }); + }); + it('returns a deleted sample for a dev/admin user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/number/Rng33', + auth: {basic: 'admin'}, + httpStatus: 200, + res: {_id: '400000000000000000000005', number: 'Rng33', type: 'as-delivered/raw', color: 'black', batch: '1653000308', condition: {condition_template: '200000000000000000000003'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 33, carbon_fiber: 0}, numbers: ['5514262406']}, notes: {}, measurements: [], status: 'deleted', user: 'admin'} + }); + }); + it ('filters out spectral data for a write user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/number/1', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {_id: '400000000000000000000001', number: '1', type: 'as-delivered/raw', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {numbers: ['5513933405'], _id: '100000000000000000000004', name: 'Schulamid 66 GF 25 H', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 25, carbon_fiber: 0}, group: 'PA66', supplier: 'Schulmann'}, user: 'janedoe', notes: {}, measurements: [{_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000007', sample_id: '400000000000000000000001', values: {device: 'Alpha II', filename: '1_1.DPT'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000009', sample_id: '400000000000000000000001', values: {device: 'Alpha II', filename: '1_2.DPT'}, measurement_template: '300000000000000000000001'}], status: 'validated'} + }); + }); + it ('returns spectral data for an admin user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/number/1', + auth: {basic: 'admin'}, + httpStatus: 200, + res: {_id: '400000000000000000000001', number: '1', type: 'as-delivered/raw', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {numbers: ['5513933405'], _id: '100000000000000000000004', name: 'Schulamid 66 GF 25 H', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 25, carbon_fiber: 0}, group: 'PA66', supplier: 'Schulmann'}, user: 'janedoe', notes: {}, measurements: [{_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[ 3997.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]],device: 'Alpha I', filename: '1_0.DPT'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000007', sample_id: '400000000000000000000001', values: {dpt: [[ 3996.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]], device: 'Alpha II', filename: '1_1.DPT'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000009', sample_id: '400000000000000000000001', values: {dpt: [[ 3996.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]], device: 'Alpha II', filename: '1_2.DPT'}, measurement_template: '300000000000000000000001'}], status: 'validated'} + }); + }); + it('returns 403 for a write user when requesting a deleted sample', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/number/Rng33', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('returns 404 for an unknown sample', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/number/Rng883', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/number/xx-xx', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/number/33', + httpStatus: 401 + }); + }); + }); - describe('PUT /sample/restore/{id}', () => { - it('sets the status', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/restore/400000000000000000000005', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {} - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'OK'}); - SampleModel.findById('400000000000000000000005').lean().exec((err, data: any) => { - if (err) return done(err); - should(data).have.property('status','new'); - done(); - }); - }); - }); - it('restores associated measurements', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/restore/400000000000000000000005', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {} - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'OK'}); - MeasurementModel.find({sample_id: mongoose.Types.ObjectId('400000000000000000000005')}).lean().exec((err, data: any) => { - if (err) return done(err); - should(data).matchEach(measurement => { - should(measurement).have.property('status', 'new') - }); - done(); - }); - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/restore/400000000000000000000005', - auth: {key: 'admin'}, - httpStatus: 401, - req: {} - }); - }); - it('rejects a write user', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/restore/400000000000000000000005', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {} - }); - }); - it('returns 404 for an unknown sample', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/restore/000000000000000000000005', - auth: {basic: 'admin'}, - httpStatus: 404, - req: {} - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/restore/400000000000000000000005', - httpStatus: 401, - req: {} - }); - }); - }); + describe('PUT /sample/restore/{id}', () => { + it('sets the status', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/restore/400000000000000000000005', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + SampleModel.findById('400000000000000000000005').lean().exec((err, data: any) => { + if (err) return done(err); + should(data).have.property('status','new'); + done(); + }); + }); + }); + it('restores associated measurements', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/restore/400000000000000000000005', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + MeasurementModel.find({sample_id: mongoose.Types.ObjectId('400000000000000000000005')}).lean().exec((err, data: any) => { + if (err) return done(err); + should(data).matchEach(measurement => { + should(measurement).have.property('status', 'new') + }); + done(); + }); + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/restore/400000000000000000000005', + auth: {key: 'admin'}, + httpStatus: 401, + req: {} + }); + }); + it('rejects a write user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/restore/400000000000000000000005', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {} + }); + }); + it('returns 404 for an unknown sample', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/restore/000000000000000000000005', + auth: {basic: 'admin'}, + httpStatus: 404, + req: {} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/restore/400000000000000000000005', + httpStatus: 401, + req: {} + }); + }); + }); - describe('PUT /sample/validate/{id}', () => { - it('sets the status', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/validate/400000000000000000000003', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {} - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'OK'}); - SampleModel.findById('400000000000000000000003').lean().exec((err, data: any) => { - if (err) return done(err); - should(data).have.property('status','validated'); - done(); - }); - }); - }); - it('validates associated measurements', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/validate/400000000000000000000003', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {} - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'OK'}); - MeasurementModel.find({sample_id: mongoose.Types.ObjectId('400000000000000000000003')}).lean().exec((err, data: any) => { - if (err) return done(err); - should(data).matchEach(measurement => { - should(measurement).have.property('status', 'validated') - }); - done(); - }); - }); - }); - it('allows validating a sample without condition', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/validate/400000000000000000000006', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {} - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'OK'}); - SampleModel.findById('400000000000000000000006').lean().exec((err, data: any) => { - if (err) return done(err); - should(data).have.property('status','validated'); - done(); - }); - }); - }); - it('allows validating a sample without measurements', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/validate/400000000000000000000004', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {} - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'OK'}); - SampleModel.findById('400000000000000000000004').lean().exec((err, data: any) => { - if (err) return done(err); - should(data).have.property('status','validated'); - done(); - }); - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/validate/400000000000000000000003', - auth: {key: 'admin'}, - httpStatus: 401, - req: {} - }); - }); - it('rejects a write user', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/validate/400000000000000000000003', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {} - }); - }); - it('returns 404 for an unknown sample', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/validate/000000000000000000000003', - auth: {basic: 'admin'}, - httpStatus: 404, - req: {} - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/validate/400000000000000000000003', - httpStatus: 401, - req: {} - }); - }); - }); + describe('PUT /sample/validate/{id}', () => { + it('sets the status', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/validate/400000000000000000000003', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + SampleModel.findById('400000000000000000000003').lean().exec((err, data: any) => { + if (err) return done(err); + should(data).have.property('status','validated'); + done(); + }); + }); + }); + it('validates associated measurements', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/validate/400000000000000000000003', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + MeasurementModel.find({sample_id: mongoose.Types.ObjectId('400000000000000000000003')}).lean().exec((err, data: any) => { + if (err) return done(err); + should(data).matchEach(measurement => { + should(measurement).have.property('status', 'validated') + }); + done(); + }); + }); + }); + it('allows validating a sample without condition', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/validate/400000000000000000000006', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + SampleModel.findById('400000000000000000000006').lean().exec((err, data: any) => { + if (err) return done(err); + should(data).have.property('status','validated'); + done(); + }); + }); + }); + it('allows validating a sample without measurements', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/validate/400000000000000000000004', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + SampleModel.findById('400000000000000000000004').lean().exec((err, data: any) => { + if (err) return done(err); + should(data).have.property('status','validated'); + done(); + }); + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/validate/400000000000000000000003', + auth: {key: 'admin'}, + httpStatus: 401, + req: {} + }); + }); + it('rejects a write user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/validate/400000000000000000000003', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {} + }); + }); + it('returns 404 for an unknown sample', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/validate/000000000000000000000003', + auth: {basic: 'admin'}, + httpStatus: 404, + req: {} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/sample/validate/400000000000000000000003', + httpStatus: 401, + req: {} + }); + }); + }); - describe('POST /sample/new', () => { - it('returns the right sample', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_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', 'condition', 'material_id', 'note_id', 'user_id', 'status', 'added'); - should(res.body).have.property('_id').be.type('string'); - should(res.body).have.property('number', 'Rng37'); - should(res.body).have.property('color', 'black'); - should(res.body).have.property('type', 'as-delivered/raw'); - should(res.body).have.property('batch', '1560237365'); - should(res.body).have.property('condition', {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}); - 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', '000000000000000000000002'); - should(res.body).have.property('status', 'new'); - should(res.body).have.property('added').be.type('string'); - should(new Date().getTime() - new Date(res.body.added).getTime()).be.below(2000); - done(); - }); - }); - it('stores the sample', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}} - }).end(err => { - if (err) return done (err); - SampleModel.find({number: 'Rng37'}).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', 'condition', 'material_id', 'note_id', 'user_id', 'status', '__v'); - should(data[0]).have.property('_id'); - should(data[0]).have.property('number', 'Rng37'); - should(data[0]).have.property('color', 'black'); - should(data[0]).have.property('type', 'as-delivered/raw'); - should(data[0]).have.property('batch', '1560237365'); - should(data[0]).have.property('condition', {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}); - should(data[0].material_id.toString()).be.eql('100000000000000000000001'); - should(data[0].user_id.toString()).be.eql('000000000000000000000002'); - should(data[0]).have.property('status','new'); - should(data[0]).have.property('note_id'); - NoteModel.findById(data[0].note_id).lean().exec((err, data: any) => { - if (err) return done (err); - should(data).have.property('_id'); - should(data).have.property('comment', 'Testcomment'); - should(data).have.property('sample_references'); - should(data.sample_references).have.lengthOf(1); - should(data.sample_references[0].sample_id.toString()).be.eql('400000000000000000000003'); - should(data.sample_references[0]).have.property('relation', 'part to this sample'); - done(); - }); - }) - }); - }); - it('creates a changelog', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - log: { - collection: 'samples', - dataAdd: { - number: 'Rng37', - user_id: '000000000000000000000002', - status: 'new' - }, - dataIgn: ['notes', 'note_id'] - } - }); - }); - it('stores the custom fields', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {color: 'black', type: 'as-delivered/raw', 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) => { - if (err) return done(err); - should(data).have.property('_id'); - should(data).have.property('comment', 'Testcomment'); - should(data).have.property('sample_references').have.lengthOf(0); - should(data).have.property('custom_fields'); - should(data.custom_fields).have.property('field1', 'a'); - should(data.custom_fields).have.property('field2', 'b'); - should(data.custom_fields).have.property('not allowed for new applications', true); - NoteFieldModel.find({name: 'field1'}).lean().exec((err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.property('qty', 1); - NoteFieldModel.find({name: 'field2'}).lean().exec((err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.property('qty', 1); - NoteFieldModel.find({name: 'not allowed for new applications'}).lean().exec((err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.property('qty', 3); - done(); - }); - }); - }); - }); - }); - }); - 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: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_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', 'condition', 'material_id', 'note_id', 'user_id', 'status', 'added'); - 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', 'as-delivered/raw'); - 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'); - should(res.body).have.property('status', 'new'); - should(res.body).have.property('added').be.type('string'); - should(new Date().getTime() - new Date(res.body.added).getTime()).be.below(1500); - done(); - }); - }); - it('accepts a sample without condition', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_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', 'condition', 'material_id', 'note_id', 'user_id', 'status', 'added'); - should(res.body).have.property('_id').be.type('string'); - should(res.body).have.property('number', 'Rng37'); - should(res.body).have.property('color', 'black'); - should(res.body).have.property('type', 'as-delivered/raw'); - should(res.body).have.property('batch', '1560237365'); - should(res.body).have.property('condition', {}); - 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', '000000000000000000000002'); - should(res.body).have.property('status', 'new'); - should(res.body).have.property('added').be.type('string'); - should(new Date().getTime() - new Date(res.body.added).getTime()).be.below(1000); - done(); - }); - }); - it('rejects an unknown material id', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '000000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Material not available'} - }); - }); - it('rejects a sample number for a write user', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {number: 'Rng34', color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Invalid body format', details: '"number" is not allowed'} - }); - }); - it('allows a sample number for an admin user', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {number: 'Rng34', color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_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', 'condition', 'material_id', 'note_id', 'user_id', 'status', 'added'); - should(res.body).have.property('_id').be.type('string'); - should(res.body).have.property('number', 'Rng34'); - should(res.body).have.property('color', 'black'); - should(res.body).have.property('type', 'as-delivered/raw'); - should(res.body).have.property('batch', '1560237365'); - should(res.body).have.property('condition', {}); - 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', '000000000000000000000003'); - should(res.body).have.property('status', 'new'); - should(res.body).have.property('added').be.type('string'); - should(new Date().getTime() - new Date(res.body.added).getTime()).be.below(1000); - done(); - }); - }); - it('rejects an existing sample number for an admin user', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {number: 'Rng33', color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Sample number already taken'} - }); - }); - it('rejects an invalid sample reference', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '000000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Sample reference not available'} - }); - }); - it('rejects an invalid condition_template id', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', weeks: 3, condition_template: '20000h000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Condition template not available'} - }); - }); - it('rejects a not existing condition_template id', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', weeks: 3, condition_template: '000000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Condition template not available'} - }); - }); - it('rejects not specified condition parameters', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', weeks: 3, xxx: 23, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Invalid body format', details: '"xxx" is not allowed'} - }); - }); - it('rejects missing condition parameters', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: { status: 'Invalid body format', details: '"weeks" is required'} - }); - }); - it('rejects condition parameters not in the value range', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'xxx', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Invalid body format', details: '"material" must be one of [copper, hot air]'} - }); - }); - it('rejects a condition parameter below minimum range', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', weeks: 0, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Invalid body format', details: '"weeks" must be greater than or equal to 1'} - }); - }); - it('rejects a condition parameter above maximum range', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', weeks: 11, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Invalid body format', details: '"weeks" must be less than or equal to 10'} - }); - }); - it('rejects a missing condition parameter marked as required', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Invalid body format', details: '"weeks" is required'} - }); - }); - it('accepts a missing condition parameter not marked as required', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: { weeks: 10, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - }).end(err => { - if (err) return done(err); - done(); - }); - }); - it('rejects a condition without condition template', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', weeks: 3}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Condition template not available'} - }); - }); - it('rejects an old version of a condition template', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {p1: 36, condition_template: '200000000000000000000004'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Old template version not allowed'} - }); - }); - it('rejects a missing color', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {type: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Invalid body format', details: '"color" is required'} - }); - }); - it('rejects a missing type', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {color: 'black', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Invalid body format', details: '"type" is required'} - }); - }); - it('rejects a missing batch', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {color: 'black', type: 'as-delivered/raw', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Invalid body format', details: '"batch" is required'} - }); - }); - it('rejects a missing material id', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Invalid body format', details: '"material_id" is required'} - }); - }); - it('rejects an invalid material id', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Invalid body format', details: 'Invalid object id'} - }); - }); - it('rejects a not accepted type', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {color: 'black', type: 'xx', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment'}}, - res: {status: 'Invalid body format', details: '"type" must be one of [as-delivered/raw, processed]'} - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {key: 'janedoe'}, - httpStatus: 401, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}} - }); - }); - it('rejects requests from a read user', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - auth: {basic: 'user'}, - httpStatus: 403, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}} - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/sample/new', - httpStatus: 401, - req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}} - }); - }); - }); + describe('POST /sample/new', () => { + it('returns the right sample', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_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', 'condition', 'material_id', 'note_id', 'user_id', 'status', 'added'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('number', 'Rng37'); + should(res.body).have.property('color', 'black'); + should(res.body).have.property('type', 'as-delivered/raw'); + should(res.body).have.property('batch', '1560237365'); + should(res.body).have.property('condition', {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}); + 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', '000000000000000000000002'); + should(res.body).have.property('status', 'new'); + should(res.body).have.property('added').be.type('string'); + should(new Date().getTime() - new Date(res.body.added).getTime()).be.below(2000); + done(); + }); + }); + it('stores the sample', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}} + }).end(err => { + if (err) return done (err); + SampleModel.find({number: 'Rng37'}).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', 'condition', 'material_id', 'note_id', 'user_id', 'status', '__v'); + should(data[0]).have.property('_id'); + should(data[0]).have.property('number', 'Rng37'); + should(data[0]).have.property('color', 'black'); + should(data[0]).have.property('type', 'as-delivered/raw'); + should(data[0]).have.property('batch', '1560237365'); + should(data[0]).have.property('condition', {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}); + should(data[0].material_id.toString()).be.eql('100000000000000000000001'); + should(data[0].user_id.toString()).be.eql('000000000000000000000002'); + should(data[0]).have.property('status','new'); + should(data[0]).have.property('note_id'); + NoteModel.findById(data[0].note_id).lean().exec((err, data: any) => { + if (err) return done (err); + should(data).have.property('_id'); + should(data).have.property('comment', 'Testcomment'); + should(data).have.property('sample_references'); + should(data.sample_references).have.lengthOf(1); + should(data.sample_references[0].sample_id.toString()).be.eql('400000000000000000000003'); + should(data.sample_references[0]).have.property('relation', 'part to this sample'); + done(); + }); + }) + }); + }); + it('creates a changelog', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + log: { + collection: 'samples', + dataAdd: { + number: 'Rng37', + user_id: '000000000000000000000002', + status: 'new' + }, + dataIgn: ['notes', 'note_id'] + } + }); + }); + it('stores the custom fields', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {color: 'black', type: 'as-delivered/raw', 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) => { + if (err) return done(err); + should(data).have.property('_id'); + should(data).have.property('comment', 'Testcomment'); + should(data).have.property('sample_references').have.lengthOf(0); + should(data).have.property('custom_fields'); + should(data.custom_fields).have.property('field1', 'a'); + should(data.custom_fields).have.property('field2', 'b'); + should(data.custom_fields).have.property('not allowed for new applications', true); + NoteFieldModel.find({name: 'field1'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('qty', 1); + NoteFieldModel.find({name: 'field2'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('qty', 1); + NoteFieldModel.find({name: 'not allowed for new applications'}).lean().exec((err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('qty', 3); + done(); + }); + }); + }); + }); + }); + }); + 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: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_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', 'condition', 'material_id', 'note_id', 'user_id', 'status', 'added'); + 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', 'as-delivered/raw'); + 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'); + should(res.body).have.property('status', 'new'); + should(res.body).have.property('added').be.type('string'); + should(new Date().getTime() - new Date(res.body.added).getTime()).be.below(1500); + done(); + }); + }); + it('accepts a sample without condition', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_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', 'condition', 'material_id', 'note_id', 'user_id', 'status', 'added'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('number', 'Rng37'); + should(res.body).have.property('color', 'black'); + should(res.body).have.property('type', 'as-delivered/raw'); + should(res.body).have.property('batch', '1560237365'); + should(res.body).have.property('condition', {}); + 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', '000000000000000000000002'); + should(res.body).have.property('status', 'new'); + should(res.body).have.property('added').be.type('string'); + should(new Date().getTime() - new Date(res.body.added).getTime()).be.below(1000); + done(); + }); + }); + it('rejects an unknown material id', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '000000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Material not available'} + }); + }); + it('rejects a sample number for a write user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {number: 'Rng34', color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format', details: '"number" is not allowed'} + }); + }); + it('allows a sample number for an admin user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {number: 'Rng34', color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_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', 'condition', 'material_id', 'note_id', 'user_id', 'status', 'added'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('number', 'Rng34'); + should(res.body).have.property('color', 'black'); + should(res.body).have.property('type', 'as-delivered/raw'); + should(res.body).have.property('batch', '1560237365'); + should(res.body).have.property('condition', {}); + 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', '000000000000000000000003'); + should(res.body).have.property('status', 'new'); + should(res.body).have.property('added').be.type('string'); + should(new Date().getTime() - new Date(res.body.added).getTime()).be.below(1000); + done(); + }); + }); + it('rejects an existing sample number for an admin user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {number: 'Rng33', color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Sample number already taken'} + }); + }); + it('rejects an invalid sample reference', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '000000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Sample reference not available'} + }); + }); + it('rejects an invalid condition_template id', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', weeks: 3, condition_template: '20000h000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Condition template not available'} + }); + }); + it('rejects a not existing condition_template id', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', weeks: 3, condition_template: '000000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Condition template not available'} + }); + }); + it('rejects not specified condition parameters', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', weeks: 3, xxx: 23, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format', details: '"xxx" is not allowed'} + }); + }); + it('rejects missing condition parameters', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: { status: 'Invalid body format', details: '"weeks" is required'} + }); + }); + it('rejects condition parameters not in the value range', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'xxx', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format', details: '"material" must be one of [copper, hot air]'} + }); + }); + it('rejects a condition parameter below minimum range', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', weeks: 0, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format', details: '"weeks" must be greater than or equal to 1'} + }); + }); + it('rejects a condition parameter above maximum range', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', weeks: 11, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format', details: '"weeks" must be less than or equal to 10'} + }); + }); + it('rejects a missing condition parameter marked as required', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format', details: '"weeks" is required'} + }); + }); + it('accepts a missing condition parameter not marked as required', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: { weeks: 10, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + }).end(err => { + if (err) return done(err); + done(); + }); + }); + it('rejects a condition without condition template', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {material: 'copper', weeks: 3}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Condition template not available'} + }); + }); + it('rejects an old version of a condition template', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', condition: {p1: 36, condition_template: '200000000000000000000004'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Old template version not allowed'} + }); + }); + it('rejects a missing color', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {type: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format', details: '"color" is required'} + }); + }); + it('rejects a missing type', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format', details: '"type" is required'} + }); + }); + it('rejects a missing batch', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'as-delivered/raw', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format', details: '"batch" is required'} + }); + }); + it('rejects a missing material id', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format', details: '"material_id" is required'} + }); + }); + it('rejects an invalid material id', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Invalid body format', details: 'Invalid object id'} + }); + }); + it('rejects a not accepted type', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'xx', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment'}}, + res: {status: 'Invalid body format', details: '"type" must be one of [as-delivered/raw, processed]'} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {key: 'janedoe'}, + httpStatus: 401, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}} + }); + }); + it('rejects requests from a read user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'user'}, + httpStatus: 403, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + httpStatus: 401, + req: {color: 'black', type: 'as-delivered/raw', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}} + }); + }); + }); - describe('GET /sample/notes/fields', () => { - it('returns all fields', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/notes/fields', - auth: {basic: 'user'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.note_fields.length); - should(res.body).matchEach(material => { - should(material).have.only.keys('name', 'qty'); - should(material).have.property('qty').be.type('number'); - }); - done(); - }); - }); - it('works with an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/notes/fields', - auth: {key: 'user'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.note_fields.length); - should(res.body).matchEach(material => { - should(material).have.only.keys('name', 'qty'); - should(material).have.property('qty').be.type('number'); - }); - done(); - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/sample/notes/fields', - httpStatus: 401 - }); - }); - }); + describe('GET /sample/notes/fields', () => { + it('returns all fields', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/notes/fields', + auth: {basic: 'user'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.note_fields.length); + should(res.body).matchEach(material => { + should(material).have.only.keys('name', 'qty'); + should(material).have.property('qty').be.type('number'); + }); + done(); + }); + }); + it('works with an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/notes/fields', + auth: {key: 'user'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.note_fields.length); + should(res.body).matchEach(material => { + should(material).have.only.keys('name', 'qty'); + should(material).have.property('qty').be.type('number'); + }); + done(); + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/notes/fields', + httpStatus: 401 + }); + }); + }); }); diff --git a/src/routes/sample.ts b/src/routes/sample.ts index cd97c01..8d47279 100644 --- a/src/routes/sample.ts +++ b/src/routes/sample.ts @@ -24,650 +24,650 @@ const router = express.Router(); router.get('/samples', async (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; - const {error, value: filters} = SampleValidate.query(req.query, ['dev', 'admin'].indexOf(req.authDetails.level) >= 0); - if (error) return res400(error, res); + const {error, value: filters} = SampleValidate.query(req.query, ['dev', 'admin'].indexOf(req.authDetails.level) >= 0); + if (error) return res400(error, res); - // spectral data and csv not allowed for read/write users - if ((filters.fields.find(e => e.indexOf('.' + globals.spectrum.dpt) >= 0) || filters.output !== 'json') && - !req.auth(res, ['dev', 'admin'], 'all')) return; + // spectral data and csv not allowed for read/write users + if ((filters.fields.find(e => e.indexOf('.' + globals.spectrum.dpt) >= 0) || filters.output !== 'json') && + !req.auth(res, ['dev', 'admin'], 'all')) return; - // evaluate sort parameter from 'color-asc' to ['color', 1] - filters.sort = filters.sort.split('-'); - filters.sort[0] = filters.sort[0] === 'added' ? '_id' : filters.sort[0]; // route added sorting criteria to _id - filters.sort[1] = filters.sort[1] === 'desc' ? -1 : 1; - if (!filters['to-page']) { // set to-page default - filters['to-page'] = 0; - } - const addedFilter = filters.filters.find(e => e.field === 'added'); - if (addedFilter) { // convert added filter to object id - filters.filters.splice(filters.filters.findIndex(e => e.field === 'added'), 1); - if (addedFilter.mode === 'in') { - const v = []; // query value - addedFilter.values.forEach(value => { - const date = [new Date(value).setHours(0,0,0,0), new Date(value).setHours(23,59,59,999)]; - v.push({$and: [{ _id: { '$gte': dateToOId(date[0])}}, { _id: { '$lte': dateToOId(date[1])}}]}); - }); - filters.filters.push({mode: 'or', field: '_id', values: v}); - } - else if (addedFilter.mode === 'nin') { - addedFilter.values = addedFilter.values.sort(); - const v = []; // query value + // evaluate sort parameter from 'color-asc' to ['color', 1] + filters.sort = filters.sort.split('-'); + filters.sort[0] = filters.sort[0] === 'added' ? '_id' : filters.sort[0]; // route added sorting criteria to _id + filters.sort[1] = filters.sort[1] === 'desc' ? -1 : 1; + if (!filters['to-page']) { // set to-page default + filters['to-page'] = 0; + } + const addedFilter = filters.filters.find(e => e.field === 'added'); + if (addedFilter) { // convert added filter to object id + filters.filters.splice(filters.filters.findIndex(e => e.field === 'added'), 1); + if (addedFilter.mode === 'in') { + const v = []; // query value + addedFilter.values.forEach(value => { + const date = [new Date(value).setHours(0,0,0,0), new Date(value).setHours(23,59,59,999)]; + v.push({$and: [{ _id: { '$gte': dateToOId(date[0])}}, { _id: { '$lte': dateToOId(date[1])}}]}); + }); + filters.filters.push({mode: 'or', field: '_id', values: v}); + } + else if (addedFilter.mode === 'nin') { + addedFilter.values = addedFilter.values.sort(); + const v = []; // query value - for (let i = 0; i <= addedFilter.values.length; i ++) { - v[i] = {$and: []}; - if (i > 0) { - const date = new Date(addedFilter.values[i - 1]).setHours(23,59,59,999); - v[i].$and.push({ _id: { '$gt': dateToOId(date)}}) ; - } - if (i < addedFilter.values.length) { - const date = new Date(addedFilter.values[i]).setHours(0,0,0,0); - v[i].$and.push({ _id: { '$lt': dateToOId(date)}}) ; - } - } - filters.filters.push({mode: 'or', field: '_id', values: v}); - } - else { - // start and end of day - const date = [new Date(addedFilter.values[0]).setHours(0,0,0,0), - new Date(addedFilter.values[0]).setHours(23,59,59,999)]; - if (addedFilter.mode === 'lt') { // lt start - filters.filters.push({mode: 'lt', field: '_id', values: [dateToOId(date[0])]}); - } - if (addedFilter.mode === 'eq' || addedFilter.mode === 'lte') { // lte end - filters.filters.push({mode: 'lte', field: '_id', values: [dateToOId(date[1])]}); - } - if (addedFilter.mode === 'gt') { // gt end - filters.filters.push({mode: 'gt', field: '_id', values: [dateToOId(date[1])]}); - } - if (addedFilter.mode === 'eq' || addedFilter.mode === 'gte') { // gte start - filters.filters.push({mode: 'gte', field: '_id', values: [dateToOId(date[0])]}); - } - if (addedFilter.mode === 'ne') { - filters.filters.push({mode: 'or', field: '_id', - values: [{ _id: { '$lt': dateToOId(date[0])}}, { _id: { '$gt': dateToOId(date[1])}}]}); - } - } - } + for (let i = 0; i <= addedFilter.values.length; i ++) { + v[i] = {$and: []}; + if (i > 0) { + const date = new Date(addedFilter.values[i - 1]).setHours(23,59,59,999); + v[i].$and.push({ _id: { '$gt': dateToOId(date)}}) ; + } + if (i < addedFilter.values.length) { + const date = new Date(addedFilter.values[i]).setHours(0,0,0,0); + v[i].$and.push({ _id: { '$lt': dateToOId(date)}}) ; + } + } + filters.filters.push({mode: 'or', field: '_id', values: v}); + } + else { + // start and end of day + const date = [new Date(addedFilter.values[0]).setHours(0,0,0,0), + new Date(addedFilter.values[0]).setHours(23,59,59,999)]; + if (addedFilter.mode === 'lt') { // lt start + filters.filters.push({mode: 'lt', field: '_id', values: [dateToOId(date[0])]}); + } + if (addedFilter.mode === 'eq' || addedFilter.mode === 'lte') { // lte end + filters.filters.push({mode: 'lte', field: '_id', values: [dateToOId(date[1])]}); + } + if (addedFilter.mode === 'gt') { // gt end + filters.filters.push({mode: 'gt', field: '_id', values: [dateToOId(date[1])]}); + } + if (addedFilter.mode === 'eq' || addedFilter.mode === 'gte') { // gte start + filters.filters.push({mode: 'gte', field: '_id', values: [dateToOId(date[0])]}); + } + if (addedFilter.mode === 'ne') { + filters.filters.push({mode: 'or', field: '_id', + values: [{ _id: { '$lt': dateToOId(date[0])}}, { _id: { '$gt': dateToOId(date[1])}}]}); + } + } + } - const sortFilterKeys = filters.filters.map(e => e.field); + const sortFilterKeys = filters.filters.map(e => e.field); - let collection; - const query = []; - let queryPtr = query; - queryPtr.push({$match: {$and: []}}); + let collection; + const query = []; + let queryPtr = query; + queryPtr.push({$match: {$and: []}}); - if (filters.sort[0].indexOf('measurements.') >= 0) { // sorting with measurements as starting collection - collection = MeasurementModel; - const [,measurementName, measurementParam] = filters.sort[0].split('.'); - const measurementTemplates = await MeasurementTemplateModel.find({name: measurementName}) - .lean().exec().catch(err => {next(err);}); - if (measurementTemplates instanceof Error) return; - if (!measurementTemplates) { - return res.status(400).json({status: 'Invalid body format', details: filters.sort[0] + ' not found'}); - } - let sortStartValue = null; - if (filters['from-id']) { // from-id specified, fetch values for sorting - const fromSample = await MeasurementModel.findOne({sample_id: mongoose.Types.ObjectId(filters['from-id'])}) - .lean().exec().catch(err => {next(err);}); - if (fromSample instanceof Error) return; - if (!fromSample) { - return res.status(400).json({status: 'Invalid body format', details: 'from-id not found'}); - } - sortStartValue = fromSample.values[measurementParam]; - } - // find measurements to sort - queryPtr[0].$match.$and.push({measurement_template: {$in: measurementTemplates.map(e => e._id)}}); - if (filters.filters.find(e => e.field === filters.sort[0])) { // sorted measurement should also be filtered - queryPtr[0].$match.$and.push(...filterQueries(filters.filters.filter(e => e.field === filters.sort[0]) - .map(e => {e.field = 'values.' + e.field.split('.')[2]; return e; }))); - } - queryPtr.push( - ...sortQuery(filters, ['values.' + measurementParam, 'sample_id'], sortStartValue), // sort measurements - {$replaceRoot: {newRoot: {measurement: '$$ROOT'}}}, // fetch samples and restructure them to fit sample structure - {$lookup: {from: 'samples', localField: 'measurement.sample_id', foreignField: '_id', as: 'sample'}}, - {$match: statusQuery(filters, 'sample.status')}, // filter out wrong status once samples were added - {$addFields: {['sample.' + measurementName]: '$measurement.values'}}, // more restructuring - {$replaceRoot: {newRoot: {$mergeObjects: [{$arrayElemAt: ['$sample', 0]}, {}]}}} - ); - } - else { // sorting with samples as starting collection - collection = SampleModel; - queryPtr[0].$match.$and.push(statusQuery(filters, 'status')); + if (filters.sort[0].indexOf('measurements.') >= 0) { // sorting with measurements as starting collection + collection = MeasurementModel; + const [,measurementName, measurementParam] = filters.sort[0].split('.'); + const measurementTemplates = await MeasurementTemplateModel.find({name: measurementName}) + .lean().exec().catch(err => {next(err);}); + if (measurementTemplates instanceof Error) return; + if (!measurementTemplates) { + return res.status(400).json({status: 'Invalid body format', details: filters.sort[0] + ' not found'}); + } + let sortStartValue = null; + if (filters['from-id']) { // from-id specified, fetch values for sorting + const fromSample = await MeasurementModel.findOne({sample_id: mongoose.Types.ObjectId(filters['from-id'])}) + .lean().exec().catch(err => {next(err);}); + if (fromSample instanceof Error) return; + if (!fromSample) { + return res.status(400).json({status: 'Invalid body format', details: 'from-id not found'}); + } + sortStartValue = fromSample.values[measurementParam]; + } + // find measurements to sort + queryPtr[0].$match.$and.push({measurement_template: {$in: measurementTemplates.map(e => e._id)}}); + if (filters.filters.find(e => e.field === filters.sort[0])) { // sorted measurement should also be filtered + queryPtr[0].$match.$and.push(...filterQueries(filters.filters.filter(e => e.field === filters.sort[0]) + .map(e => {e.field = 'values.' + e.field.split('.')[2]; return e; }))); + } + queryPtr.push( + ...sortQuery(filters, ['values.' + measurementParam, 'sample_id'], sortStartValue), // sort measurements + {$replaceRoot: {newRoot: {measurement: '$$ROOT'}}}, // fetch samples and restructure them to fit sample structure + {$lookup: {from: 'samples', localField: 'measurement.sample_id', foreignField: '_id', as: 'sample'}}, + {$match: statusQuery(filters, 'sample.status')}, // filter out wrong status once samples were added + {$addFields: {['sample.' + measurementName]: '$measurement.values'}}, // more restructuring + {$replaceRoot: {newRoot: {$mergeObjects: [{$arrayElemAt: ['$sample', 0]}, {}]}}} + ); + } + else { // sorting with samples as starting collection + collection = SampleModel; + queryPtr[0].$match.$and.push(statusQuery(filters, 'status')); - // sorting for sample keys - if (SampleValidate.sampleKeys.indexOf(filters.sort[0]) >= 0 || /condition\./.test(filters.sort[0])) { - let sortStartValue = null; - if (filters['from-id']) { // from-id specified - const fromSample = await SampleModel.findById(filters['from-id']).lean().exec().catch(err => { - next(err); - }); - if (fromSample instanceof Error) return; - if (!fromSample) { - return res.status(400).json({status: 'Invalid body format', details: 'from-id not found'}); - } - sortStartValue = fromSample[filters.sort[0]]; - } - queryPtr.push(...sortQuery(filters, [filters.sort[0], '_id'], sortStartValue)); - } - else { // add sort key to list to add field later - sortFilterKeys.push(filters.sort[0]); - } - } + // sorting for sample keys + if (SampleValidate.sampleKeys.indexOf(filters.sort[0]) >= 0 || /condition\./.test(filters.sort[0])) { + let sortStartValue = null; + if (filters['from-id']) { // from-id specified + const fromSample = await SampleModel.findById(filters['from-id']).lean().exec().catch(err => { + next(err); + }); + if (fromSample instanceof Error) return; + if (!fromSample) { + return res.status(400).json({status: 'Invalid body format', details: 'from-id not found'}); + } + sortStartValue = fromSample[filters.sort[0]]; + } + queryPtr.push(...sortQuery(filters, [filters.sort[0], '_id'], sortStartValue)); + } + else { // add sort key to list to add field later + sortFilterKeys.push(filters.sort[0]); + } + } - addFilterQueries(queryPtr, filters.filters.filter( - e => (SampleValidate.sampleKeys.indexOf(e.field) >= 0) || /condition\./.test(e.field)) - ); // sample filters + addFilterQueries(queryPtr, filters.filters.filter( + e => (SampleValidate.sampleKeys.indexOf(e.field) >= 0) || /condition\./.test(e.field)) + ); // sample filters - let materialQuery = []; // put material query together separate first to reuse for first-id - let materialAdded = false; - if (sortFilterKeys.find(e => /material\./.test(e))) { // add material fields - materialAdded = true; - materialQuery.push( // add material properties - {$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}}, - {$addFields: {material: {$arrayElemAt: ['$material', 0]}}} - ); - const baseMFilters = sortFilterKeys.filter(e => /material\./.test(e)) - .filter(e => ['material.supplier', 'material.group', 'material.number'].indexOf(e) < 0); - // base material filters - addFilterQueries(materialQuery, filters.filters.filter(e => baseMFilters.indexOf(e.field) >= 0)); - if (sortFilterKeys.find(e => e === 'material.supplier')) { // add supplier if needed - materialQuery.push( - {$lookup: { - from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier'} - }, - {$addFields: {'material.supplier': {$arrayElemAt: ['$material.supplier.name', 0]}}} - ); - } - if (sortFilterKeys.find(e => e === 'material.group')) { // add group if needed - materialQuery.push( - {$lookup: { - from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group' } - }, - {$addFields: {'material.group': { $arrayElemAt: ['$material.group.name', 0]}}} - ); - } - const specialMFilters = sortFilterKeys.filter(e => /material\./.test(e)) - .filter(e => ['material.supplier', 'material.group', 'material.number'].indexOf(e) >= 0); - // base material filters - addFilterQueries(materialQuery, filters.filters.filter(e => specialMFilters.indexOf(e.field) >= 0)); - queryPtr.push(...materialQuery); - if (/material\./.test(filters.sort[0])) { // sort by material key - let sortStartValue = null; - if (filters['from-id']) { // from-id specified - const fromSample = await SampleModel.aggregate( - [{$match: {_id: mongoose.Types.ObjectId(filters['from-id'])}}, ...materialQuery] - ).exec().catch(err => {next(err);}); - if (fromSample instanceof Error) return; - if (!fromSample) { - return res.status(400).json({status: 'Invalid body format', details: 'from-id not found'}); - } - const filterKey = filters.sort[0].split('.'); - if (filterKey.length === 2) { - sortStartValue = fromSample[0][filterKey[0]][filterKey[1]]; - } - else { - sortStartValue = fromSample[0][filterKey[0]]; - } - } - queryPtr.push(...sortQuery(filters, [filters.sort[0], '_id'], sortStartValue)); - } - } + let materialQuery = []; // put material query together separate first to reuse for first-id + let materialAdded = false; + if (sortFilterKeys.find(e => /material\./.test(e))) { // add material fields + materialAdded = true; + materialQuery.push( // add material properties + {$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}}, + {$addFields: {material: {$arrayElemAt: ['$material', 0]}}} + ); + const baseMFilters = sortFilterKeys.filter(e => /material\./.test(e)) + .filter(e => ['material.supplier', 'material.group', 'material.number'].indexOf(e) < 0); + // base material filters + addFilterQueries(materialQuery, filters.filters.filter(e => baseMFilters.indexOf(e.field) >= 0)); + if (sortFilterKeys.find(e => e === 'material.supplier')) { // add supplier if needed + materialQuery.push( + {$lookup: { + from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier'} + }, + {$addFields: {'material.supplier': {$arrayElemAt: ['$material.supplier.name', 0]}}} + ); + } + if (sortFilterKeys.find(e => e === 'material.group')) { // add group if needed + materialQuery.push( + {$lookup: { + from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group' } + }, + {$addFields: {'material.group': { $arrayElemAt: ['$material.group.name', 0]}}} + ); + } + const specialMFilters = sortFilterKeys.filter(e => /material\./.test(e)) + .filter(e => ['material.supplier', 'material.group', 'material.number'].indexOf(e) >= 0); + // base material filters + addFilterQueries(materialQuery, filters.filters.filter(e => specialMFilters.indexOf(e.field) >= 0)); + queryPtr.push(...materialQuery); + if (/material\./.test(filters.sort[0])) { // sort by material key + let sortStartValue = null; + if (filters['from-id']) { // from-id specified + const fromSample = await SampleModel.aggregate( + [{$match: {_id: mongoose.Types.ObjectId(filters['from-id'])}}, ...materialQuery] + ).exec().catch(err => {next(err);}); + if (fromSample instanceof Error) return; + if (!fromSample) { + return res.status(400).json({status: 'Invalid body format', details: 'from-id not found'}); + } + const filterKey = filters.sort[0].split('.'); + if (filterKey.length === 2) { + sortStartValue = fromSample[0][filterKey[0]][filterKey[1]]; + } + else { + sortStartValue = fromSample[0][filterKey[0]]; + } + } + queryPtr.push(...sortQuery(filters, [filters.sort[0], '_id'], sortStartValue)); + } + } - if (sortFilterKeys.find(e => e === 'measurements')) { // filter for samples without measurements - queryPtr.push({$lookup: { - from: 'measurements', let: {sId: '$_id'}, - pipeline: [{$match:{$expr:{$and:[{$eq:['$sample_id','$$sId']}]}}}, {$project: {_id: true}}], - as: 'measurementCount' - }}, - {$match: {measurementCount: {$size: 0}}} - ); - } - const measurementFilterFields = _.uniq(sortFilterKeys.filter(e => /measurements\./.test(e)) - .map(e => e.split('.')[1])); // filter measurement names and remove duplicates from parameters - if (sortFilterKeys.find(e => /measurements\./.test(e))) { // add measurement fields - const measurementTemplates = await MeasurementTemplateModel.find({name: {$in: measurementFilterFields}}) - .lean().exec().catch(err => {next(err);}); - if (measurementTemplates instanceof Error) return; - if (measurementTemplates.length < measurementFilterFields.length) { - return res.status(400).json({status: 'Invalid body format', details: 'Measurement key not found'}); - } - const pipeline: any[] = [{$match: {$expr: {$and: [ - {$eq: ['$sample_id', '$$sId']}, - {$in: ['$measurement_template', measurementTemplates.map(e => mongoose.Types.ObjectId(e._id))]} - ]}}} - ]; - if (measurementFilterFields.indexOf(globals.spectrum.spectrum) >= 0) { // filter out dpts - pipeline.push( - {$project: {['values.' + globals.spectrum.dpt]: false}}, - {$addFields: {'values._id': '$_id'}} - ); - } - queryPtr.push({$lookup: { - from: 'measurements', let: {sId: '$_id'}, - pipeline: pipeline, - as: 'measurements' - }}); - const groupedMeasurementTemplates = measurementTemplates.reduce((s, e) => { - if (s.hasOwnProperty(e.name)) { - s[e.name].push(e); - } - else { - s[e.name] = [e]; - } - return s; - }, {}); - Object.values(groupedMeasurementTemplates).forEach(templates => { - addMeasurements(queryPtr, templates); - }); - addFilterQueries(queryPtr, filters.filters - .filter(e => sortFilterKeys.filter(e => /measurements\./.test(e)).indexOf(e.field) >= 0) - .map(e => {e.field = e.field.replace('measurements.', ''); return e; }) - ); // measurement filters - } + if (sortFilterKeys.find(e => e === 'measurements')) { // filter for samples without measurements + queryPtr.push({$lookup: { + from: 'measurements', let: {sId: '$_id'}, + pipeline: [{$match:{$expr:{$and:[{$eq:['$sample_id','$$sId']}]}}}, {$project: {_id: true}}], + as: 'measurementCount' + }}, + {$match: {measurementCount: {$size: 0}}} + ); + } + const measurementFilterFields = _.uniq(sortFilterKeys.filter(e => /measurements\./.test(e)) + .map(e => e.split('.')[1])); // filter measurement names and remove duplicates from parameters + if (sortFilterKeys.find(e => /measurements\./.test(e))) { // add measurement fields + const measurementTemplates = await MeasurementTemplateModel.find({name: {$in: measurementFilterFields}}) + .lean().exec().catch(err => {next(err);}); + if (measurementTemplates instanceof Error) return; + if (measurementTemplates.length < measurementFilterFields.length) { + return res.status(400).json({status: 'Invalid body format', details: 'Measurement key not found'}); + } + const pipeline: any[] = [{$match: {$expr: {$and: [ + {$eq: ['$sample_id', '$$sId']}, + {$in: ['$measurement_template', measurementTemplates.map(e => mongoose.Types.ObjectId(e._id))]} + ]}}} + ]; + if (measurementFilterFields.indexOf(globals.spectrum.spectrum) >= 0) { // filter out dpts + pipeline.push( + {$project: {['values.' + globals.spectrum.dpt]: false}}, + {$addFields: {'values._id': '$_id'}} + ); + } + queryPtr.push({$lookup: { + from: 'measurements', let: {sId: '$_id'}, + pipeline: pipeline, + as: 'measurements' + }}); + const groupedMeasurementTemplates = measurementTemplates.reduce((s, e) => { + if (s.hasOwnProperty(e.name)) { + s[e.name].push(e); + } + else { + s[e.name] = [e]; + } + return s; + }, {}); + Object.values(groupedMeasurementTemplates).forEach(templates => { + addMeasurements(queryPtr, templates); + }); + addFilterQueries(queryPtr, filters.filters + .filter(e => sortFilterKeys.filter(e => /measurements\./.test(e)).indexOf(e.field) >= 0) + .map(e => {e.field = e.field.replace('measurements.', ''); return e; }) + ); // measurement filters + } - if (sortFilterKeys.find(e => e === 'notes.comment')) { - addNotes(queryPtr); - addFilterQueries(queryPtr, filters.filters.filter(e => e.field === 'notes.comment')); - } + if (sortFilterKeys.find(e => e === 'notes.comment')) { + addNotes(queryPtr); + addFilterQueries(queryPtr, filters.filters.filter(e => e.field === 'notes.comment')); + } - // count total number of items before $skip and $limit, only works when from-id is not specified and spectra are not - // included - if (!filters.fields.find(e => - e.indexOf(globals.spectrum.spectrum + '.' + globals.spectrum.dpt) >= 0) && !filters['from-id'] - ) { - queryPtr.push({$facet: {count: [{$count: 'count'}], samples: []}}); - queryPtr = queryPtr[queryPtr.length - 1].$facet.samples; // add rest of aggregation pipeline into $facet - } + // count total number of items before $skip and $limit, only works when from-id is not specified and spectra are not + // included + if (!filters.fields.find(e => + e.indexOf(globals.spectrum.spectrum + '.' + globals.spectrum.dpt) >= 0) && !filters['from-id'] + ) { + queryPtr.push({$facet: {count: [{$count: 'count'}], samples: []}}); + queryPtr = queryPtr[queryPtr.length - 1].$facet.samples; // add rest of aggregation pipeline into $facet + } - // paging - if (filters['to-page']) { - // number to skip, if going back pages, one page has to be skipped less but on sample more - queryPtr.push({$skip: Math.abs(filters['to-page'] + Number(filters['to-page'] < 0)) * filters['page-size'] + - Number(filters['to-page'] < 0)}) - } - if (filters['page-size']) { - queryPtr.push({$limit: filters['page-size']}); - } + // paging + if (filters['to-page']) { + // number to skip, if going back pages, one page has to be skipped less but on sample more + queryPtr.push({$skip: Math.abs(filters['to-page'] + Number(filters['to-page'] < 0)) * filters['page-size'] + + Number(filters['to-page'] < 0)}) + } + if (filters['page-size']) { + queryPtr.push({$limit: filters['page-size']}); + } - const fieldsToAdd = filters.fields.filter(e => // fields to add - sortFilterKeys.indexOf(e) < 0 // field was not in filter - && e !== filters.sort[0] // field was not in sort - ); + const fieldsToAdd = filters.fields.filter(e => // fields to add + sortFilterKeys.indexOf(e) < 0 // field was not in filter + && e !== filters.sort[0] // field was not in sort + ); - if (fieldsToAdd.find(e => /^notes(\..+|$)/m.test(e))) { // add notes - addNotes(queryPtr); - } + if (fieldsToAdd.find(e => /^notes(\..+|$)/m.test(e))) { // add notes + addNotes(queryPtr); + } - if (fieldsToAdd.find(e => /material\./.test(e)) && !materialAdded) { // add material, was not added already - queryPtr.push( - {$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}}, - {$addFields: {material: { $arrayElemAt: ['$material', 0]}}} - ); - } - if (fieldsToAdd.indexOf('material.supplier') >= 0) { // add supplier if needed - queryPtr.push( - {$lookup: { - from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier' - }}, - {$addFields: {'material.supplier': {$arrayElemAt: ['$material.supplier.name', 0]}}} - ); - } - if (fieldsToAdd.indexOf('material.group') >= 0) { // add group if needed - queryPtr.push( - {$lookup: { - from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group' - }}, - {$addFields: {'material.group': { $arrayElemAt: ['$material.group.name', 0]}}} - ); - } + if (fieldsToAdd.find(e => /material\./.test(e)) && !materialAdded) { // add material, was not added already + queryPtr.push( + {$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}}, + {$addFields: {material: { $arrayElemAt: ['$material', 0]}}} + ); + } + if (fieldsToAdd.indexOf('material.supplier') >= 0) { // add supplier if needed + queryPtr.push( + {$lookup: { + from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier' + }}, + {$addFields: {'material.supplier': {$arrayElemAt: ['$material.supplier.name', 0]}}} + ); + } + if (fieldsToAdd.indexOf('material.group') >= 0) { // add group if needed + queryPtr.push( + {$lookup: { + from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group' + }}, + {$addFields: {'material.group': { $arrayElemAt: ['$material.group.name', 0]}}} + ); + } - let measurementFieldsFields: string[] = _.uniq( - fieldsToAdd.filter(e => /measurements\./.test(e)).map(e => e.split('.')[1]) - ); // filter measurement names and remove duplicates from parameters - if (fieldsToAdd.find(e => /measurements\./.test(e))) { // add measurement fields - const measurementTemplates = await MeasurementTemplateModel.find({name: {$in: measurementFieldsFields}}) - .lean().exec().catch(err => {next(err);}); - if (measurementTemplates instanceof Error) return; - if (measurementTemplates.length < measurementFieldsFields.length) { - return res.status(400).json({status: 'Invalid body format', details: 'Measurement key not found'}); - } - // use different lookup methods with and without dpt for the best performance - if (fieldsToAdd.find(e => new RegExp('measurements\\.' + globals.spectrum.spectrum).test(e))) { // with dpt - // spectrum was already used for filters - if (sortFilterKeys.find(e => new RegExp('measurements\\.' + globals.spectrum.spectrum).test(e))) { - queryPtr.push( - {$lookup: {from: 'measurements', localField: 'spectrum._id', foreignField: '_id', as: 'measurements'}} - ); - } - else { - queryPtr.push( - {$lookup: {from: 'measurements', localField: '_id', foreignField: 'sample_id', as: 'measurements'}} - ); - } - } - else { - queryPtr.push({$lookup: { - from: 'measurements', let: {sId: '$_id'}, - pipeline: [{$match: {$expr: {$and: [ - {$eq: ['$sample_id', '$$sId']}, - {$in: ['$measurement_template', measurementTemplates.map(e => mongoose.Types.ObjectId(e._id))]} - ]}}}, - {$project: _.merge( - filters.fields.filter(e => /measurements\./.test(e)) - .map(e => 'values.' + e.split('.')[2]).reduce((s, e) => {s[e] = true; return s; }, {}), - {measurement_template: true, status: true, sample_id: true} - )} - ], - as: 'measurements' - }}); - } - const groupedMeasurementTemplates = measurementTemplates.reduce((s, e) => { - if (s.hasOwnProperty(e.name)) { - s[e.name].push(e); - } - else { - s[e.name] = [e]; - } - return s; - }, {}); - Object.values(groupedMeasurementTemplates).forEach(templates => { - addMeasurements(queryPtr, templates); - }); - queryPtr.push({$project: {measurements: 0}}); - } + let measurementFieldsFields: string[] = _.uniq( + fieldsToAdd.filter(e => /measurements\./.test(e)).map(e => e.split('.')[1]) + ); // filter measurement names and remove duplicates from parameters + if (fieldsToAdd.find(e => /measurements\./.test(e))) { // add measurement fields + const measurementTemplates = await MeasurementTemplateModel.find({name: {$in: measurementFieldsFields}}) + .lean().exec().catch(err => {next(err);}); + if (measurementTemplates instanceof Error) return; + if (measurementTemplates.length < measurementFieldsFields.length) { + return res.status(400).json({status: 'Invalid body format', details: 'Measurement key not found'}); + } + // use different lookup methods with and without dpt for the best performance + if (fieldsToAdd.find(e => new RegExp('measurements\\.' + globals.spectrum.spectrum).test(e))) { // with dpt + // spectrum was already used for filters + if (sortFilterKeys.find(e => new RegExp('measurements\\.' + globals.spectrum.spectrum).test(e))) { + queryPtr.push( + {$lookup: {from: 'measurements', localField: 'spectrum._id', foreignField: '_id', as: 'measurements'}} + ); + } + else { + queryPtr.push( + {$lookup: {from: 'measurements', localField: '_id', foreignField: 'sample_id', as: 'measurements'}} + ); + } + } + else { + queryPtr.push({$lookup: { + from: 'measurements', let: {sId: '$_id'}, + pipeline: [{$match: {$expr: {$and: [ + {$eq: ['$sample_id', '$$sId']}, + {$in: ['$measurement_template', measurementTemplates.map(e => mongoose.Types.ObjectId(e._id))]} + ]}}}, + {$project: _.merge( + filters.fields.filter(e => /measurements\./.test(e)) + .map(e => 'values.' + e.split('.')[2]).reduce((s, e) => {s[e] = true; return s; }, {}), + {measurement_template: true, status: true, sample_id: true} + )} + ], + as: 'measurements' + }}); + } + const groupedMeasurementTemplates = measurementTemplates.reduce((s, e) => { + if (s.hasOwnProperty(e.name)) { + s[e.name].push(e); + } + else { + s[e.name] = [e]; + } + return s; + }, {}); + Object.values(groupedMeasurementTemplates).forEach(templates => { + addMeasurements(queryPtr, templates); + }); + queryPtr.push({$project: {measurements: 0}}); + } - const projection = filters.fields.map(e => e.replace('measurements.', '')) - .reduce((s, e) => {s[e] = true; return s; }, {}); - if (filters.fields.indexOf('_id') < 0 && filters.fields.indexOf('added') < 0) { // disable _id explicitly - projection._id = false; - } - queryPtr.push({$project: projection}); - // use streaming when including spectrum files - if (!fieldsToAdd.find(e => e.indexOf(globals.spectrum.spectrum + '.' + globals.spectrum.dpt) >= 0)) { - collection.aggregate(query).allowDiskUse(true).exec((err, data) => { - if (err) return next(err); - if (data[0] && data[0].count) { - res.header('x-total-items', data[0].count.length > 0 ? data[0].count[0].count : 0); - res.header('Access-Control-Expose-Headers', 'x-total-items'); - data = data[0].samples; - } - if (filters.fields.indexOf('added') >= 0) { // add added date - data.map(e => { - e.added = e._id.getTimestamp(); - if (filters.fields.indexOf('_id') < 0) { - delete e._id; - } - return e - }); - } - if (filters['to-page'] < 0) { - data.reverse(); - } - const measurementFields = _.uniq( - [filters.sort[0].split('.')[1], - ...measurementFilterFields, ...measurementFieldsFields] - ); - if (filters.output === 'csv') { // output as csv - csv(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields))), (err, data) => { - if (err) return next(err); - res.set('Content-Type', 'text/csv'); - res.set('Content-Disposition', 'attachment; filename="samples.csv"'); - res.send(data); - }); - } - else if (filters.output === 'flatten') { - res.json(_.compact(data.map(e => flatten(SampleValidate.output(e, 'refs', measurementFields), true)))); - } - else { // validate all and filter null values from validation errors - res.json(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields)))); - } - }); - } - else { - res.writeHead(200, {'Content-Type': 'application/json; charset=utf-8'}); - res.write('['); - let count = 0; - const stream = collection.aggregate(query).allowDiskUse(true).cursor().exec(); - stream.on('data', data => { - if (filters.fields.indexOf('added') >= 0) { // add added date - data.added = data._id.getTimestamp(); - if (filters.fields.indexOf('_id') < 0) { - delete data._id; - } - } - if (filters.output === 'flatten') { - data = flatten(data, true); - } - res.write((count === 0 ? '' : ',\n') + JSON.stringify(data)); count ++; - }); - stream.on('error', err => { - console.error(err); - }); - stream.on('close', () => { - res.write(']'); - res.end(); - }); - } + const projection = filters.fields.map(e => e.replace('measurements.', '')) + .reduce((s, e) => {s[e] = true; return s; }, {}); + if (filters.fields.indexOf('_id') < 0 && filters.fields.indexOf('added') < 0) { // disable _id explicitly + projection._id = false; + } + queryPtr.push({$project: projection}); + // use streaming when including spectrum files + if (!fieldsToAdd.find(e => e.indexOf(globals.spectrum.spectrum + '.' + globals.spectrum.dpt) >= 0)) { + collection.aggregate(query).allowDiskUse(true).exec((err, data) => { + if (err) return next(err); + if (data[0] && data[0].count) { + res.header('x-total-items', data[0].count.length > 0 ? data[0].count[0].count : 0); + res.header('Access-Control-Expose-Headers', 'x-total-items'); + data = data[0].samples; + } + if (filters.fields.indexOf('added') >= 0) { // add added date + data.map(e => { + e.added = e._id.getTimestamp(); + if (filters.fields.indexOf('_id') < 0) { + delete e._id; + } + return e + }); + } + if (filters['to-page'] < 0) { + data.reverse(); + } + const measurementFields = _.uniq( + [filters.sort[0].split('.')[1], + ...measurementFilterFields, ...measurementFieldsFields] + ); + if (filters.output === 'csv') { // output as csv + csv(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields))), (err, data) => { + if (err) return next(err); + res.set('Content-Type', 'text/csv'); + res.set('Content-Disposition', 'attachment; filename="samples.csv"'); + res.send(data); + }); + } + else if (filters.output === 'flatten') { + res.json(_.compact(data.map(e => flatten(SampleValidate.output(e, 'refs', measurementFields), true)))); + } + else { // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields)))); + } + }); + } + else { + res.writeHead(200, {'Content-Type': 'application/json; charset=utf-8'}); + res.write('['); + let count = 0; + const stream = collection.aggregate(query).allowDiskUse(true).cursor().exec(); + stream.on('data', data => { + if (filters.fields.indexOf('added') >= 0) { // add added date + data.added = data._id.getTimestamp(); + if (filters.fields.indexOf('_id') < 0) { + delete data._id; + } + } + if (filters.output === 'flatten') { + data = flatten(data, true); + } + res.write((count === 0 ? '' : ',\n') + JSON.stringify(data)); count ++; + }); + stream.on('error', err => { + console.error(err); + }); + stream.on('close', () => { + res.write(']'); + res.end(); + }); + } }); router.get(`/samples/:state(${globals.status.new}|${globals.status.del})`, (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - SampleModel.find({status: req.params.state}).lean().exec((err, data) => { - if (err) return next(err); - // validate all and filter null values from validation errors - res.json(_.compact(data.map(e => SampleValidate.output(e)))); - }); + SampleModel.find({status: req.params.state}).lean().exec((err, data) => { + if (err) return next(err); + // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => SampleValidate.output(e)))); + }); }); router.get('/samples/count', (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; - SampleModel.estimatedDocumentCount((err, data) => { - if (err) return next(err); - res.json({count: data}); - }); + SampleModel.estimatedDocumentCount((err, data) => { + if (err) return next(err); + res.json({count: data}); + }); }); router.get('/sample/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; - SampleModel.findById(req.params.id).populate('material_id').populate('user_id', 'name').populate('note_id') - .exec(async (err, sampleData: any) => { - if (err) return next(err); - await sampleReturn(sampleData, req, res, next); - }); + SampleModel.findById(req.params.id).populate('material_id').populate('user_id', 'name').populate('note_id') + .exec(async (err, sampleData: any) => { + if (err) return next(err); + await sampleReturn(sampleData, req, res, next); + }); }); router.put('/sample/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; - const {error, value: sample} = SampleValidate.input(req.body, 'change'); - if (error) return res400(error, res); + const {error, value: sample} = SampleValidate.input(req.body, 'change'); + if (error) return res400(error, res); - SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => { // check if id exists - if (err) return next(err); - if (!sampleData) { - return res.status(404).json({status: 'Not found'}); - } - if (sampleData.status === globals.status.del) { - return res.status(403).json({status: 'Forbidden'}); - } + SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => { // check if id exists + if (err) return next(err); + if (!sampleData) { + return res.status(404).json({status: 'Not found'}); + } + if (sampleData.status === globals.status.del) { + return res.status(403).json({status: 'Forbidden'}); + } - // only dev and admin are allowed to edit other user's data - if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic')) return; - if (sample.hasOwnProperty('material_id')) { - if (!await materialCheck(sample, res, next)) return; - } - // do not execute check if condition is and was empty - if (sample.hasOwnProperty('condition') && !(_.isEmpty(sample.condition) && _.isEmpty(sampleData.condition))) { - sample.condition = await conditionCheck(sample.condition, 'change', res, next, - !(sampleData.condition.condition_template && - sampleData.condition.condition_template.toString() === sample.condition.condition_template)); - if (!sample.condition) return; - } + // only dev and admin are allowed to edit other user's data + if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic')) return; + if (sample.hasOwnProperty('material_id')) { + if (!await materialCheck(sample, res, next)) return; + } + // do not execute check if condition is and was empty + if (sample.hasOwnProperty('condition') && !(_.isEmpty(sample.condition) && _.isEmpty(sampleData.condition))) { + sample.condition = await conditionCheck(sample.condition, 'change', res, next, + !(sampleData.condition.condition_template && + sampleData.condition.condition_template.toString() === sample.condition.condition_template)); + if (!sample.condition) return; + } - if (sample.hasOwnProperty('notes')) { - let newNotes = true; - 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; - // check if notes were changed - newNotes = !_.isEqual(_.pick(IdValidate.stringify(data), _.keys(sample.notes)), sample.notes); - if (newNotes) { - if (data.hasOwnProperty('custom_fields')) { // update note_fields - customFieldsChange(Object.keys(data.custom_fields), -1, req); - } - await NoteModel.findByIdAndDelete(sampleData.note_id).log(req).lean().exec(err => { // delete old notes - if (err) return console.error(err); - }); - } - } + if (sample.hasOwnProperty('notes')) { + let newNotes = true; + 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; + // check if notes were changed + newNotes = !_.isEqual(_.pick(IdValidate.stringify(data), _.keys(sample.notes)), sample.notes); + if (newNotes) { + if (data.hasOwnProperty('custom_fields')) { // update note_fields + customFieldsChange(Object.keys(data.custom_fields), -1, req); + } + await NoteModel.findByIdAndDelete(sampleData.note_id).log(req).lean().exec(err => { // delete old notes + if (err) return console.error(err); + }); + } + } - if (_.keys(sample.notes).length > 0 && newNotes) { // save new notes - if (!await sampleRefCheck(sample, res, next)) return; - // new custom_fields - if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) { - customFieldsChange(Object.keys(sample.notes.custom_fields), 1, req); - } - let data = await new NoteModel(sample.notes).save().catch(err => { return next(err)}); // save new notes - db.log(req, 'notes', {_id: data._id}, data.toObject()); - delete sample.notes; - sample.note_id = data._id; - } - } + if (_.keys(sample.notes).length > 0 && newNotes) { // save new notes + if (!await sampleRefCheck(sample, res, next)) return; + // new custom_fields + if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) { + customFieldsChange(Object.keys(sample.notes.custom_fields), 1, req); + } + let data = await new NoteModel(sample.notes).save().catch(err => { return next(err)}); // save new notes + db.log(req, 'notes', {_id: data._id}, data.toObject()); + delete sample.notes; + sample.note_id = data._id; + } + } - // check for changes - if (!_.isEqual(_.pick(IdValidate.stringify(sampleData), _.keys(sample)), _.omit(sample, ['notes']))) { - sample.status = globals.status.new; - } + // check for changes + if (!_.isEqual(_.pick(IdValidate.stringify(sampleData), _.keys(sample)), _.omit(sample, ['notes']))) { + sample.status = globals.status.new; + } - await SampleModel.findByIdAndUpdate(req.params.id, sample, {new: true}).log(req).lean().exec((err, data: any) => { - if (err) return next(err); - res.json(SampleValidate.output(data)); - }); + await SampleModel.findByIdAndUpdate(req.params.id, sample, {new: true}).log(req).lean().exec((err, data: any) => { + if (err) return next(err); + res.json(SampleValidate.output(data)); + }); - }); + }); }); router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; - SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => { // check if id exists - if (err) return next(err); - if (!sampleData) { - return res.status(404).json({status: 'Not found'}); - } + SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => { // check if id exists + if (err) return next(err); + if (!sampleData) { + return res.status(404).json({status: 'Not found'}); + } - // only dev and admin are allowed to edit other user's data - if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic')) return; + // only dev and admin are allowed to edit other user's data + if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic')) return; - // set sample status - await SampleModel.findByIdAndUpdate(req.params.id, {status:'deleted'}).log(req).lean().exec(err => { - if (err) return next(err); + // set sample status + await SampleModel.findByIdAndUpdate(req.params.id, {status:'deleted'}).log(req).lean().exec(err => { + if (err) return next(err); - // set status of associated measurements also to deleted - MeasurementModel.updateMany({sample_id: mongoose.Types.ObjectId(req.params.id)}, {status: globals.status.del}) - .log(req).lean().exec(err => { - if (err) return next(err); + // set status of associated measurements also to deleted + MeasurementModel.updateMany({sample_id: mongoose.Types.ObjectId(req.params.id)}, {status: globals.status.del}) + .log(req).lean().exec(err => { + if (err) return next(err); - 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 - customFieldsChange(Object.keys(data.custom_fields), -1, req); - } - res.json({status: 'OK'}); - }); - } - else { - res.json({status: 'OK'}); - } - }); - }); - }); + 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 + customFieldsChange(Object.keys(data.custom_fields), -1, req); + } + res.json({status: 'OK'}); + }); + } + else { + res.json({status: 'OK'}); + } + }); + }); + }); }); router.get('/sample/number/:number', (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; - SampleModel.findOne({number: req.params.number}).populate('material_id').populate('user_id', 'name') - .populate('note_id').exec(async (err, sampleData: any) => { - if (err) return next(err); - await sampleReturn(sampleData, req, res, next); - }); + SampleModel.findOne({number: req.params.number}).populate('material_id').populate('user_id', 'name') + .populate('note_id').exec(async (err, sampleData: any) => { + if (err) return next(err); + await sampleReturn(sampleData, req, res, next); + }); }); router.put('/sample/restore/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - setStatus(globals.status.new, req, res, next); + setStatus(globals.status.new, req, res, next); }); router.put('/sample/validate/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - setStatus(globals.status.val, req, res, next); + setStatus(globals.status.val, req, res, next); }); router.post('/sample/new', async (req, res, next) => { - if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return; - if (!req.body.hasOwnProperty('condition')) { // add empty condition if not specified - req.body.condition = {}; - } + if (!req.body.hasOwnProperty('condition')) { // add empty condition if not specified + req.body.condition = {}; + } - const {error, value: sample} = - SampleValidate.input(req.body, 'new' + (req.authDetails.level === 'admin' ? '-admin' : '')); - if (error) return res400(error, res); + const {error, value: sample} = + SampleValidate.input(req.body, 'new' + (req.authDetails.level === 'admin' ? '-admin' : '')); + if (error) return res400(error, res); - if (!await materialCheck(sample, res, next)) return; - if (!await sampleRefCheck(sample, res, next)) return; + if (!await materialCheck(sample, res, next)) return; + if (!await sampleRefCheck(sample, res, next)) return; - // new custom_fields - if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) { - customFieldsChange(Object.keys(sample.notes.custom_fields), 1, req); - } + // new custom_fields + if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) { + customFieldsChange(Object.keys(sample.notes.custom_fields), 1, req); + } - if (!_.isEmpty(sample.condition)) { // do not execute check if condition is empty - sample.condition = await conditionCheck(sample.condition, 'change', res, next); - if (!sample.condition) return; - } + if (!_.isEmpty(sample.condition)) { // do not execute check if condition is empty + sample.condition = await conditionCheck(sample.condition, 'change', res, next); + if (!sample.condition) return; + } - sample.status = globals.status.new; // set status to new - if (sample.hasOwnProperty('number')) { - if (!await numberCheck(sample, res, next)) return; - } - else { - sample.number = await numberGenerate(sample, req, res, next); - } - if (!sample.number) return; + sample.status = globals.status.new; // set status to new + if (sample.hasOwnProperty('number')) { + if (!await numberCheck(sample, res, next)) return; + } + else { + 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); - db.log(req, 'notes', {_id: data._id}, data.toObject()); - delete sample.notes; - sample.note_id = data._id; - sample.user_id = req.authDetails.id; + await new NoteModel(sample.notes).save((err, data) => { // save notes + if (err) return next(err); + db.log(req, 'notes', {_id: data._id}, data.toObject()); + delete sample.notes; + sample.note_id = data._id; + sample.user_id = req.authDetails.id; - new SampleModel(sample).save((err, data) => { - if (err) return next(err); - db.log(req, 'samples', {_id: data._id}, data.toObject()); - res.json(SampleValidate.output(data.toObject())); - }); - }); + new SampleModel(sample).save((err, data) => { + if (err) return next(err); + db.log(req, 'samples', {_id: data._id}, data.toObject()); + res.json(SampleValidate.output(data.toObject())); + }); + }); }); router.get('/sample/notes/fields', (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return; - NoteFieldModel.find({}).lean().exec((err, data) => { - if (err) return next(err); - // validate all and filter null values from validation errors - res.json(_.compact(data.map(e => NoteFieldValidate.output(e)))); - }) + NoteFieldModel.find({}).lean().exec((err, data) => { + if (err) return next(err); + // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => NoteFieldValidate.output(e)))); + }) }); @@ -678,267 +678,267 @@ const numberBuffer: {[location: string]: number} = {}; // generate number in format Location32, returns false on error async function numberGenerate (sample, req, res, next) { - const sampleData = await SampleModel - .aggregate([ - {$match: {number: new RegExp('^' + req.authDetails.location + '[0-9]+$', 'm')}}, - {$addFields: {sortNumber: {$let: { - vars: {tmp: {$concat: ['000000000000000000000000000000', - {$arrayElemAt: [{$split: - [{$arrayElemAt: [{$split: ['$number', req.authDetails.location]}, 1]}, '_']}, 0]}]}}, - in: {$substrCP: ['$$tmp', {$subtract: [{$strLenCP: '$$tmp'}, 30]}, {$strLenCP: '$$tmp'}]} - }}}}, - {$sort: {sortNumber: -1}}, - {$limit: 1} - ]) - .exec() - .catch(err => next(err)); - if (sampleData instanceof Error) return false; - let number = (sampleData[0] ? Number(sampleData[0].number.replace(/[^0-9]+/g, '')) : 0); - if (numberBuffer[req.authDetails.location] && numberBuffer[req.authDetails.location] >= number) { - number = numberBuffer[req.authDetails.location]; - } - number ++; - numberBuffer[req.authDetails.location] = number; - return req.authDetails.location + number; + const sampleData = await SampleModel + .aggregate([ + {$match: {number: new RegExp('^' + req.authDetails.location + '[0-9]+$', 'm')}}, + {$addFields: {sortNumber: {$let: { + vars: {tmp: {$concat: ['000000000000000000000000000000', + {$arrayElemAt: [{$split: + [{$arrayElemAt: [{$split: ['$number', req.authDetails.location]}, 1]}, '_']}, 0]}]}}, + in: {$substrCP: ['$$tmp', {$subtract: [{$strLenCP: '$$tmp'}, 30]}, {$strLenCP: '$$tmp'}]} + }}}}, + {$sort: {sortNumber: -1}}, + {$limit: 1} + ]) + .exec() + .catch(err => next(err)); + if (sampleData instanceof Error) return false; + let number = (sampleData[0] ? Number(sampleData[0].number.replace(/[^0-9]+/g, '')) : 0); + if (numberBuffer[req.authDetails.location] && numberBuffer[req.authDetails.location] >= number) { + number = numberBuffer[req.authDetails.location]; + } + number ++; + numberBuffer[req.authDetails.location] = number; + return req.authDetails.location + number; } async function numberCheck(sample, res, next) { - 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; + 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; } // validate material_id and color, returns false if invalid async function materialCheck (sample, res, next) { - const materialData = await MaterialModel.findById(sample.material_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'}); - return false; - } - return true; + const materialData = await MaterialModel.findById(sample.material_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'}); + return false; + } + return true; } // validate treatment template, returns false if invalid, otherwise template data async function conditionCheck (condition, param, res, next, checkVersion = true) { - if (!condition.condition_template || !IdValidate.valid(condition.condition_template)) { // template id not found - res.status(400).json({status: 'Condition template not available'}); - return false; - } - const conditionData = await ConditionTemplateModel.findById(condition.condition_template) - .lean().exec().catch(err => next(err)) as any; - if (conditionData instanceof Error) return false; - if (!conditionData) { // template not found - res.status(400).json({status: 'Condition template not available'}); - return false; - } + if (!condition.condition_template || !IdValidate.valid(condition.condition_template)) { // template id not found + res.status(400).json({status: 'Condition template not available'}); + return false; + } + const conditionData = await ConditionTemplateModel.findById(condition.condition_template) + .lean().exec().catch(err => next(err)) as any; + if (conditionData instanceof Error) return false; + if (!conditionData) { // template not found + res.status(400).json({status: 'Condition template not available'}); + return false; + } - if (checkVersion) { - // get all template versions and check if given is latest - const conditionVersions = await ConditionTemplateModel.find({first_id: conditionData.first_id}) - .sort({version: -1}).lean().exec().catch(err => next(err)) as any; - if (conditionVersions instanceof Error) return false; - if (condition.condition_template !== conditionVersions[0]._id.toString()) { // template not latest - res.status(400).json({status: 'Old template version not allowed'}); - return false; - } - } + if (checkVersion) { + // get all template versions and check if given is latest + const conditionVersions = await ConditionTemplateModel.find({first_id: conditionData.first_id}) + .sort({version: -1}).lean().exec().catch(err => next(err)) as any; + if (conditionVersions instanceof Error) return false; + if (condition.condition_template !== conditionVersions[0]._id.toString()) { // template not latest + res.status(400).json({status: 'Old template version not allowed'}); + return false; + } + } - // validate parameters - const {error, value} = - ParametersValidate.input(_.omit(condition, 'condition_template'), conditionData.parameters, param); - if (error) {res400(error, res); return false;} - value.condition_template = condition.condition_template; - return value; + // validate parameters + const {error, value} = + ParametersValidate.input(_.omit(condition, 'condition_template'), conditionData.parameters, param); + if (error) {res400(error, res); return false;} + value.condition_template = condition.condition_template; + return value; } function sampleRefCheck (sample, res, next) { // validate sample_references, resolves false for invalid reference - return new Promise(resolve => { - // there are sample_references - if (sample.notes.hasOwnProperty('sample_references') && sample.notes.sample_references.length > 0) { - let referencesCount = sample.notes.sample_references.length; // count to keep track of running async operations + return new Promise(resolve => { + // there are sample_references + if (sample.notes.hasOwnProperty('sample_references') && sample.notes.sample_references.length > 0) { + let referencesCount = sample.notes.sample_references.length; // count to keep track of running async operations - sample.notes.sample_references.forEach(reference => { - SampleModel.findById(reference.sample_id).lean().exec((err, data) => { - if (err) {next(err); resolve(false)} - if (!data) { - res.status(400).json({status: 'Sample reference not available'}); - return resolve(false); - } - referencesCount --; - if (referencesCount <= 0) { // all async requests done - resolve(true); - } - }); - }); - } - else { - resolve(true); - } - }); + sample.notes.sample_references.forEach(reference => { + SampleModel.findById(reference.sample_id).lean().exec((err, data) => { + if (err) {next(err); resolve(false)} + if (!data) { + res.status(400).json({status: 'Sample reference not available'}); + return resolve(false); + } + referencesCount --; + if (referencesCount <= 0) { // all async requests done + resolve(true); + } + }); + }); + } + else { + resolve(true); + } + }); } function customFieldsChange (fields, amount, req) { // update custom_fields and respective quantities - fields.forEach(field => { - NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: amount}} as any, {new: true}) - .log(req).lean().exec((err, data: any) => { // check if field exists - if (err) return console.error(err); - if (!data) { // new field - new NoteFieldModel({name: field, qty: 1}).save((err, data) => { - if (err) return console.error(err); - db.log(req, 'note_fields', {_id: data._id}, data.toObject()); - }) - } - else if (data.qty <= 0) { // delete document if field is not used anymore - NoteFieldModel.findOneAndDelete({name: field}).log(req).lean().exec(err => { - if (err) return console.error(err); - }); - } - }); - }); + fields.forEach(field => { + NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: amount}} as any, {new: true}) + .log(req).lean().exec((err, data: any) => { // check if field exists + if (err) return console.error(err); + if (!data) { // new field + new NoteFieldModel({name: field, qty: 1}).save((err, data) => { + if (err) return console.error(err); + db.log(req, 'note_fields', {_id: data._id}, data.toObject()); + }) + } + else if (data.qty <= 0) { // delete document if field is not used anymore + NoteFieldModel.findOneAndDelete({name: field}).log(req).lean().exec(err => { + if (err) return console.error(err); + }); + } + }); + }); } function sortQuery(filters, sortKeys, sortStartValue) { // sortKeys = ['primary key', 'secondary key'] - if (filters['from-id']) { // from-id specified - const ssv = sortStartValue !== undefined; // if value is not given, match for existence - if ((filters['to-page'] === 0 && filters.sort[1] === 1) || (filters.sort[1] * filters['to-page'] > 0)) { // asc - return [ - {$match: {$or: [ - {[sortKeys[0]]: ssv ? {$gt: sortStartValue} : {$exists: true}}, - {$and: [ - {[sortKeys[0]]: ssv ? sortStartValue : {$exists: false}}, - {[sortKeys[1]]: {$gte: new mongoose.Types.ObjectId(filters['from-id'])}} - ]} - ]}}, - {$sort: {[sortKeys[0]]: 1, _id: 1}} - ]; - } else { - return [ - {$match: {$or: [ - {[sortKeys[0]]: ssv ? {$lt: sortStartValue} : {$exists: false}}, - {$and: [ - {[sortKeys[0]]: ssv ? sortStartValue : {$exists: true}}, - {[sortKeys[1]]: {$lte: new mongoose.Types.ObjectId(filters['from-id'])}} - ]} - ]}}, - {$sort: {[sortKeys[0]]: -1, _id: -1}} - ]; - } - } else { // sort from beginning - return [{$sort: {[sortKeys[0]]: filters.sort[1], [sortKeys[1]]: filters.sort[1]}}]; // set _id as secondary sort - } + if (filters['from-id']) { // from-id specified + const ssv = sortStartValue !== undefined; // if value is not given, match for existence + if ((filters['to-page'] === 0 && filters.sort[1] === 1) || (filters.sort[1] * filters['to-page'] > 0)) { // asc + return [ + {$match: {$or: [ + {[sortKeys[0]]: ssv ? {$gt: sortStartValue} : {$exists: true}}, + {$and: [ + {[sortKeys[0]]: ssv ? sortStartValue : {$exists: false}}, + {[sortKeys[1]]: {$gte: new mongoose.Types.ObjectId(filters['from-id'])}} + ]} + ]}}, + {$sort: {[sortKeys[0]]: 1, _id: 1}} + ]; + } else { + return [ + {$match: {$or: [ + {[sortKeys[0]]: ssv ? {$lt: sortStartValue} : {$exists: false}}, + {$and: [ + {[sortKeys[0]]: ssv ? sortStartValue : {$exists: true}}, + {[sortKeys[1]]: {$lte: new mongoose.Types.ObjectId(filters['from-id'])}} + ]} + ]}}, + {$sort: {[sortKeys[0]]: -1, _id: -1}} + ]; + } + } else { // sort from beginning + return [{$sort: {[sortKeys[0]]: filters.sort[1], [sortKeys[1]]: filters.sort[1]}}]; // set _id as secondary sort + } } function statusQuery(filters, field) { - return {$or: filters.status.map(e => ({[field]: e}))}; + return {$or: filters.status.map(e => ({[field]: e}))}; } function addFilterQueries (queryPtr, filters) { // returns array of match queries from given filters - if (filters.length) { - queryPtr.push({$match: {$and: filterQueries(filters)}}); - } + if (filters.length) { + queryPtr.push({$match: {$and: filterQueries(filters)}}); + } } function filterQueries (filters) { - return filters.map(e => { - if (e.mode === 'or') { // allow or queries (needed for $ne added) - return {['$' + e.mode]: e.values}; - } - else if (e.mode === 'stringin') { - return {[e.field]: {['$in']: [new RegExp(e.values[0])]}}; - } - else { - // add filter criteria as {field: {$mode: value}}, only use first value when mode is not in/nin - return {[e.field]: {['$' + e.mode]: (e.mode.indexOf('in') >= 0 ? e.values : e.values[0])}}; - } - }); + return filters.map(e => { + if (e.mode === 'or') { // allow or queries (needed for $ne added) + return {['$' + e.mode]: e.values}; + } + else if (e.mode === 'stringin') { + return {[e.field]: {['$in']: [new RegExp(e.values[0])]}}; + } + else { + // add filter criteria as {field: {$mode: value}}, only use first value when mode is not in/nin + return {[e.field]: {['$' + e.mode]: (e.mode.indexOf('in') >= 0 ? e.values : e.values[0])}}; + } + }); } // add measurements as property [template.name], if one result, array is reduced to direct values. All given templates // must have the same name function addMeasurements(queryPtr, templates) { - queryPtr.push( - {$addFields: {[templates[0].name]: {$let: {vars: { - arr: {$filter: { - input: '$measurements', cond: {$and: [ - {$in: ['$$this.measurement_template', templates.map(e => mongoose.Types.ObjectId(e._id))]}, - {$ne: ['$$this.status', globals.status.del]} - ]}, - }}}, - in: {$cond: [{$lte: [{$size: '$$arr'}, 1]}, {$arrayElemAt: ['$$arr', 0]}, '$$arr']} - }}}}, - {$addFields: {[templates[0].name]: {$cond: [ - '$' + templates[0].name + '.values', - '$' + templates[0].name + '.values', - templates[0].parameters.reduce((s, e) => {s[e.name] = null; return s;}, {}) - ]}}}, - {$unwind: '$' + templates[0].name} - ); + queryPtr.push( + {$addFields: {[templates[0].name]: {$let: {vars: { + arr: {$filter: { + input: '$measurements', cond: {$and: [ + {$in: ['$$this.measurement_template', templates.map(e => mongoose.Types.ObjectId(e._id))]}, + {$ne: ['$$this.status', globals.status.del]} + ]}, + }}}, + in: {$cond: [{$lte: [{$size: '$$arr'}, 1]}, {$arrayElemAt: ['$$arr', 0]}, '$$arr']} + }}}}, + {$addFields: {[templates[0].name]: {$cond: [ + '$' + templates[0].name + '.values', + '$' + templates[0].name + '.values', + templates[0].parameters.reduce((s, e) => {s[e.name] = null; return s;}, {}) + ]}}}, + {$unwind: '$' + templates[0].name} + ); } function addNotes(queryPtr) { // add note fields with default, if no notes are found - queryPtr.push( - {$lookup: {from: 'notes', localField: 'note_id', foreignField: '_id', as: 'notes'}}, - {$addFields: {notes: {$cond: [ - {'$arrayElemAt': ['$notes', 0]}, - {'$arrayElemAt': ['$notes', 0]}, - {comment: null, sample_references: []} - ]}}} - ); + queryPtr.push( + {$lookup: {from: 'notes', localField: 'note_id', foreignField: '_id', as: 'notes'}}, + {$addFields: {notes: {$cond: [ + {'$arrayElemAt': ['$notes', 0]}, + {'$arrayElemAt': ['$notes', 0]}, + {comment: null, sample_references: []} + ]}}} + ); } function dateToOId (date) { // convert date to ObjectId - return mongoose.Types.ObjectId(Math.floor(date / 1000).toString(16) + '0000000000000000'); + return mongoose.Types.ObjectId(Math.floor(date / 1000).toString(16) + '0000000000000000'); } async function sampleReturn (sampleData, req, res, next) { - if (sampleData) { - await sampleData.populate('material_id.group_id').populate('material_id.supplier_id') - .execPopulate().catch(err => next(err)); - if (sampleData instanceof Error) return; - sampleData = sampleData.toObject(); + if (sampleData) { + await sampleData.populate('material_id.group_id').populate('material_id.supplier_id') + .execPopulate().catch(err => next(err)); + if (sampleData instanceof Error) return; + sampleData = sampleData.toObject(); - // deleted samples only available for dev/admin - if (sampleData.status === globals.status.del && !req.auth(res, ['dev', 'admin'], 'all')) return; - sampleData.material = sampleData.material_id; // map data to right keys - sampleData.material.group = sampleData.material.group_id.name; - sampleData.material.supplier = sampleData.material.supplier_id.name; - sampleData.user = sampleData.user_id.name; - sampleData.notes = sampleData.note_id ? sampleData.note_id : {}; - MeasurementModel.find({sample_id: sampleData._id, status: {$ne: 'deleted'}}) - .lean().exec((err, data) => { - sampleData.measurements = data; - if (['dev', 'admin'].indexOf(req.authDetails.level) < 0) { // strip dpt values if not dev or admin - sampleData.measurements.forEach(measurement => { - if (measurement.values[globals.spectrum.dpt]) { - delete measurement.values[globals.spectrum.dpt]; - } - }); - } - res.json(SampleValidate.output(sampleData, 'details')); - }); - } - else { - res.status(404).json({status: 'Not found'}); - } + // deleted samples only available for dev/admin + if (sampleData.status === globals.status.del && !req.auth(res, ['dev', 'admin'], 'all')) return; + sampleData.material = sampleData.material_id; // map data to right keys + sampleData.material.group = sampleData.material.group_id.name; + sampleData.material.supplier = sampleData.material.supplier_id.name; + sampleData.user = sampleData.user_id.name; + sampleData.notes = sampleData.note_id ? sampleData.note_id : {}; + MeasurementModel.find({sample_id: sampleData._id, status: {$ne: 'deleted'}}) + .lean().exec((err, data) => { + sampleData.measurements = data; + if (['dev', 'admin'].indexOf(req.authDetails.level) < 0) { // strip dpt values if not dev or admin + sampleData.measurements.forEach(measurement => { + if (measurement.values[globals.spectrum.dpt]) { + delete measurement.values[globals.spectrum.dpt]; + } + }); + } + res.json(SampleValidate.output(sampleData, 'details')); + }); + } + else { + res.status(404).json({status: 'Not found'}); + } } function setStatus (status, req, res, next) { - SampleModel.findByIdAndUpdate(req.params.id, {status}).log(req).lean().exec((err, data) => { - if (err) return next(err); + SampleModel.findByIdAndUpdate(req.params.id, {status}).log(req).lean().exec((err, data) => { + if (err) return next(err); - if (!data) { - return res.status(404).json({status: 'Not found'}); - } - MeasurementModel.updateMany({sample_id: mongoose.Types.ObjectId(req.params.id)}, {status}) - .log(req).lean().exec(err => { - if (err) return next(err); + if (!data) { + return res.status(404).json({status: 'Not found'}); + } + MeasurementModel.updateMany({sample_id: mongoose.Types.ObjectId(req.params.id)}, {status}) + .log(req).lean().exec(err => { + if (err) return next(err); - res.json({status: 'OK'}); - }); - }); -} \ No newline at end of file + res.json({status: 'OK'}); + }); + }); +} diff --git a/src/routes/template.spec.ts b/src/routes/template.spec.ts index d7e4b79..6b38e75 100644 --- a/src/routes/template.spec.ts +++ b/src/routes/template.spec.ts @@ -9,653 +9,653 @@ import mongoose from 'mongoose'; describe('/template', () => { - let server; - before(done => TestHelper.before(done)); - beforeEach(done => server = TestHelper.beforeEach(server, done)); - afterEach(done => TestHelper.afterEach(server, done)); - after(done => TestHelper.after(done)); + let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); + after(done => TestHelper.after(done)); - describe('/template/condition', () => { - describe('GET /template/conditions', () => { - it('returns all condition templates', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/template/conditions', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.condition_templates.length); - should(res.body).matchEach(condition => { - should(condition).have.only.keys('_id', 'name', 'version', 'first_id', 'parameters'); - should(condition).have.property('_id').be.type('string'); - should(condition).have.property('name').be.type('string'); - should(condition).have.property('version').be.type('number'); - should(condition).have.property('first_id').be.type('string'); - should(condition.parameters).matchEach(number => { - should(number).have.only.keys('name', 'range'); - should(number).have.property('name').be.type('string'); - should(number).have.property('range').be.type('object'); - }); - }); - done(); - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/template/conditions', - auth: {key: 'janedoe'}, - httpStatus: 401 - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/template/conditions', - httpStatus: 401 - }); - }); - }); + describe('/template/condition', () => { + describe('GET /template/conditions', () => { + it('returns all condition templates', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/conditions', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.condition_templates.length); + should(res.body).matchEach(condition => { + should(condition).have.only.keys('_id', 'name', 'version', 'first_id', 'parameters'); + should(condition).have.property('_id').be.type('string'); + should(condition).have.property('name').be.type('string'); + should(condition).have.property('version').be.type('number'); + should(condition).have.property('first_id').be.type('string'); + should(condition.parameters).matchEach(number => { + should(number).have.only.keys('name', 'range'); + should(number).have.property('name').be.type('string'); + should(number).have.property('range').be.type('object'); + }); + }); + done(); + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/conditions', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/conditions', + httpStatus: 401 + }); + }); + }); - describe('GET /template/condition/{id}', () => { - it('returns the right condition template', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/template/condition/200000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, first_id: '200000000000000000000001', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10, required: true}}]} - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/template/condition/200000000000000000000001', - auth: {key: 'janedoe'}, - httpStatus: 401 - }); - }); - it('rejects an unknown id', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/template/condition/000000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 404 - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/template/condition/200000000000000000000001', - httpStatus: 401 - }); - }); - }); + describe('GET /template/condition/{id}', () => { + it('returns the right condition template', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/condition/200000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, first_id: '200000000000000000000001', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10, required: true}}]} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/condition/200000000000000000000001', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects an unknown id', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/condition/000000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/condition/200000000000000000000001', + httpStatus: 401 + }); + }); + }); - describe('PUT /template/condition/{id}', () => { - it('returns the right condition template', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/200000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {}, - res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, first_id: '200000000000000000000001', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10, required: true}}]} - }); - }); - it('keeps unchanged properties', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/200000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {name: 'heat treatment', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10, required: true}}]}, - res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, first_id: '200000000000000000000001', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10, required: true}}]} - }); - }); - it('keeps only one unchanged property', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/200000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {name: 'heat treatment'}, - res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, first_id: '200000000000000000000001', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10, required: true}}]} - }); - }); - it('changes the given properties', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/200000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} - }).end((err, res) => { - if (err) return done(err); - TemplateConditionModel.findById(res.body._id).lean().exec((err, data:any) => { - if (err) return done(err); - should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v'); - should(data.first_id.toString()).be.eql('200000000000000000000001'); - should(data).have.property('name', 'heat aging'); - should(data).have.property('version', 2); - should(data.first_id.toString()).be.eql('200000000000000000000001'); - should(data).have.property('parameters').have.lengthOf(1); - should(data.parameters[0]).have.property('name', 'time'); - should(data.parameters[0]).have.property('range'); - should(data.parameters[0].range).have.property('min', 1); - done(); - }); - }); - }); - it('renames all occurrences instead of creating a new version when only the parameter name is changed', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/200000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, req: {name: 'heat treatment', parameters: [{name: 'treatmentMaterial', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10, required: true}}]} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({_id: '200000000000000000000001', name: 'heat treatment', version: 1, first_id: '200000000000000000000001', parameters: [{name: 'treatmentMaterial', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10, required: true}}]}); - SampleModel.find({'condition.condition_template': mongoose.Types.ObjectId('200000000000000000000001')}).lean().exec((err, data:any) => { - if (err) return done(err); - should(data).matchEach(sample => { - should(sample.condition).have.only.keys('treatmentMaterial', 'weeks', 'condition_template'); - }); - done(); - }); - }); - }); - it('creates a changelog', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/200000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]}, - log: { - collection: 'condition_templates', - dataAdd: { - first_id: '200000000000000000000001', - version: 2 - } - } - }); - }); - it('does not increase the version on name change', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/200000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {name: 'heat aging'} - }).end((err, res) => { - if (err) return done(err); - TemplateConditionModel.findById(res.body._id).lean().exec((err, data:any) => { - if (err) return done(err); - should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v'); - should(data.first_id.toString()).be.eql('200000000000000000000001'); - should(data).have.property('name', 'heat aging'); - should(data).have.property('version', 1); - should(data).have.property('parameters').have.lengthOf(2); - should(data.parameters[0]).have.property('name', 'material'); - should(data.parameters[1]).have.property('name', 'weeks'); - done(); - }); - }); - }); - it('does not increase the version on name change when property ranges stayed the same', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/200000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {name: 'heat aging', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'duration', range: {min: 1, max: 10, required: true}}]} - }).end((err, res) => { - if (err) return done(err); - TemplateConditionModel.findById(res.body._id).lean().exec((err, data:any) => { - if (err) return done(err); - should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v'); - should(data.first_id.toString()).be.eql('200000000000000000000001'); - should(data).have.property('name', 'heat aging'); - should(data).have.property('version', 1); - should(data).have.property('parameters').have.lengthOf(2); - should(data.parameters[0]).have.property('name', 'material'); - should(data.parameters[1]).have.property('name', 'duration'); - done(); - }); - }); - }); - it('supports values ranges', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/200000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {parameters: [{name: 'time', range: {values: [1, 2, 5]}}]} - }).end((err, res) => { - if (err) return done(err); - should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, first_id: '200000000000000000000001', parameters: [{name: 'time', range: {values: [1, 2, 5]}}]}); - done(); - }); - }); - it('supports min max ranges', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/200000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {parameters: [{name: 'time', range: {min: 1, max: 11}}]} - }).end((err, res) => { - if (err) return done(err); - should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, first_id: '200000000000000000000001', parameters: [{name: 'time', range: {min: 1, max: 11}}]}); - done(); - }); - }); - it('supports array type ranges', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/200000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {parameters: [{name: 'time', range: {type: 'array'}}]} - }).end((err, res) => { - if (err) return done(err); - should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, first_id: '200000000000000000000001', parameters: [{name: 'time', range: {type: 'array'}}]}); - done(); - }); - }); - it('supports required ranges', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/200000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {parameters: [{name: 'time', range: {required: true}}]} - }).end((err, res) => { - if (err) return done(err); - should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, first_id: '200000000000000000000001', parameters: [{name: 'time', range: {required: true}}]}); - done(); - }); - }); - it('supports empty ranges', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/200000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {parameters: [{name: 'time', range: {}}]} - }).end((err, res) => { - if (err) return done(err); - should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, first_id: '200000000000000000000001', parameters: [{name: 'time', range: {}}]}); - done(); - }); - }); - it('rejects `condition_template` as parameter name', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/200000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {parameters: [{name: 'condition_template', range: {}}]}, - res: {status: 'Invalid body format', details: '"parameters[0].name" contains an invalid value'} - }); - }); - it('rejects not specified parameters', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/200000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {name: 'heat treatment', parameters: [{name: 'material', range: {xx: 5}}]}, - res: {status: 'Invalid body format', details: '"parameters[0].range.xx" is not allowed'} - }); - }); - it('rejects an invalid id', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/2000000000h0000000000001', - auth: {basic: 'admin'}, - httpStatus: 404, - req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} - }); - }); - it('rejects an unknown id', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/000000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 404, - req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/200000000000000000000001', - auth: {key: 'admin'}, - httpStatus: 401, - req: {} - }); - }); - it('rejects requests from a write user', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/200000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {} - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/condition/200000000000000000000001', - httpStatus: 401, - req: {} - }); - }); - }); + describe('PUT /template/condition/{id}', () => { + it('returns the right condition template', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/200000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {}, + res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, first_id: '200000000000000000000001', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10, required: true}}]} + }); + }); + it('keeps unchanged properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/200000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'heat treatment', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10, required: true}}]}, + res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, first_id: '200000000000000000000001', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10, required: true}}]} + }); + }); + it('keeps only one unchanged property', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/200000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'heat treatment'}, + res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, first_id: '200000000000000000000001', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10, required: true}}]} + }); + }); + it('changes the given properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/200000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} + }).end((err, res) => { + if (err) return done(err); + TemplateConditionModel.findById(res.body._id).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v'); + should(data.first_id.toString()).be.eql('200000000000000000000001'); + should(data).have.property('name', 'heat aging'); + should(data).have.property('version', 2); + should(data.first_id.toString()).be.eql('200000000000000000000001'); + should(data).have.property('parameters').have.lengthOf(1); + should(data.parameters[0]).have.property('name', 'time'); + should(data.parameters[0]).have.property('range'); + should(data.parameters[0].range).have.property('min', 1); + done(); + }); + }); + }); + it('renames all occurrences instead of creating a new version when only the parameter name is changed', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/200000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, req: {name: 'heat treatment', parameters: [{name: 'treatmentMaterial', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10, required: true}}]} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '200000000000000000000001', name: 'heat treatment', version: 1, first_id: '200000000000000000000001', parameters: [{name: 'treatmentMaterial', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10, required: true}}]}); + SampleModel.find({'condition.condition_template': mongoose.Types.ObjectId('200000000000000000000001')}).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).matchEach(sample => { + should(sample.condition).have.only.keys('treatmentMaterial', 'weeks', 'condition_template'); + }); + done(); + }); + }); + }); + it('creates a changelog', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/200000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]}, + log: { + collection: 'condition_templates', + dataAdd: { + first_id: '200000000000000000000001', + version: 2 + } + } + }); + }); + it('does not increase the version on name change', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/200000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'heat aging'} + }).end((err, res) => { + if (err) return done(err); + TemplateConditionModel.findById(res.body._id).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v'); + should(data.first_id.toString()).be.eql('200000000000000000000001'); + should(data).have.property('name', 'heat aging'); + should(data).have.property('version', 1); + should(data).have.property('parameters').have.lengthOf(2); + should(data.parameters[0]).have.property('name', 'material'); + should(data.parameters[1]).have.property('name', 'weeks'); + done(); + }); + }); + }); + it('does not increase the version on name change when property ranges stayed the same', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/200000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'heat aging', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'duration', range: {min: 1, max: 10, required: true}}]} + }).end((err, res) => { + if (err) return done(err); + TemplateConditionModel.findById(res.body._id).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v'); + should(data.first_id.toString()).be.eql('200000000000000000000001'); + should(data).have.property('name', 'heat aging'); + should(data).have.property('version', 1); + should(data).have.property('parameters').have.lengthOf(2); + should(data.parameters[0]).have.property('name', 'material'); + should(data.parameters[1]).have.property('name', 'duration'); + done(); + }); + }); + }); + it('supports values ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/200000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'time', range: {values: [1, 2, 5]}}]} + }).end((err, res) => { + if (err) return done(err); + should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, first_id: '200000000000000000000001', parameters: [{name: 'time', range: {values: [1, 2, 5]}}]}); + done(); + }); + }); + it('supports min max ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/200000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'time', range: {min: 1, max: 11}}]} + }).end((err, res) => { + if (err) return done(err); + should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, first_id: '200000000000000000000001', parameters: [{name: 'time', range: {min: 1, max: 11}}]}); + done(); + }); + }); + it('supports array type ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/200000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'time', range: {type: 'array'}}]} + }).end((err, res) => { + if (err) return done(err); + should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, first_id: '200000000000000000000001', parameters: [{name: 'time', range: {type: 'array'}}]}); + done(); + }); + }); + it('supports required ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/200000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'time', range: {required: true}}]} + }).end((err, res) => { + if (err) return done(err); + should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, first_id: '200000000000000000000001', parameters: [{name: 'time', range: {required: true}}]}); + done(); + }); + }); + it('supports empty ranges', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/200000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {parameters: [{name: 'time', range: {}}]} + }).end((err, res) => { + if (err) return done(err); + should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, first_id: '200000000000000000000001', parameters: [{name: 'time', range: {}}]}); + done(); + }); + }); + it('rejects `condition_template` as parameter name', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/200000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {parameters: [{name: 'condition_template', range: {}}]}, + res: {status: 'Invalid body format', details: '"parameters[0].name" contains an invalid value'} + }); + }); + it('rejects not specified parameters', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/200000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'heat treatment', parameters: [{name: 'material', range: {xx: 5}}]}, + res: {status: 'Invalid body format', details: '"parameters[0].range.xx" is not allowed'} + }); + }); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/2000000000h0000000000001', + auth: {basic: 'admin'}, + httpStatus: 404, + req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} + }); + }); + it('rejects an unknown id', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/000000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 404, + req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/200000000000000000000001', + auth: {key: 'admin'}, + httpStatus: 401, + req: {} + }); + }); + it('rejects requests from a write user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/200000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/condition/200000000000000000000001', + httpStatus: 401, + req: {} + }); + }); + }); - describe('POST /template/condition/new', () => { - it('returns the right condition template', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/template/condition/new', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {name: 'heat treatment3', parameters: [{name: 'material', range: {values: ['copper']}}]} - }).end((err, res) => { - if (err) return done(err); - should(res.body).have.only.keys('_id', 'name', 'version', 'first_id', 'parameters'); - should(res.body).have.property('name', 'heat treatment3'); - should(res.body).have.property('version', 1); - should(res.body._id).be.eql(res.body.first_id); - should(res.body).have.property('parameters').have.lengthOf(1); - should(res.body.parameters[0]).have.property('name', 'material'); - should(res.body.parameters[0]).have.property('range'); - should(res.body.parameters[0].range).have.property('values'); - should(res.body.parameters[0].range.values[0]).be.eql('copper'); - done(); - }); - }); - it('stores the template', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/template/condition/new', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} - }).end((err, res) => { - if (err) return done(err); - TemplateConditionModel.findById(res.body._id).lean().exec((err, data:any) => { - if (err) return done(err); - should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v'); - should(data.first_id.toString()).be.eql(data._id.toString()); - should(data).have.property('name', 'heat aging'); - should(data).have.property('version', 1); - should(res.body._id).be.eql(res.body.first_id); - should(data).have.property('parameters').have.lengthOf(1); - should(data.parameters[0]).have.property('name', 'time'); - should(data.parameters[0]).have.property('range'); - should(data.parameters[0].range).have.property('min', 1); - done(); - }); - }); - }); - it('creates a changelog', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/template/condition/new', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]}, - log: { - collection: 'condition_templates', - dataAdd: {version: 1}, - dataIgn: ['first_id'] - } - }); - }); - it('rejects a missing name', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/template/condition/new', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {parameters: [{name: 'time', range: {min: 1}}]}, - res: {status: 'Invalid body format', details: '"name" is required'} - }); - }); - it('rejects `condition_template` as parameter name', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/template/condition/new', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {name: 'heat aging', parameters: [{name: 'condition_template', range: {min: 1}}]}, - res: {status: 'Invalid body format', details: '"parameters[0].name" contains an invalid value'} - }); - }); - it('rejects a number prefix', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/template/condition/new', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {name: 'heat aging', number_prefix: 'C', parameters: [{name: 'time', range: {min: 1}}]}, - res: {status: 'Invalid body format', details: '"number_prefix" is not allowed'} - }); - }); - it('rejects missing parameters', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/template/condition/new', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {name: 'heat aging'}, - res: {status: 'Invalid body format', details: '"parameters" is required'} - }); - }); - it('rejects a missing parameter name', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/template/condition/new', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {name: 'heat aging', parameters: [{range: {min: 1}}]}, - res: {status: 'Invalid body format', details: '"parameters[0].name" is required'} - }); - }); - it('rejects a missing parameter range', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/template/condition/new', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {name: 'heat aging', parameters: [{name: 'time'}]}, - res: {status: 'Invalid body format', details: '"parameters[0].range" is required'} - }); - }); - it('rejects an invalid parameter range property', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/template/condition/new', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {name: 'heat aging', parameters: [{name: 'time', range: {xx: 1}}]}, - res: {status: 'Invalid body format', details: '"parameters[0].range.xx" is not allowed'} - }); - }); - it('rejects wrong properties', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/template/condition/new', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {name: 'heat aging', parameters: [{name: 'time', range: {}}], xx: 33}, - res: {status: 'Invalid body format', details: '"xx" is not allowed'} - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/template/condition/new', - auth: {key: 'admin'}, - httpStatus: 401, - req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} - }); - }); - it('rejects requests from a write user', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/template/condition/new', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/template/condition/new', - httpStatus: 401, - req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} - }); - }); - }); - }); + describe('POST /template/condition/new', () => { + it('returns the right condition template', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/condition/new', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'heat treatment3', parameters: [{name: 'material', range: {values: ['copper']}}]} + }).end((err, res) => { + if (err) return done(err); + should(res.body).have.only.keys('_id', 'name', 'version', 'first_id', 'parameters'); + should(res.body).have.property('name', 'heat treatment3'); + should(res.body).have.property('version', 1); + should(res.body._id).be.eql(res.body.first_id); + should(res.body).have.property('parameters').have.lengthOf(1); + should(res.body.parameters[0]).have.property('name', 'material'); + should(res.body.parameters[0]).have.property('range'); + should(res.body.parameters[0].range).have.property('values'); + should(res.body.parameters[0].range.values[0]).be.eql('copper'); + done(); + }); + }); + it('stores the template', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/condition/new', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} + }).end((err, res) => { + if (err) return done(err); + TemplateConditionModel.findById(res.body._id).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v'); + should(data.first_id.toString()).be.eql(data._id.toString()); + should(data).have.property('name', 'heat aging'); + should(data).have.property('version', 1); + should(res.body._id).be.eql(res.body.first_id); + should(data).have.property('parameters').have.lengthOf(1); + should(data.parameters[0]).have.property('name', 'time'); + should(data.parameters[0]).have.property('range'); + should(data.parameters[0].range).have.property('min', 1); + done(); + }); + }); + }); + it('creates a changelog', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/condition/new', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]}, + log: { + collection: 'condition_templates', + dataAdd: {version: 1}, + dataIgn: ['first_id'] + } + }); + }); + it('rejects a missing name', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/condition/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {parameters: [{name: 'time', range: {min: 1}}]}, + res: {status: 'Invalid body format', details: '"name" is required'} + }); + }); + it('rejects `condition_template` as parameter name', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/condition/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'heat aging', parameters: [{name: 'condition_template', range: {min: 1}}]}, + res: {status: 'Invalid body format', details: '"parameters[0].name" contains an invalid value'} + }); + }); + it('rejects a number prefix', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/condition/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'heat aging', number_prefix: 'C', parameters: [{name: 'time', range: {min: 1}}]}, + res: {status: 'Invalid body format', details: '"number_prefix" is not allowed'} + }); + }); + it('rejects missing parameters', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/condition/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'heat aging'}, + res: {status: 'Invalid body format', details: '"parameters" is required'} + }); + }); + it('rejects a missing parameter name', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/condition/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'heat aging', parameters: [{range: {min: 1}}]}, + res: {status: 'Invalid body format', details: '"parameters[0].name" is required'} + }); + }); + it('rejects a missing parameter range', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/condition/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'heat aging', parameters: [{name: 'time'}]}, + res: {status: 'Invalid body format', details: '"parameters[0].range" is required'} + }); + }); + it('rejects an invalid parameter range property', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/condition/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'heat aging', parameters: [{name: 'time', range: {xx: 1}}]}, + res: {status: 'Invalid body format', details: '"parameters[0].range.xx" is not allowed'} + }); + }); + it('rejects wrong properties', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/condition/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {name: 'heat aging', parameters: [{name: 'time', range: {}}], xx: 33}, + res: {status: 'Invalid body format', details: '"xx" is not allowed'} + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/condition/new', + auth: {key: 'admin'}, + httpStatus: 401, + req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} + }); + }); + it('rejects requests from a write user', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/condition/new', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/template/condition/new', + httpStatus: 401, + req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} + }); + }); + }); + }); - describe('/template/measurement', () => { - describe('GET /template/measurements', () => { - it('returns all measurement templates', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/template/measurements', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.measurement_templates.length); - should(res.body).matchEach(measurement => { - should(measurement).have.only.keys('_id', 'name', 'version', 'first_id', 'parameters'); - should(measurement).have.property('_id').be.type('string'); - should(measurement).have.property('first_id').be.type('string'); - should(measurement).have.property('name').be.type('string'); - should(measurement).have.property('version').be.type('number'); - should(measurement.parameters).matchEach(number => { - should(number).have.only.keys('name', 'range'); - should(number).have.property('name').be.type('string'); - should(number).have.property('range').be.type('object'); - }); - }); - done(); - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/template/measurements', - auth: {key: 'janedoe'}, - httpStatus: 401 - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/template/measurements', - httpStatus: 401 - }); - }); - }); - describe('PUT /template/measurement/{id}', () => { - it('renames all occurrences instead of creating a new version when only the parameter name is changed', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/measurement/300000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, req: {name: 'spectrum', parameters: [{name: 'spectrumValues', range: {type: 'array'}}, {name: 'device', range: {}}, {name: 'filename', range: {}}]} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({_id: '300000000000000000000001', name: 'spectrum', version: 1, first_id: '300000000000000000000001', parameters: [{name: 'spectrumValues', range: {type: 'array'}}, {name: 'device', range: {}}, {name: 'filename', range: {}}]}); - MeasurementModel.find({'measurement_template': mongoose.Types.ObjectId('300000000000000000000001')}).lean().exec((err, data:any) => { - if (err) return done(err); - should(data).matchEach(measurement => { - should(measurement.values).have.only.keys('spectrumValues', 'device', 'filename'); - }); - done(); - }); - }); - }); - }); - // other methods should be covered by condition tests - }); + describe('/template/measurement', () => { + describe('GET /template/measurements', () => { + it('returns all measurement templates', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/measurements', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.measurement_templates.length); + should(res.body).matchEach(measurement => { + should(measurement).have.only.keys('_id', 'name', 'version', 'first_id', 'parameters'); + should(measurement).have.property('_id').be.type('string'); + should(measurement).have.property('first_id').be.type('string'); + should(measurement).have.property('name').be.type('string'); + should(measurement).have.property('version').be.type('number'); + should(measurement.parameters).matchEach(number => { + should(number).have.only.keys('name', 'range'); + should(number).have.property('name').be.type('string'); + should(number).have.property('range').be.type('object'); + }); + }); + done(); + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/measurements', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/measurements', + httpStatus: 401 + }); + }); + }); + describe('PUT /template/measurement/{id}', () => { + it('renames all occurrences instead of creating a new version when only the parameter name is changed', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/measurement/300000000000000000000001', + auth: {basic: 'admin'}, + httpStatus: 200, req: {name: 'spectrum', parameters: [{name: 'spectrumValues', range: {type: 'array'}}, {name: 'device', range: {}}, {name: 'filename', range: {}}]} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '300000000000000000000001', name: 'spectrum', version: 1, first_id: '300000000000000000000001', parameters: [{name: 'spectrumValues', range: {type: 'array'}}, {name: 'device', range: {}}, {name: 'filename', range: {}}]}); + MeasurementModel.find({'measurement_template': mongoose.Types.ObjectId('300000000000000000000001')}).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).matchEach(measurement => { + should(measurement.values).have.only.keys('spectrumValues', 'device', 'filename'); + }); + done(); + }); + }); + }); + }); + // other methods should be covered by condition tests + }); - describe('/template/material', () => { - describe('GET /template/materials', () => { - it('returns all material templates', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/template/materials', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.material_templates.length); - should(res.body).matchEach(measurement => { - should(measurement).have.only.keys('_id', 'name', 'version', 'first_id', 'parameters'); - should(measurement).have.property('_id').be.type('string'); - should(measurement).have.property('name').be.type('string'); - should(measurement).have.property('version').be.type('number'); - should(measurement.parameters).matchEach(number => { - should(number).have.only.keys('name', 'range'); - should(number).have.property('name').be.type('string'); - should(number).have.property('range').be.type('object'); - }); - }); - done(); - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/template/materials', - auth: {key: 'janedoe'}, - httpStatus: 401 - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/template/materials', - httpStatus: 401 - }); - }); - }); - describe('PUT /template/material/{id}', () => { - it('renames all occurrences instead of creating a new version when only the parameter name is changed', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/material/130000000000000000000003', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {name: 'plastic', parameters: [ {name: 'glassfiber', range: {min: 0, max: 100, required: true}}, {name: 'carbonfiber', range: {min: 0, max: 100, required: true}}, {name: 'mineral', range: {min: 0, max: 100, required: true}}]} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({_id: '130000000000000000000003', name: 'plastic', version: 2, first_id: '130000000000000000000001', parameters: [ {name: 'glassfiber', range: {min: 0, max: 100, required: true}}, {name: 'carbonfiber', range: {min: 0, max: 100, required: true}}, {name: 'mineral', range: {min: 0, max: 100, required: true}}]}); - MaterialModel.find({'properties': mongoose.Types.ObjectId('130000000000000000000003')}).lean().exec((err, data:any) => { - if (err) return done(err); - should(data).matchEach(material => { - should(material.parameters).have.only.keys('glassfiber', 'carbonfiber', 'mineral', 'material_template'); - }); - done(); - }); - }); - }); - }); - // other methods should be covered by condition tests - }); -}); \ No newline at end of file + describe('/template/material', () => { + describe('GET /template/materials', () => { + it('returns all material templates', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/materials', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.material_templates.length); + should(res.body).matchEach(measurement => { + should(measurement).have.only.keys('_id', 'name', 'version', 'first_id', 'parameters'); + should(measurement).have.property('_id').be.type('string'); + should(measurement).have.property('name').be.type('string'); + should(measurement).have.property('version').be.type('number'); + should(measurement.parameters).matchEach(number => { + should(number).have.only.keys('name', 'range'); + should(number).have.property('name').be.type('string'); + should(number).have.property('range').be.type('object'); + }); + }); + done(); + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/materials', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/materials', + httpStatus: 401 + }); + }); + }); + describe('PUT /template/material/{id}', () => { + it('renames all occurrences instead of creating a new version when only the parameter name is changed', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/template/material/130000000000000000000003', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'plastic', parameters: [ {name: 'glassfiber', range: {min: 0, max: 100, required: true}}, {name: 'carbonfiber', range: {min: 0, max: 100, required: true}}, {name: 'mineral', range: {min: 0, max: 100, required: true}}]} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '130000000000000000000003', name: 'plastic', version: 2, first_id: '130000000000000000000001', parameters: [ {name: 'glassfiber', range: {min: 0, max: 100, required: true}}, {name: 'carbonfiber', range: {min: 0, max: 100, required: true}}, {name: 'mineral', range: {min: 0, max: 100, required: true}}]}); + MaterialModel.find({'properties': mongoose.Types.ObjectId('130000000000000000000003')}).lean().exec((err, data:any) => { + if (err) return done(err); + should(data).matchEach(material => { + should(material.parameters).have.only.keys('glassfiber', 'carbonfiber', 'mineral', 'material_template'); + }); + done(); + }); + }); + }); + }); + // other methods should be covered by condition tests + }); +}); diff --git a/src/routes/template.ts b/src/routes/template.ts index 8eeffdd..d8966f9 100644 --- a/src/routes/template.ts +++ b/src/routes/template.ts @@ -18,141 +18,141 @@ import db from '../db'; const router = express.Router(); router.get('/template/:collection(measurements|conditions|materials)', (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return; - req.params.collection = req.params.collection.replace(/s$/g, ''); // remove trailing s - model(req).find({}).lean().exec((err, data) => { - if (err) next (err); - // validate all and filter null values from validation errors - res.json(_.compact(data.map(e => TemplateValidate.output(e)))); - }); + req.params.collection = req.params.collection.replace(/s$/g, ''); // remove trailing s + model(req).find({}).lean().exec((err, data) => { + if (err) next (err); + // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => TemplateValidate.output(e)))); + }); }); router.get('/template/:collection(measurement|condition|material)/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return; - model(req).findById(req.params.id).lean().exec((err, data) => { - if (err) next (err); - if (data) { - res.json(TemplateValidate.output(data)); - } - else { - res.status(404).json({status: 'Not found'}); - } - }); + model(req).findById(req.params.id).lean().exec((err, data) => { + if (err) next (err); + if (data) { + res.json(TemplateValidate.output(data)); + } + else { + res.status(404).json({status: 'Not found'}); + } + }); }); router.put('/template/:collection(measurement|condition|material)/' + IdValidate.parameter(), - async (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + async (req, res, next) => { + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - const {error, value: template} = TemplateValidate.input(req.body, 'change'); - if (error) return res400(error, res); + const {error, value: template} = TemplateValidate.input(req.body, 'change'); + if (error) return res400(error, res); - // find given template - const templateRef = await model(req).findById(req.params.id).lean().exec().catch(err => {next(err);}) as any; - if (templateRef instanceof Error) return; - if (!templateRef) { - return res.status(404).json({status: 'Not found'}); - } - // find latest version - const templateData = await model(req).findOne({first_id: templateRef.first_id}).sort({version: -1}) - .lean().exec().catch(err => {next(err);}) as any; - if (templateData instanceof Error) return; - if (!templateData) { - return res.status(404).json({status: 'Not found'}); - } + // find given template + const templateRef = await model(req).findById(req.params.id).lean().exec().catch(err => {next(err);}) as any; + if (templateRef instanceof Error) return; + if (!templateRef) { + return res.status(404).json({status: 'Not found'}); + } + // find latest version + const templateData = await model(req).findOne({first_id: templateRef.first_id}).sort({version: -1}) + .lean().exec().catch(err => {next(err);}) as any; + if (templateData instanceof Error) return; + if (!templateData) { + return res.status(404).json({status: 'Not found'}); + } - if (!_.isEqual(_.pick(templateData, _.keys(template)), template)) { // data was changed - if (!template.parameters || _.isEqual(templateData.parameters, template.parameters)) { // only name was changed - model(req).findByIdAndUpdate(req.params.id, {name: template.name}, {new: true}) - .log(req).lean().exec((err, data) => { - if (err) next (err); - res.json(TemplateValidate.output(data)); - }); - } - else if (template.parameters.filter((e, i) => _.isEqual(e.range, templateData.parameters[i].range)).length - === templateData.parameters.length) { // only names changed - const changedParameterNames = template.parameters.map((e, i) => ( // list of new names - {name: e.name, index: i, oldName: templateData.parameters[i].name} - )).filter(e => e.name !== e.oldName); + if (!_.isEqual(_.pick(templateData, _.keys(template)), template)) { // data was changed + if (!template.parameters || _.isEqual(templateData.parameters, template.parameters)) { // only name was changed + model(req).findByIdAndUpdate(req.params.id, {name: template.name}, {new: true}) + .log(req).lean().exec((err, data) => { + if (err) next (err); + res.json(TemplateValidate.output(data)); + }); + } + else if (template.parameters.filter((e, i) => _.isEqual(e.range, templateData.parameters[i].range)).length + === templateData.parameters.length) { // only names changed + const changedParameterNames = template.parameters.map((e, i) => ( // list of new names + {name: e.name, index: i, oldName: templateData.parameters[i].name} + )).filter(e => e.name !== e.oldName); - // custom mappings for different collections - let targetModel; // model of the collection where the template is used - let pathPrefix; // path to the parameters in use - let templatePath; // complete path of the template property - switch (req.params.collection) { - case 'condition': - targetModel = SampleModel; - pathPrefix = 'condition.'; - templatePath = 'condition.condition_template'; - break; - case 'measurement': - targetModel = MeasurementModel; - pathPrefix = 'values.'; - templatePath = 'measurement_template'; - break; - case 'material': - targetModel = MaterialModel; - pathPrefix = 'properties.'; - templatePath = 'properties.material_template'; - break; - } + // custom mappings for different collections + let targetModel; // model of the collection where the template is used + let pathPrefix; // path to the parameters in use + let templatePath; // complete path of the template property + switch (req.params.collection) { + case 'condition': + targetModel = SampleModel; + pathPrefix = 'condition.'; + templatePath = 'condition.condition_template'; + break; + case 'measurement': + targetModel = MeasurementModel; + pathPrefix = 'values.'; + templatePath = 'measurement_template'; + break; + case 'material': + targetModel = MaterialModel; + pathPrefix = 'properties.'; + templatePath = 'properties.material_template'; + break; + } - targetModel.updateMany({[templatePath]: mongoose.Types.ObjectId(templateData._id)}, - {$rename: - changedParameterNames.reduce((s, e) => {s[pathPrefix + e.oldName] = pathPrefix + e.name; return s;}, {}) - }) .log(req).lean().exec(err => { - if (err) return next(err); - model(req).findByIdAndUpdate(req.params.id, - {$set: - changedParameterNames.reduce( - (s, e) => {s[`parameters.${e.index}.name`] = e.name; return s;}, {name: template.name} - ), - },{new: true}).log(req).lean().exec((err, data) => { - if (err) next (err); - res.json(TemplateValidate.output(data)); - }); - }); - } - else { - template.version = templateData.version + 1; // increase version - // save new template, fill with old properties - await new (model(req))(_.assign({}, _.omit(templateData, ['_id', '__v']), template)).save((err, data) => { - if (err) next (err); - db.log(req, req.params.collection + '_templates', {_id: data._id}, data.toObject()); - res.json(TemplateValidate.output(data.toObject())); - }); - } - } - else { - res.json(TemplateValidate.output(templateData)); - } -}); + targetModel.updateMany({[templatePath]: mongoose.Types.ObjectId(templateData._id)}, + {$rename: + changedParameterNames.reduce((s, e) => {s[pathPrefix + e.oldName] = pathPrefix + e.name; return s;}, {}) + }) .log(req).lean().exec(err => { + if (err) return next(err); + model(req).findByIdAndUpdate(req.params.id, + {$set: + changedParameterNames.reduce( + (s, e) => {s[`parameters.${e.index}.name`] = e.name; return s;}, {name: template.name} + ), + },{new: true}).log(req).lean().exec((err, data) => { + if (err) next (err); + res.json(TemplateValidate.output(data)); + }); + }); + } + else { + template.version = templateData.version + 1; // increase version + // save new template, fill with old properties + await new (model(req))(_.assign({}, _.omit(templateData, ['_id', '__v']), template)).save((err, data) => { + if (err) next (err); + db.log(req, req.params.collection + '_templates', {_id: data._id}, data.toObject()); + res.json(TemplateValidate.output(data.toObject())); + }); + } + } + else { + res.json(TemplateValidate.output(templateData)); + } + }); -router.post('/template/:collection(measurement|condition|material)/new', async (req, res, next) => { - if (!req.auth(res, ['dev', 'admin'], 'basic')) return; + router.post('/template/:collection(measurement|condition|material)/new', async (req, res, next) => { + if (!req.auth(res, ['dev', 'admin'], 'basic')) return; - const {error, value: template} = TemplateValidate.input(req.body, 'new'); - if (error) return res400(error, res); + const {error, value: template} = TemplateValidate.input(req.body, 'new'); + if (error) return res400(error, res); - template._id = mongoose.Types.ObjectId(); // set reference to itself for first version of template - template.first_id = template._id; - template.version = 1; // set template version - await new (model(req))(template).save((err, data) => { - if (err) next (err); - db.log(req, req.params.collection + '_templates', {_id: data._id}, data.toObject()); - res.json(TemplateValidate.output(data.toObject())); - }); -}); + template._id = mongoose.Types.ObjectId(); // set reference to itself for first version of template + template.first_id = template._id; + template.version = 1; // set template version + await new (model(req))(template).save((err, data) => { + if (err) next (err); + db.log(req, req.params.collection + '_templates', {_id: data._id}, data.toObject()); + res.json(TemplateValidate.output(data.toObject())); + }); + }); -module.exports = router; + module.exports = router; -function model (req) { // return right template model - switch (req.params.collection) { - case 'condition': return ConditionTemplateModel - case 'measurement': return MeasurementTemplateModel - case 'material': return MaterialTemplateModel - } -} \ No newline at end of file + function model (req) { // return right template model + switch (req.params.collection) { + case 'condition': return ConditionTemplateModel + case 'measurement': return MeasurementTemplateModel + case 'material': return MaterialTemplateModel + } + } diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts index c63806b..309c23f 100644 --- a/src/routes/user.spec.ts +++ b/src/routes/user.spec.ts @@ -5,792 +5,792 @@ import TestHelper from "../test/helper"; describe('/user', () => { - let server; - before(done => TestHelper.before(done)); - beforeEach(done => server = TestHelper.beforeEach(server, done)); - afterEach(done => TestHelper.afterEach(server, done)); - after(done => TestHelper.after(done)); + let server; + before(done => TestHelper.before(done)); + beforeEach(done => server = TestHelper.beforeEach(server, done)); + afterEach(done => TestHelper.afterEach(server, done)); + after(done => TestHelper.after(done)); - describe('GET /users', () => { - it('returns all users', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/users', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - const json = require('../test/db.json'); - should(res.body).have.lengthOf(json.collections.users.length); - should(res.body).matchEach(user => { - should(user).have.only.keys('_id', 'email', 'name', 'level', 'location', 'devices', 'models', 'status'); - should(user).have.property('_id').be.type('string'); - should(user).have.property('email').be.type('string'); - should(user).have.property('name').be.type('string'); - should(user).have.property('level').be.type('string'); - should(user).have.property('location').be.type('string'); - should(user.devices).matchEach(device => { - should(device).be.type('string'); - }); - should(user.models).matchEach(model => { - should(model).be.type('string'); - }); - should(user).have.property('status').be.type('string'); - }); - done(); - }); - }); - it('rejects requests from non-admins', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/users', - auth: {basic: 'janedoe'}, - httpStatus: 403 - }); - }); - it('rejects requests from an admin API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/users', - auth: {key: 'admin'}, - httpStatus: 401 - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/users', - httpStatus: 401 - }); - }); - }); + describe('GET /users', () => { + it('returns all users', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/users', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.users.length); + should(res.body).matchEach(user => { + should(user).have.only.keys('_id', 'email', 'name', 'level', 'location', 'devices', 'models', 'status'); + should(user).have.property('_id').be.type('string'); + should(user).have.property('email').be.type('string'); + should(user).have.property('name').be.type('string'); + should(user).have.property('level').be.type('string'); + should(user).have.property('location').be.type('string'); + should(user.devices).matchEach(device => { + should(device).be.type('string'); + }); + should(user.models).matchEach(model => { + should(model).be.type('string'); + }); + should(user).have.property('status').be.type('string'); + }); + done(); + }); + }); + it('rejects requests from non-admins', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/users', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('rejects requests from an admin API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/users', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/users', + httpStatus: 401 + }); + }); + }); - describe('GET /user/{name}', () => { - it('returns own user details', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/user', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done (err); - should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'devices', 'models'); - should(res.body).have.property('_id').be.type('string'); - should(res.body).have.property('email', 'jane.doe@bosch.com'); - should(res.body).have.property('name', 'janedoe'); - should(res.body).have.property('level', 'write'); - should(res.body).have.property('location', 'Rng'); - should(res.body).have.property('devices', ['Alpha I']); - should(res.body).have.property('models', []); - done(); - }); - }); - it('returns other user details for admin', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/user/janedoe', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done (err); - should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'devices', 'models'); - should(res.body).have.property('_id').be.type('string'); - should(res.body).have.property('email', 'jane.doe@bosch.com'); - should(res.body).have.property('name', 'janedoe'); - should(res.body).have.property('level', 'write'); - should(res.body).have.property('location', 'Rng'); - should(res.body).have.property('devices', ['Alpha I']); - should(res.body).have.property('models', []); - done(); - }); - }); - it('rejects requests from non-admins for another user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/user/admin', - auth: {basic: 'janedoe'}, - httpStatus: 403 - }); - }); - it('rejects requests from a user API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/user', - auth: {key: 'janedoe'}, - httpStatus: 401 - }); - }); - it('rejects requests from an admin API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/user/janedoe', - auth: {key: 'janedoe'}, - httpStatus: 401 - }); - }); - it('returns 404 for an unknown user', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/user/unknown', - auth: {basic: 'admin'}, - httpStatus: 404 - }); - }); - it('rejects requests from an admin API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/user/janedoe', - httpStatus: 401 - }); - }); - }); + describe('GET /user/{name}', () => { + it('returns own user details', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'devices', 'models'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('devices', ['Alpha I']); + should(res.body).have.property('models', []); + done(); + }); + }); + it('returns other user details for admin', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/janedoe', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'devices', 'models'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('devices', ['Alpha I']); + should(res.body).have.property('models', []); + done(); + }); + }); + it('rejects requests from non-admins for another user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/admin', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('rejects requests from a user API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects requests from an admin API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/janedoe', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('returns 404 for an unknown user', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/unknown', + auth: {basic: 'admin'}, + httpStatus: 404 + }); + }); + it('rejects requests from an admin API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/janedoe', + httpStatus: 401 + }); + }); + }); - describe('PUT /user/{name}', () => { - it('returns own user details', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {} - }).end((err, res) => { - if (err) return done (err); - should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'devices', 'models'); - should(res.body).have.property('_id').be.type('string'); - should(res.body).have.property('email', 'jane.doe@bosch.com'); - should(res.body).have.property('name', 'janedoe'); - should(res.body).have.property('level', 'write'); - should(res.body).have.property('location', 'Rng'); - should(res.body).have.property('devices', ['Alpha I']); - should(res.body).have.property('models', []); - done(); - }); - }); - it('returns other user details for admin', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user/janedoe', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {} - }).end((err, res) => { - if (err) return done (err); - should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'devices', 'models'); - should(res.body).have.property('_id').be.type('string'); - should(res.body).have.property('email', 'jane.doe@bosch.com'); - should(res.body).have.property('name', 'janedoe'); - should(res.body).have.property('level', 'write'); - should(res.body).have.property('location', 'Rng'); - should(res.body).have.property('devices', ['Alpha I']); - should(res.body).have.property('models', []); - done(); - }); - }); - it('changes user details as given', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {name: 'adminnew', email: 'adminnew@bosch.com', pass: 'Abc123##', location: 'Abt', devices: ['test'], models: ['120000000000000000000002']} - }).end(err => { - if (err) return done (err); - UserModel.find({name: 'adminnew'}).lean().exec( (err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.only.keys('_id', 'name', 'pass', 'email', 'level', 'location', 'devices', 'models', 'key', 'status', '__v'); - should(data[0]).have.property('_id'); - should(data[0]).have.property('name', 'adminnew'); - should(data[0]).have.property('email', 'adminnew@bosch.com'); - should(data[0]).have.property('pass').not.eql('Abc123##'); - should(data[0]).have.property('level', 'admin'); - should(data[0]).have.property('location', 'Abt'); - should(data[0]).have.property('devices', ['test']); - should(data[0].models[0].toString()).be.eql('120000000000000000000002'); - should(data[0]).have.property('status', 'new'); - done(); - }); - }); - }); - it('creates a changelog', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {name: 'adminnew', email: 'adminnew@bosch.com', pass: 'Abc123##', location: 'Abt', devices: ['test']}, - log: { - collection: 'users', - dataIgn: ['pass'] - } - }); - }); - it('lets the admin change a user level', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user/janedoe', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {level: 'read'} - }).end(err => { - if (err) return done (err); - UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.property('level', 'read'); - done(); - }); - }); - }); - it('does not change the level', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user', - auth: {basic: 'janedoe'}, - httpStatus: 400, default: false, - req: {level: 'read'} - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'Invalid body format', details: '"level" is not allowed'}); - UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.property('level', 'write'); - done(); - }); - }); - }); - it('lets the admin change accessible models', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user/janedoe', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {models: ['120000000000000000000001']} - }).end(err => { - if (err) return done (err); - UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0].models[0].toString()).be.eql('120000000000000000000001'); - done(); - }); - }); - }); - it('does not change the models', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user', - auth: {basic: 'janedoe'}, - httpStatus: 400, default: false, - req: {models: ['120000000000000000000001']} - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'Invalid body format', details: '"models" is not allowed'}); - UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.property('models', []); - done(); - }); - }); - }); - it('rejects a username already in use', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user', - auth: {basic: 'admin'}, - httpStatus: 400, default: false, - req: {name: 'janedoe'} - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'Username already taken'}); - UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - done(); - }); - }); - }); - it('rejects a username which is in the special names', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/user/new', - auth: {basic: 'admin'}, - httpStatus: 400, default: false, - req: {email: 'j.doe@bosch.com', name: 'passreset', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II'], models: []}, - res: {status: 'Username already taken'} - }); - }); - it('rejects invalid user details', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', location: 44, devices: ['Alpha II']}, - res: {status: 'Invalid body format', details: '"location" must be a string'} - }); - }); - it('rejects an invalid email address', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {email: 'john.doe'}, - res: {status: 'Invalid body format', details: '"email" must be a valid email'} - }); - }); - it('rejects an invalid password', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {pass: 'pass'}, - res: {status: 'Invalid body format', details: '"pass" length must be at least 8 characters long'} - }); - }); - it('rejects requests from non-admins for another user', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user/admin', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {} - }); - }); - it('rejects requests from a user API key', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user', - auth: {key: 'janedoe'}, - httpStatus: 401, - req: {} - }); - }); - it('rejects requests from an admin API key', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user/janedoe', - auth: {key: 'admin'}, - httpStatus: 401, - req: {} - }); - }); - it('returns 404 for an unknown user', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user/unknown', - auth: {basic: 'admin'}, - httpStatus: 404, - req: {} - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user/janedoe', - httpStatus: 401, - req: {} - }); - }); - }); + describe('PUT /user/{name}', () => { + it('returns own user details', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'devices', 'models'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('devices', ['Alpha I']); + should(res.body).have.property('models', []); + done(); + }); + }); + it('returns other user details for admin', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/janedoe', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'devices', 'models'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('devices', ['Alpha I']); + should(res.body).have.property('models', []); + done(); + }); + }); + it('changes user details as given', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'adminnew', email: 'adminnew@bosch.com', pass: 'Abc123##', location: 'Abt', devices: ['test'], models: ['120000000000000000000002']} + }).end(err => { + if (err) return done (err); + UserModel.find({name: 'adminnew'}).lean().exec( (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'pass', 'email', 'level', 'location', 'devices', 'models', 'key', 'status', '__v'); + should(data[0]).have.property('_id'); + should(data[0]).have.property('name', 'adminnew'); + should(data[0]).have.property('email', 'adminnew@bosch.com'); + should(data[0]).have.property('pass').not.eql('Abc123##'); + should(data[0]).have.property('level', 'admin'); + should(data[0]).have.property('location', 'Abt'); + should(data[0]).have.property('devices', ['test']); + should(data[0].models[0].toString()).be.eql('120000000000000000000002'); + should(data[0]).have.property('status', 'new'); + done(); + }); + }); + }); + it('creates a changelog', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {name: 'adminnew', email: 'adminnew@bosch.com', pass: 'Abc123##', location: 'Abt', devices: ['test']}, + log: { + collection: 'users', + dataIgn: ['pass'] + } + }); + }); + it('lets the admin change a user level', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/janedoe', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {level: 'read'} + }).end(err => { + if (err) return done (err); + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('level', 'read'); + done(); + }); + }); + }); + it('does not change the level', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'janedoe'}, + httpStatus: 400, default: false, + req: {level: 'read'} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'Invalid body format', details: '"level" is not allowed'}); + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('level', 'write'); + done(); + }); + }); + }); + it('lets the admin change accessible models', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/janedoe', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {models: ['120000000000000000000001']} + }).end(err => { + if (err) return done (err); + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0].models[0].toString()).be.eql('120000000000000000000001'); + done(); + }); + }); + }); + it('does not change the models', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'janedoe'}, + httpStatus: 400, default: false, + req: {models: ['120000000000000000000001']} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'Invalid body format', details: '"models" is not allowed'}); + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('models', []); + done(); + }); + }); + }); + it('rejects a username already in use', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'admin'}, + httpStatus: 400, default: false, + req: {name: 'janedoe'} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'Username already taken'}); + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + done(); + }); + }); + }); + it('rejects a username which is in the special names', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, default: false, + req: {email: 'j.doe@bosch.com', name: 'passreset', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II'], models: []}, + res: {status: 'Username already taken'} + }); + }); + it('rejects invalid user details', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', location: 44, devices: ['Alpha II']}, + res: {status: 'Invalid body format', details: '"location" must be a string'} + }); + }); + it('rejects an invalid email address', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe'}, + res: {status: 'Invalid body format', details: '"email" must be a valid email'} + }); + }); + it('rejects an invalid password', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {pass: 'pass'}, + res: {status: 'Invalid body format', details: '"pass" length must be at least 8 characters long'} + }); + }); + it('rejects requests from non-admins for another user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/admin', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {} + }); + }); + it('rejects requests from a user API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user', + auth: {key: 'janedoe'}, + httpStatus: 401, + req: {} + }); + }); + it('rejects requests from an admin API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/janedoe', + auth: {key: 'admin'}, + httpStatus: 401, + req: {} + }); + }); + it('returns 404 for an unknown user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/unknown', + auth: {basic: 'admin'}, + httpStatus: 404, + req: {} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/janedoe', + httpStatus: 401, + req: {} + }); + }); + }); - describe('DELETE /user/{name}', () => { - it('sets own user details to deleted', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/user', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'OK'}); - UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.property('status', 'deleted'); - done(); - }); - }); - }); - it('sets other user details to deleted for admin', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/user/janedoe', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'OK'}); - UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.property('status', 'deleted'); - done(); - }); - }); - }); - it('creates a changelog', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/user', - auth: {basic: 'janedoe'}, - httpStatus: 200, - log: { - collection: 'users', - dataAdd: {status: 'deleted'} - } - }); - }); - it('rejects requests from non-admins for another user', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/user/admin', - auth: {basic: 'janedoe'}, - httpStatus: 403 - }); - }); - it('rejects requests from a user API key', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/user', - auth: {key: 'janedoe'}, - httpStatus: 401 - }); - }); - it('rejects requests from an admin API key', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/user/janedoe', - auth: {key: 'admin'}, - httpStatus: 401 - }); - }); - it('returns 404 for an unknown user', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/user/unknown', - auth: {basic: 'admin'}, - httpStatus: 404 - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/user/janedoe', - httpStatus: 401 - }); - }); - }); + describe('DELETE /user/{name}', () => { + it('sets own user details to deleted', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('status', 'deleted'); + done(); + }); + }); + }); + it('sets other user details to deleted for admin', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user/janedoe', + auth: {basic: 'admin'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('status', 'deleted'); + done(); + }); + }); + }); + it('creates a changelog', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user', + auth: {basic: 'janedoe'}, + httpStatus: 200, + log: { + collection: 'users', + dataAdd: {status: 'deleted'} + } + }); + }); + it('rejects requests from non-admins for another user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user/admin', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('rejects requests from a user API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects requests from an admin API key', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user/janedoe', + auth: {key: 'admin'}, + httpStatus: 401 + }); + }); + it('returns 404 for an unknown user', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user/unknown', + auth: {basic: 'admin'}, + httpStatus: 404 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'delete', + url: '/user/janedoe', + httpStatus: 401 + }); + }); + }); - describe('PUT /user/restore/{name}', () => { - it('sets the status', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user/restore/customerold', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {} - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'OK'}); - UserModel.findOne({name: 'customerold'}).lean().exec((err, data: any) => { - if (err) return done(err); - should(data).have.property('status','new'); - done(); - }); - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user/restore/customerold', - auth: {key: 'admin'}, - httpStatus: 401, - req: {} - }); - }); - it('rejects a write user', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user/restore/customerold', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {} - }); - }); - it('returns 404 for an unknown sample', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user/restore/xxx', - auth: {basic: 'admin'}, - httpStatus: 404, - req: {} - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/user/restore/customerold', - httpStatus: 401, - req: {} - }); - }); - }); + describe('PUT /user/restore/{name}', () => { + it('sets the status', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/restore/customerold', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'OK'}); + UserModel.findOne({name: 'customerold'}).lean().exec((err, data: any) => { + if (err) return done(err); + should(data).have.property('status','new'); + done(); + }); + }); + }); + it('rejects an API key', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/restore/customerold', + auth: {key: 'admin'}, + httpStatus: 401, + req: {} + }); + }); + it('rejects a write user', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/restore/customerold', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {} + }); + }); + it('returns 404 for an unknown sample', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/restore/xxx', + auth: {basic: 'admin'}, + httpStatus: 404, + req: {} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/user/restore/customerold', + httpStatus: 401, + req: {} + }); + }); + }); - describe('GET /user/key', () => { - it('returns the right API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/user/key', - auth: {basic: 'janedoe'}, - httpStatus: 200, - res: {key: TestHelper.auth.janedoe.key} - }); - }); - it('rejects requests from an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/user/key', - auth: {key: 'janedoe'}, - httpStatus: 401 - }); - }); - it('rejects requests from an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/user/key', - httpStatus: 401 - }); - }); - }); + describe('GET /user/key', () => { + it('returns the right API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/key', + auth: {basic: 'janedoe'}, + httpStatus: 200, + res: {key: TestHelper.auth.janedoe.key} + }); + }); + it('rejects requests from an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/key', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects requests from an API key', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/user/key', + httpStatus: 401 + }); + }); + }); - describe('POST /user/new', () => { - it('returns the added user data', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/user/new', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II'], models: ['120000000000000000000003']} - }).end((err, res) => { - if (err) return done (err); - should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'devices', 'models'); - should(res.body).have.property('_id').be.type('string'); - should(res.body).have.property('email', 'john.doe@bosch.com'); - should(res.body).have.property('name', 'johndoe'); - should(res.body).have.property('level', 'read'); - should(res.body).have.property('location', 'Rng'); - should(res.body).have.property('devices', ['Alpha II']); - should(res.body).have.property('models', ['120000000000000000000003']); - done(); - }); - }); - it('stores the data', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/user/new', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II'], models: ['120000000000000000000002']} - }).end(err => { - if (err) return done (err); - UserModel.find({name: 'johndoe'}).lean().exec( (err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - should(data[0]).have.only.keys('_id', 'name', 'pass', 'email', 'level', 'location', 'devices', 'models', 'key', 'status', '__v'); - should(data[0]).have.property('_id'); - should(data[0]).have.property('name', 'johndoe'); - should(data[0]).have.property('email', 'john.doe@bosch.com'); - should(data[0]).have.property('pass').not.eql('Abc123!#'); - should(data[0]).have.property('level', 'read'); - should(data[0]).have.property('location', 'Rng'); - should(data[0]).have.property('devices', ['Alpha II']); - should(data[0].models.toString()).be.eql('120000000000000000000002'); - should(data[0]).have.property('status', 'new'); - done(); - }); - }); - }); - it('creates a changelog', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/user/new', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II'], models: ['120000000000000000000002']}, - log: { - collection: 'users', - dataIgn: ['pass', 'key'], - dataAdd: { status: 'new'} - } - }); - }); - it('rejects a username already in use', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/user/new', - auth: {basic: 'admin'}, - httpStatus: 400, default: false, - req: {email: 'j.doe@bosch.com', name: 'janedoe', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II'], models: []} - }).end((err, res) => { - if (err) return done (err); - should(res.body).be.eql({status: 'Username already taken'}); - UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { - if (err) return done(err); - should(data).have.lengthOf(1); - done(); - }); - }); - }); - it('rejects a username which is in the special names', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/user/new', - auth: {basic: 'admin'}, - httpStatus: 400, default: false, - req: {email: 'j.doe@bosch.com', name: 'passreset', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II'], models: []}, - res: {status: 'Username already taken'} - }); - }); - it('rejects invalid user details', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/user/new', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 44, devices: ['Alpha II']}, - res: {status: 'Invalid body format', details: '"location" must be a string'} - }); - }); - it('rejects an invalid user level', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/user/new', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'xxx', location: 'Rng', devices: ['Alpha II']}, - res: {status: 'Invalid body format', details: '"level" must be one of [predict, read, write, dev, admin]'} - }); - }); - it('rejects an invalid email address', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/user/new', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {email: 'john.doe', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II']}, - res: {status: 'Invalid body format', details: '"email" must be a valid email'} - }); - }); - it('rejects an invalid password', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/user/new', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'pass', level: 'read', location: 'Rng', devices: ['Alpha II']}, - res: {status: 'Invalid body format', details: '"pass" length must be at least 8 characters long'} - }); - }); - it('rejects an invalid model', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/user/new', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II'], models: ['120000000000000000000001', '000000000000000000000001']}, - res: {status: 'Invalid model id'} - }); - }); - it('rejects requests from non-admins', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/user/new', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II']} - }); - }); - it('rejects requests from an admin API key', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/user/new', - auth: {key: 'admin'}, - httpStatus: 401, - req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II']} - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/user/new', - httpStatus: 401, - req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II']} - }); - }); - }); + describe('POST /user/new', () => { + it('returns the added user data', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II'], models: ['120000000000000000000003']} + }).end((err, res) => { + if (err) return done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'devices', 'models'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'john.doe@bosch.com'); + should(res.body).have.property('name', 'johndoe'); + should(res.body).have.property('level', 'read'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('devices', ['Alpha II']); + should(res.body).have.property('models', ['120000000000000000000003']); + done(); + }); + }); + it('stores the data', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II'], models: ['120000000000000000000002']} + }).end(err => { + if (err) return done (err); + UserModel.find({name: 'johndoe'}).lean().exec( (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'pass', 'email', 'level', 'location', 'devices', 'models', 'key', 'status', '__v'); + should(data[0]).have.property('_id'); + should(data[0]).have.property('name', 'johndoe'); + should(data[0]).have.property('email', 'john.doe@bosch.com'); + should(data[0]).have.property('pass').not.eql('Abc123!#'); + should(data[0]).have.property('level', 'read'); + should(data[0]).have.property('location', 'Rng'); + should(data[0]).have.property('devices', ['Alpha II']); + should(data[0].models.toString()).be.eql('120000000000000000000002'); + should(data[0]).have.property('status', 'new'); + done(); + }); + }); + }); + it('creates a changelog', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 200, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II'], models: ['120000000000000000000002']}, + log: { + collection: 'users', + dataIgn: ['pass', 'key'], + dataAdd: { status: 'new'} + } + }); + }); + it('rejects a username already in use', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, default: false, + req: {email: 'j.doe@bosch.com', name: 'janedoe', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II'], models: []} + }).end((err, res) => { + if (err) return done (err); + should(res.body).be.eql({status: 'Username already taken'}); + UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + done(); + }); + }); + }); + it('rejects a username which is in the special names', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, default: false, + req: {email: 'j.doe@bosch.com', name: 'passreset', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II'], models: []}, + res: {status: 'Username already taken'} + }); + }); + it('rejects invalid user details', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 44, devices: ['Alpha II']}, + res: {status: 'Invalid body format', details: '"location" must be a string'} + }); + }); + it('rejects an invalid user level', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'xxx', location: 'Rng', devices: ['Alpha II']}, + res: {status: 'Invalid body format', details: '"level" must be one of [predict, read, write, dev, admin]'} + }); + }); + it('rejects an invalid email address', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II']}, + res: {status: 'Invalid body format', details: '"email" must be a valid email'} + }); + }); + it('rejects an invalid password', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'pass', level: 'read', location: 'Rng', devices: ['Alpha II']}, + res: {status: 'Invalid body format', details: '"pass" length must be at least 8 characters long'} + }); + }); + it('rejects an invalid model', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'admin'}, + httpStatus: 400, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II'], models: ['120000000000000000000001', '000000000000000000000001']}, + res: {status: 'Invalid model id'} + }); + }); + it('rejects requests from non-admins', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {basic: 'janedoe'}, + httpStatus: 403, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II']} + }); + }); + it('rejects requests from an admin API key', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + auth: {key: 'admin'}, + httpStatus: 401, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II']} + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/new', + httpStatus: 401, + req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', devices: ['Alpha II']} + }); + }); + }); - describe('POST /user/passreset', () => { - it('returns the ok response', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/user/passreset', - httpStatus: 200, - req: {email: 'jane.doe@bosch.com', name: 'janedoe'}, - res: {status: 'OK'} - }); - }); - it('creates a changelog', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/user/passreset', - httpStatus: 200, - req: {email: 'jane.doe@bosch.com', name: 'janedoe'}, - log: { - collection: 'users', - dataIgn: ['email', 'name', 'pass'] - } - }); - }); - it('returns 404 for wrong username/email combo', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/user/passreset', - httpStatus: 404, - req: {email: 'jane.doe@bosch.com', name: 'admin'} - }); - }); - it('returns 404 for unknown username', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/user/passreset', - httpStatus: 404, - req: {email: 'jane.doe@bosch.com', name: 'username'} - }); - }); - it('changes the user password', done => { - UserModel.find({name: 'janedoe'}).lean().exec( (err, data: any) => { - if (err) return done(err); - const oldpass = data[0].pass; - TestHelper.request(server, done, { - method: 'post', - url: '/user/passreset', - httpStatus: 200, - req: {email: 'jane.doe@bosch.com', name: 'janedoe'} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - UserModel.find({name: 'janedoe'}).lean().exec( (err, data: any) => { - if (err) return done(err); - should(data[0].pass).not.eql(oldpass); - done(); - }); - }); - }); - }); - }); -}); \ No newline at end of file + describe('POST /user/passreset', () => { + it('returns the ok response', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/passreset', + httpStatus: 200, + req: {email: 'jane.doe@bosch.com', name: 'janedoe'}, + res: {status: 'OK'} + }); + }); + it('creates a changelog', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/passreset', + httpStatus: 200, + req: {email: 'jane.doe@bosch.com', name: 'janedoe'}, + log: { + collection: 'users', + dataIgn: ['email', 'name', 'pass'] + } + }); + }); + it('returns 404 for wrong username/email combo', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/passreset', + httpStatus: 404, + req: {email: 'jane.doe@bosch.com', name: 'admin'} + }); + }); + it('returns 404 for unknown username', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/user/passreset', + httpStatus: 404, + req: {email: 'jane.doe@bosch.com', name: 'username'} + }); + }); + it('changes the user password', done => { + UserModel.find({name: 'janedoe'}).lean().exec( (err, data: any) => { + if (err) return done(err); + const oldpass = data[0].pass; + TestHelper.request(server, done, { + method: 'post', + url: '/user/passreset', + httpStatus: 200, + req: {email: 'jane.doe@bosch.com', name: 'janedoe'} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({status: 'OK'}); + UserModel.find({name: 'janedoe'}).lean().exec( (err, data: any) => { + if (err) return done(err); + should(data[0].pass).not.eql(oldpass); + done(); + }); + }); + }); + }); + }); +}); diff --git a/src/routes/user.ts b/src/routes/user.ts index 7f5e791..0238713 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -15,206 +15,206 @@ const router = express.Router(); router.get('/users', (req, res) => { - if (!req.auth(res, ['admin'], 'basic')) return; + if (!req.auth(res, ['admin'], 'basic')) return; - UserModel.find({}).lean().exec( (err, data:any) => { - // validate all and filter null values from validation errors - res.json(_.compact(data.map(e => UserValidate.output(e, 'admin')))); - }); + UserModel.find({}).lean().exec( (err, data:any) => { + // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => UserValidate.output(e, 'admin')))); + }); }); // 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) => { - if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return; - const username = getUsername(req, res); - if (!username) return; - UserModel.findOne({name: username}).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'}); - } - }); + const username = getUsername(req, res); + if (!username) return; + UserModel.findOne({name: username}).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'}); + } + }); }); // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new router.put('/user:username([/](?!key|new|restore).?*|/?)', async (req, res, next) => { - if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return; - const username = getUsername(req, res); - if (!username) return; + const username = getUsername(req, res); + if (!username) return; - const {error, value: user} = UserValidate.input(req.body, 'change' + - (req.authDetails.level === 'admin'? 'admin' : '')); - if (error) return res400(error, res); + const {error, value: user} = UserValidate.input(req.body, 'change' + + (req.authDetails.level === 'admin'? 'admin' : '')); + if (error) return res400(error, res); - if (user.hasOwnProperty('pass')) { - user.pass = bcrypt.hashSync(user.pass, 10); - } + if (user.hasOwnProperty('pass')) { + user.pass = bcrypt.hashSync(user.pass, 10); + } - // check that user does not already exist if new name was specified - if (user.hasOwnProperty('name') && user.name !== username) { - if (!await usernameCheck(user.name, res, next)) return; - } + // check that user does not already exist if new name was specified + if (user.hasOwnProperty('name') && user.name !== username) { + if (!await usernameCheck(user.name, res, next)) return; + } - if (user.hasOwnProperty('models')) { - if (!await modelsCheck(user.models, res, next)) return; - } + if (user.hasOwnProperty('models')) { + if (!await modelsCheck(user.models, res, next)) return; + } - // get current mail address to compare to given address - const oldUserData = await UserModel.findOne({name: username}).lean().exec().catch(err => next(err)); + // get current mail address to compare to given address + const oldUserData = await UserModel.findOne({name: username}).lean().exec().catch(err => next(err)); - await UserModel.findOneAndUpdate({name: username}, user, {new: true}).log(req).lean().exec( (err, data:any) => { - if (err) return next(err); - if (data) { - if (data.email !== oldUserData.email) { // mail address was changed, send notice to old address - Mail.send(oldUserData.email, 'Email change in your DeFinMa database account', - 'Hi,

Your email address of your DeFinMa account was changed to ' + data.mail + - '

If you actually did this, just delete this email.' + - '

If you did not change your email, someone might be messing around with your account, ' + - 'so talk to the sysadmin quickly!

Have a nice day.' + - '

The DeFinMa team'); - } - res.json(UserValidate.output(data)); - } - else { - res.status(404).json({status: 'Not found'}); - } - }); + await UserModel.findOneAndUpdate({name: username}, user, {new: true}).log(req).lean().exec( (err, data:any) => { + if (err) return next(err); + if (data) { + if (data.email !== oldUserData.email) { // mail address was changed, send notice to old address + Mail.send(oldUserData.email, 'Email change in your DeFinMa database account', + 'Hi,

Your email address of your DeFinMa account was changed to ' + data.mail + + '

If you actually did this, just delete this email.' + + '

If you did not change your email, someone might be messing around with your account, ' + + 'so talk to the sysadmin quickly!

Have a nice day.' + + '

The DeFinMa team'); + } + res.json(UserValidate.output(data)); + } + else { + res.status(404).json({status: 'Not found'}); + } + }); }); // 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) => { - if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return; - const username = getUsername(req, res); - if (!username) return; + const username = getUsername(req, res); + if (!username) return; - UserModel.findOneAndUpdate({name: username}, {status: globals.status.del}).log(req).lean().exec( (err, data:any) => { - if (err) return next(err); - if (data) { - res.json({status: 'OK'}) - } - else { - res.status(404).json({status: 'Not found'}); - } - }); + UserModel.findOneAndUpdate({name: username}, {status: globals.status.del}).log(req).lean().exec( (err, data:any) => { + if (err) return next(err); + if (data) { + res.json({status: 'OK'}) + } + else { + res.status(404).json({status: 'Not found'}); + } + }); }); router.put('/user/restore/:username', (req, res, next) => { - if (!req.auth(res, ['admin'], 'basic')) return; + if (!req.auth(res, ['admin'], 'basic')) return; - UserModel.findOneAndUpdate({name: req.params.username}, {status: globals.status.new}) - .log(req).lean().exec((err, data) => { - if (err) return next(err); + UserModel.findOneAndUpdate({name: req.params.username}, {status: globals.status.new}) + .log(req).lean().exec((err, data) => { + if (err) return next(err); - if (!data) { - return res.status(404).json({status: 'Not found'}); - } - res.json({status: 'OK'}); - }); + if (!data) { + return res.status(404).json({status: 'Not found'}); + } + res.json({status: 'OK'}); + }); }); router.get('/user/key', (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return; + if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return; - UserModel.findOne({name: req.authDetails.username}).lean().exec( (err, data:any) => { - if (err) return next(err); - res.json({key: data.key}); - }); + UserModel.findOne({name: req.authDetails.username}).lean().exec( (err, data:any) => { + if (err) return next(err); + res.json({key: data.key}); + }); }); router.post('/user/new', async (req, res, next) => { - if (!req.auth(res, ['admin'], 'basic')) return; + if (!req.auth(res, ['admin'], 'basic')) return; - // validate input - const {error, value: user} = UserValidate.input(req.body, 'new'); - if (error) return res400(error, res); + // validate input + const {error, value: user} = UserValidate.input(req.body, 'new'); + if (error) return res400(error, res); - // check that user does not already exist - if (!await usernameCheck(user.name, res, next)) return; - if (!await modelsCheck(user.models, res, next)) return; + // check that user does not already exist + if (!await usernameCheck(user.name, res, next)) return; + if (!await modelsCheck(user.models, res, next)) return; - user.key = mongoose.Types.ObjectId(); // use object id as unique API key - user.status = globals.status.new; - 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); - db.log(req, 'users', {_id: data._id}, data.toObject()); - res.json(UserValidate.output(data.toObject())); - }); - }); + user.key = mongoose.Types.ObjectId(); // use object id as unique API key + user.status = globals.status.new; + 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); + db.log(req, 'users', {_id: data._id}, data.toObject()); + res.json(UserValidate.output(data.toObject())); + }); + }); }); router.post('/user/passreset', (req, res, next) => { - // check if user/email combo exists - 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); // generate temporary password - bcrypt.hash(newPass, 10, (err, hash) => { // password hashing - if (err) return next(err); + // check if user/email combo exists + 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); // generate temporary password + bcrypt.hash(newPass, 10, (err, hash) => { // password hashing + if (err) return next(err); - UserModel.findByIdAndUpdate(data[0]._id, {pass: hash}).log(req).exec(err => { // write new password - if (err) return next(err); + UserModel.findByIdAndUpdate(data[0]._id, {pass: hash}).log(req).exec(err => { // write new password + if (err) return next(err); - // send email - Mail.send(data[0].email, 'Your new password for the DeFinMa 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 DeFinMa team', err => { - if (err) return next(err); - res.json({status: 'OK'}); - }); - }); - }); - } - else { - res.status(404).json({status: 'Not found'}); - } - }); + // send email + Mail.send(data[0].email, 'Your new password for the DeFinMa 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 DeFinMa team', err => { + if (err) return next(err); + res.json({status: 'OK'}); + }); + }); + }); + } + else { + res.status(404).json({status: 'Not found'}); + } + }); }); 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; - } + 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; - if (userData || UserValidate.isSpecialName(name)) { - res.status(400).json({status: 'Username already taken'}); - return false; - } - return true; + const userData = await UserModel.findOne({name: name}).lean().exec().catch(err => next(err)) as any; +if (userData instanceof Error) return false; +if (userData || UserValidate.isSpecialName(name)) { + res.status(400).json({status: 'Username already taken'}); + return false; +} +return true; } async function modelsCheck (models, res, next) { // check if model ids exist, returns false on error - let result = true; - for (let i in models) { - const model = await ModelModel.findOne({'models._id': mongoose.Types.ObjectId(models[i])}) - .lean().exec().catch(err => next(err)) as any; - if(!model) { - res.status(400).json({status: 'Invalid model id'}); - result = false; - break; - } - } - return result; -} \ No newline at end of file + let result = true; +for (let i in models) { + const model = await ModelModel.findOne({'models._id': mongoose.Types.ObjectId(models[i])}) + .lean().exec().catch(err => next(err)) as any; + if(!model) { + res.status(400).json({status: 'Invalid model id'}); + result = false; + break; + } +} +return result; +} diff --git a/src/routes/validate/help.ts b/src/routes/validate/help.ts index 6dcf64a..cb97d81 100644 --- a/src/routes/validate/help.ts +++ b/src/routes/validate/help.ts @@ -2,33 +2,33 @@ import Joi from 'joi'; import globals from '../../globals'; export default class HelpValidate { - private static help = { - text: Joi.string() - .allow('') - .max(8192), + private static help = { + text: Joi.string() + .allow('') + .max(8192), - level: Joi.string() - .valid('none', ...Object.values(globals.levels)) - } + level: Joi.string() + .valid('none', ...Object.values(globals.levels)) + } - static input (data) { - return Joi.object({ - text: this.help.text.required(), - level: this.help.level.required() - }).validate(data); - } + static input (data) { + return Joi.object({ + text: this.help.text.required(), + level: this.help.level.required() + }).validate(data); + } - static output (data) { - const {value, error} = Joi.object({ - text: this.help.text, - level: this.help.level - }).validate(data, {stripUnknown: true}); - return error !== undefined? null : value; - } + static output (data) { + const {value, error} = Joi.object({ + text: this.help.text, + level: this.help.level + }).validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } - static params(data) { - return Joi.object({ - key: Joi.string().min(1).max(128) - }).validate(data); - } -} \ No newline at end of file + static params(data) { + return Joi.object({ + key: Joi.string().min(1).max(128) + }).validate(data); + } +} diff --git a/src/routes/validate/id.ts b/src/routes/validate/id.ts index e0d8362..9e85b58 100644 --- a/src/routes/validate/id.ts +++ b/src/routes/validate/id.ts @@ -1,33 +1,33 @@ import Joi from 'joi'; export default class IdValidate { - private static id = Joi.string() - .pattern(new RegExp('[0-9a-f]{24}')) - .length(24) - .messages({'string.pattern.base': 'Invalid object id'}); + private static id = Joi.string() + .pattern(new RegExp('[0-9a-f]{24}')) + .length(24) + .messages({'string.pattern.base': 'Invalid object id'}); - static get () { // return joi validation - return this.id; - } + static get () { // return joi validation + return this.id; + } - static valid (id) { // validate id - return this.id.validate(id).error === undefined; - } + static valid (id) { // validate id + return this.id.validate(id).error === undefined; + } - static parameter () { // :id url parameter - return ':id([0-9a-f]{24})'; - } + static parameter () { // :id url parameter + return ':id([0-9a-f]{24})'; + } - static stringify (data) { // convert all ObjectID objects to plain strings - Object.keys(data).forEach(key => { - // stringify id - if (data[key] !== null && data[key].hasOwnProperty('_bsontype') && data[key]._bsontype === 'ObjectID') { - 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; - } -} \ No newline at end of file + static stringify (data) { // convert all ObjectID objects to plain strings + Object.keys(data).forEach(key => { + // stringify id + if (data[key] !== null && data[key].hasOwnProperty('_bsontype') && data[key]._bsontype === 'ObjectID') { + 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 cf5782d..7bbb698 100644 --- a/src/routes/validate/material.ts +++ b/src/routes/validate/material.ts @@ -4,99 +4,99 @@ import IdValidate from './id'; import globals from '../../globals'; export default class MaterialValidate { // validate input for material - private static material = { - name: Joi.string() - .max(128), + private static material = { + name: Joi.string() + .max(128), - supplier: Joi.string() - .max(128), + supplier: Joi.string() + .max(128), - group: Joi.string() - .max(128), + group: Joi.string() + .max(128), - properties: Joi.object(), + properties: Joi.object(), - numbers: Joi.array() - .items( - Joi.string() - .max(64) - ), + numbers: Joi.array() + .items( + Joi.string() + .max(64) + ), - status: Joi.string() - .valid(...Object.values(globals.status)) - }; + status: Joi.string() + .valid(...Object.values(globals.status)) + }; - 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(), - supplier: this.material.supplier.required(), - group: this.material.group.required(), - properties: this.material.properties.required(), - numbers: this.material.numbers.required() - }).validate(data); - } - else if (param === 'change') { - return Joi.object({ - name: this.material.name, - supplier: this.material.supplier, - group: this.material.group, - properties: this.material.properties, - numbers: this.material.numbers - }).validate(data); - } - else { - return{error: 'No parameter specified!', value: {}}; - } - } + 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(), + supplier: this.material.supplier.required(), + group: this.material.group.required(), + properties: this.material.properties.required(), + numbers: this.material.numbers.required() + }).validate(data); + } + else if (param === 'change') { + return Joi.object({ + name: this.material.name, + supplier: this.material.supplier, + group: this.material.group, + properties: this.material.properties, + numbers: this.material.numbers + }).validate(data); + } + else { + return{error: 'No parameter specified!', value: {}}; + } + } - static output (data, status = false) { // validate output and strip unwanted properties, returns null if not valid - data = IdValidate.stringify(data); - data.group = data.group_id.name; - data.supplier = data.supplier_id.name; - const validate: any = { - _id: IdValidate.get(), - name: this.material.name, - supplier: this.material.supplier, - group: this.material.group, - properties: this.material.properties, - numbers: this.material.numbers - }; - if (status) { - validate.status = this.material.status; - } - const {value, error} = Joi.object(validate).validate(data, {stripUnknown: true}); - return error !== undefined? null : value; - } + static output (data, status = false) { // validate output and strip unwanted properties, returns null if not valid + data = IdValidate.stringify(data); + data.group = data.group_id.name; + data.supplier = data.supplier_id.name; + const validate: any = { + _id: IdValidate.get(), + name: this.material.name, + supplier: this.material.supplier, + group: this.material.group, + properties: this.material.properties, + numbers: this.material.numbers + }; + if (status) { + validate.status = this.material.status; + } + const {value, error} = Joi.object(validate).validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } - static outputGroups (data) {// validate groups output and strip unwanted properties, returns null if not valid - const {value, error} = this.material.group.validate(data, {stripUnknown: true}); - return error !== undefined? null : value; - } + static outputGroups (data) {// validate groups output and strip unwanted properties, returns null if not valid + const {value, error} = this.material.group.validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } - static outputSuppliers (data) {// validate suppliers output and strip unwanted properties, returns null if not valid - const {value, error} = this.material.supplier.validate(data, {stripUnknown: true}); - return error !== undefined? null : value; - } + static outputSuppliers (data) {// validate suppliers output and strip unwanted properties, returns null if not valid + const {value, error} = this.material.supplier.validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } - static outputV() { // return output validator - return Joi.object({ - _id: IdValidate.get(), - name: this.material.name, - supplier: this.material.supplier, - group: this.material.group, - properties: this.material.properties, - numbers: this.material.numbers - }); - } + static outputV() { // return output validator + return Joi.object({ + _id: IdValidate.get(), + name: this.material.name, + supplier: this.material.supplier, + group: this.material.group, + properties: this.material.properties, + numbers: this.material.numbers + }); + } - static query (data, dev = false) { - const acceptedStatuses = [globals.status.val, globals.status.new]; - if (dev) { // dev and admin can also access deleted samples - acceptedStatuses.push(globals.status.del) - } - return Joi.object({ - status: Joi.array().items(Joi.string().valid(...acceptedStatuses)).default([globals.status.val]) - }).validate(data); - } -} \ No newline at end of file + static query (data, dev = false) { + const acceptedStatuses = [globals.status.val, globals.status.new]; + if (dev) { // dev and admin can also access deleted samples + acceptedStatuses.push(globals.status.del) + } + return Joi.object({ + status: Joi.array().items(Joi.string().valid(...acceptedStatuses)).default([globals.status.val]) + }).validate(data); + } +} diff --git a/src/routes/validate/measurement.ts b/src/routes/validate/measurement.ts index 2c31000..f5a8f60 100644 --- a/src/routes/validate/measurement.ts +++ b/src/routes/validate/measurement.ts @@ -4,64 +4,64 @@ import IdValidate from './id'; import globals from '../../globals'; export default class MeasurementValidate { - private static measurement = { - values: Joi.object() - .pattern(/.*/, Joi.alternatives() - .try( - Joi.string().max(128), - Joi.number(), - Joi.boolean(), - Joi.array().items(Joi.array().items(Joi.number())), // for spectra - Joi.array() - ) - .allow(null) - ) - }; + private static measurement = { + values: Joi.object() + .pattern(/.*/, Joi.alternatives() +.try( + Joi.string().max(128), + Joi.number(), + Joi.boolean(), + Joi.array().items(Joi.array().items(Joi.number())), // for spectra + Joi.array() +) +.allow(null) + ) + }; - 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(), - values: this.measurement.values.required(), - measurement_template: IdValidate.get().required() - }).validate(data); - } - else if (param === 'change') { - return Joi.object({ - values: this.measurement.values - }).validate(data); - } - else { - return{error: 'No parameter specified!', value: {}}; - } - } + 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(), + values: this.measurement.values.required(), + measurement_template: IdValidate.get().required() + }).validate(data); + } + else if (param === 'change') { + return Joi.object({ + values: this.measurement.values + }).validate(data); + } + else { + return{error: 'No parameter specified!', value: {}}; + } + } - // validate output and strip unwanted properties, returns null if not valid - static output (data, req, status = false) { - data = IdValidate.stringify(data); - // spectral data not allowed for read/write users - if (['dev', 'admin'].indexOf(req.authDetails.level) < 0 && data.values[globals.spectrum.dpt]) { - delete data.values[globals.spectrum.dpt]; - } - const validation: any = { - _id: IdValidate.get(), - sample_id: IdValidate.get(), - values: this.measurement.values, - measurement_template: IdValidate.get() - }; - if (status) { - validation.status = Joi.string().valid(...Object.values(globals.status)); - } - const {value, error} = Joi.object(validation).validate(data, {stripUnknown: true}); - return error !== undefined? null : value; - } + // validate output and strip unwanted properties, returns null if not valid + static output (data, req, status = false) { + data = IdValidate.stringify(data); + // spectral data not allowed for read/write users + if (['dev', 'admin'].indexOf(req.authDetails.level) < 0 && data.values[globals.spectrum.dpt]) { + delete data.values[globals.spectrum.dpt]; + } + const validation: any = { + _id: IdValidate.get(), + sample_id: IdValidate.get(), + values: this.measurement.values, + measurement_template: IdValidate.get() + }; + if (status) { + validation.status = Joi.string().valid(...Object.values(globals.status)); + } + const {value, error} = Joi.object(validation).validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } - static outputV() { // return output validator - return Joi.object({ - _id: IdValidate.get(), - sample_id: IdValidate.get(), - values: this.measurement.values, - measurement_template: IdValidate.get() - }); - } -} \ No newline at end of file + static outputV() { // return output validator + return Joi.object({ + _id: IdValidate.get(), + sample_id: IdValidate.get(), + values: this.measurement.values, + measurement_template: IdValidate.get() + }); + } +} diff --git a/src/routes/validate/model.ts b/src/routes/validate/model.ts index 971cfd3..7700638 100644 --- a/src/routes/validate/model.ts +++ b/src/routes/validate/model.ts @@ -3,40 +3,40 @@ import IdValidate from './id'; export default class ModelValidate { // validate input for model - private static model = { - group: Joi.string() - .disallow('file') - .max(128), + private static model = { + group: Joi.string() + .disallow('file') + .max(128), - model: Joi.object({ - name: Joi.string() - .max(128) - .required(), + model: Joi.object({ + name: Joi.string() + .max(128) + .required(), - url: Joi.string() - .uri() - .max(512) - .required() - }) - }; + url: Joi.string() + .uri() + .max(512) + .required() + }) + }; - static input (data) { // validate input - return this.model.model.required().validate(data); - } + static input (data) { // validate input + return this.model.model.required().validate(data); + } - static output (data) { // validate output and strip unwanted properties, returns null if not valid - data = IdValidate.stringify(data); - const {value, error} = Joi.object({ - group: this.model.group, - models: Joi.array().items(this.model.model.append({_id: IdValidate.get()})) - }).validate(data, {stripUnknown: true}); - return error !== undefined? null : value; - } + static output (data) { // validate output and strip unwanted properties, returns null if not valid + data = IdValidate.stringify(data); + const {value, error} = Joi.object({ + group: this.model.group, + models: Joi.array().items(this.model.model.append({_id: IdValidate.get()})) + }).validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } - static fileOutput (data) { - return { - name: data.name, - size: data.data.length - } - } -} \ No newline at end of file + static fileOutput (data) { + return { + name: data.name, + size: data.data.length + } + } +} diff --git a/src/routes/validate/note_field.ts b/src/routes/validate/note_field.ts index 5185443..ead0a65 100644 --- a/src/routes/validate/note_field.ts +++ b/src/routes/validate/note_field.ts @@ -1,18 +1,18 @@ import Joi from 'joi'; export default class NoteFieldValidate { - private static note_field = { - name: Joi.string() - .max(128), + private static note_field = { + name: Joi.string() + .max(128), - qty: Joi.number() - }; + qty: Joi.number() + }; - 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 - }).validate(data, {stripUnknown: true}); - return error !== undefined? null : value; - } -} \ No newline at end of file + 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 + }).validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } +} diff --git a/src/routes/validate/parameters.ts b/src/routes/validate/parameters.ts index a54804b..6c5bf56 100644 --- a/src/routes/validate/parameters.ts +++ b/src/routes/validate/parameters.ts @@ -1,42 +1,42 @@ import Joi from 'joi'; export default class ParametersValidate { - // data to validate, parameters from template, param: 'new', 'change', 'null'(null values are allowed) - static input (data, parameters, param) { - let joiObject = {}; - parameters.forEach(parameter => { - switch (parameter.range.type) { - case 'number': joiObject[parameter.name] = Joi.number(); - break; - case 'boolean': joiObject[parameter.name] = Joi.boolean(); - break; - case 'array': joiObject[parameter.name] = Joi.array(); - break; - case 'string': joiObject[parameter.name] = Joi.string().max(128); - break; // min or max implicitly define the value to be a number - default: if (parameter.range.hasOwnProperty('min') || parameter.range.hasOwnProperty('max')) { - joiObject[parameter.name] = Joi.number(); - } - else { - joiObject[parameter.name] = Joi.string().max(128); - } - } - if (parameter.range.hasOwnProperty('min')) { - joiObject[parameter.name] = joiObject[parameter.name].min(parameter.range.min) - } - if (parameter.range.hasOwnProperty('max')) { - joiObject[parameter.name] = joiObject[parameter.name].max(parameter.range.max) - } - if (parameter.range.hasOwnProperty('values')) { - joiObject[parameter.name] = joiObject[parameter.name].valid(...parameter.range.values); - } - if (parameter.range.hasOwnProperty('required') && parameter.range.required) { - joiObject[parameter.name] = joiObject[parameter.name].required(); - } - if (param === 'null') { - joiObject[parameter.name] = joiObject[parameter.name].allow(null) - } - }); - return Joi.object(joiObject).validate(data); - } -} \ No newline at end of file + // data to validate, parameters from template, param: 'new', 'change', 'null'(null values are allowed) + static input (data, parameters, param) { + let joiObject = {}; + parameters.forEach(parameter => { + switch (parameter.range.type) { + case 'number': joiObject[parameter.name] = Joi.number(); + break; + case 'boolean': joiObject[parameter.name] = Joi.boolean(); + break; + case 'array': joiObject[parameter.name] = Joi.array(); + break; + case 'string': joiObject[parameter.name] = Joi.string().max(128); + break; // min or max implicitly define the value to be a number + default: if (parameter.range.hasOwnProperty('min') || parameter.range.hasOwnProperty('max')) { + joiObject[parameter.name] = Joi.number(); + } + else { + joiObject[parameter.name] = Joi.string().max(128); + } + } + if (parameter.range.hasOwnProperty('min')) { + joiObject[parameter.name] = joiObject[parameter.name].min(parameter.range.min) + } + if (parameter.range.hasOwnProperty('max')) { + joiObject[parameter.name] = joiObject[parameter.name].max(parameter.range.max) + } + if (parameter.range.hasOwnProperty('values')) { + joiObject[parameter.name] = joiObject[parameter.name].valid(...parameter.range.values); + } + if (parameter.range.hasOwnProperty('required') && parameter.range.required) { + joiObject[parameter.name] = joiObject[parameter.name].required(); + } + if (param === 'null') { + joiObject[parameter.name] = joiObject[parameter.name].allow(null) + } + }); + return Joi.object(joiObject).validate(data); + } +} diff --git a/src/routes/validate/res400.ts b/src/routes/validate/res400.ts index e4595c8..d5a2ba4 100644 --- a/src/routes/validate/res400.ts +++ b/src/routes/validate/res400.ts @@ -1,5 +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 + res.status(400).json({status: 'Invalid body format', details: error.details[0].message}); +} diff --git a/src/routes/validate/root.ts b/src/routes/validate/root.ts index a6dfe33..144814e 100644 --- a/src/routes/validate/root.ts +++ b/src/routes/validate/root.ts @@ -2,50 +2,50 @@ import Joi from 'joi'; import IdValidate from './id'; export default class RootValidate { // validate input for root methods - private static changelog = { - timestamp: Joi.date() - .iso() - .min('1970-01-01T00:00:00.000Z'), + private static changelog = { + timestamp: Joi.date() + .iso() + .min('1970-01-01T00:00:00.000Z'), - page: Joi.number() - .integer() - .min(0) - .default(0), + page: Joi.number() + .integer() + .min(0) + .default(0), - pagesize: Joi.number() - .integer() - .min(0) - .default(25), + pagesize: Joi.number() + .integer() + .min(0) + .default(25), - action: Joi.string(), + action: Joi.string(), - collection: Joi.string(), + collection: Joi.string(), - conditions: Joi.object(), + conditions: Joi.object(), - data: Joi.object() - }; + data: Joi.object() + }; - static changelogParams (data) { - return Joi.object({ - id: IdValidate.get(), - page: this.changelog.page, - pagesize: this.changelog.pagesize - }).validate(data); - } + static changelogParams (data) { + return Joi.object({ + id: IdValidate.get(), + page: this.changelog.page, + pagesize: this.changelog.pagesize + }).validate(data); + } - static changelogOutput (data) { - data.date = data._id.getTimestamp(); - data.collection = data.collection_name; - data = IdValidate.stringify(data); - const {value, error} = Joi.object({ - _id: IdValidate.get(), - date: this.changelog.timestamp, - action: this.changelog.action, - collection: this.changelog.collection, - conditions: this.changelog.conditions, - data: this.changelog.data, - }).validate(data, {stripUnknown: true}); - return error !== undefined? null : value; - } -} \ No newline at end of file + static changelogOutput (data) { + data.date = data._id.getTimestamp(); + data.collection = data.collection_name; + data = IdValidate.stringify(data); + const {value, error} = Joi.object({ + _id: IdValidate.get(), + date: this.changelog.timestamp, + action: this.changelog.action, + collection: this.changelog.collection, + conditions: this.changelog.conditions, + data: this.changelog.data, + }).validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } +} diff --git a/src/routes/validate/sample.ts b/src/routes/validate/sample.ts index efaedf1..6f6b0e2 100644 --- a/src/routes/validate/sample.ts +++ b/src/routes/validate/sample.ts @@ -8,273 +8,273 @@ import globals from '../../globals'; export default class SampleValidate { - private static sample = { - number: Joi.string() - .max(128), + private static sample = { + number: Joi.string() + .max(128), - color: Joi.string() - .max(128) - .allow(''), + color: Joi.string() + .max(128) + .allow(''), - type: Joi.string() - .valid('as-delivered/raw', 'processed'), + type: Joi.string() + .valid('as-delivered/raw', 'processed'), - batch: Joi.string() - .max(128) - .allow(''), + batch: Joi.string() + .max(128) + .allow(''), - condition: Joi.object(), + condition: Joi.object(), - notes: Joi.object({ - comment: Joi.string() - .max(512) - .allow('') - .allow(null), + notes: Joi.object({ + comment: Joi.string() + .max(512) + .allow('') + .allow(null), - sample_references: Joi.array() - .items(Joi.object({ - sample_id: IdValidate.get(), + sample_references: Joi.array() + .items(Joi.object({ + sample_id: IdValidate.get(), - relation: Joi.string() - .max(128) - })), + relation: Joi.string() + .max(128) + })), - custom_fields: Joi.object() - .pattern(/.*/, Joi.alternatives() - .try( - Joi.string().max(128), - Joi.number(), - Joi.boolean(), - Joi.date() - ) - ) - }), + custom_fields: Joi.object() + .pattern(/.*/, Joi.alternatives() +.try( + Joi.string().max(128), + Joi.number(), + Joi.boolean(), + Joi.date() +) + ) + }), - added: Joi.date() - .iso() - .min('1970-01-01T00:00:00.000Z'), + added: Joi.date() + .iso() + .min('1970-01-01T00:00:00.000Z'), - status: Joi.string() - .valid(...Object.values(globals.status)) - }; + status: Joi.string() + .valid(...Object.values(globals.status)) + }; - static readonly sampleKeys = [ // keys which can be found in the sample directly - '_id', - 'color', - 'number', - 'type', - 'batch', - 'added', - 'condition', - 'material_id', - 'note_id', - 'user_id' - ]; + static readonly sampleKeys = [ // keys which can be found in the sample directly + '_id', + 'color', + 'number', + 'type', + 'batch', + 'added', + 'condition', + 'material_id', + 'note_id', + 'user_id' + ]; - private static sortKeys = [ - '_id', - 'color', - 'number', - 'type', - 'batch', - 'added', - 'status', - 'notes.comment', - 'material.name', - 'material.supplier', - 'material.group', - 'material.properties.*', - 'condition.*', - `measurements.(?!${globals.spectrum.spectrum}.${globals.spectrum.dpt})*` - ]; + private static sortKeys = [ + '_id', + 'color', + 'number', + 'type', + 'batch', + 'added', + 'status', + 'notes.comment', + 'material.name', + 'material.supplier', + 'material.group', + 'material.properties.*', + 'condition.*', + `measurements.(?!${globals.spectrum.spectrum}.${globals.spectrum.dpt})*` + ]; - private static fieldKeys = [ - ...SampleValidate.sortKeys, - 'condition', - 'notes', - 'material_id', - 'material', - 'note_id', - 'user_id', - 'material._id', - 'material.numbers', - 'measurements', - `measurements.${globals.spectrum.spectrum}.${globals.spectrum.dpt}`, - ]; + private static fieldKeys = [ + ...SampleValidate.sortKeys, + 'condition', + 'notes', + 'material_id', + 'material', + 'note_id', + 'user_id', + 'material._id', + 'material.numbers', + 'measurements', + `measurements.${globals.spectrum.spectrum}.${globals.spectrum.dpt}`, + ]; - static input (data, param) { // validate input, set param to 'new' to make all attributes required - if (param === 'new') { - return Joi.object({ - color: this.sample.color.required(), - type: this.sample.type.required(), - batch: this.sample.batch.required(), - condition: this.sample.condition.required(), - material_id: IdValidate.get().required(), - notes: this.sample.notes.required() - }).validate(data); - } - else if (param === 'change') { - return Joi.object({ - color: this.sample.color, - type: this.sample.type, - batch: this.sample.batch, - condition: this.sample.condition, - material_id: IdValidate.get(), - notes: this.sample.notes, - }).validate(data); - } - else if (param === 'new-admin') { - return Joi.object({ - number: this.sample.number, - color: this.sample.color.required(), - type: this.sample.type.required(), - batch: this.sample.batch.required(), - condition: this.sample.condition.required(), - material_id: IdValidate.get().required(), - notes: this.sample.notes.required() - }).validate(data); - } - else { - return{error: 'No parameter specified!', value: {}}; - } - } + static input (data, param) { // validate input, set param to 'new' to make all attributes required + if (param === 'new') { + return Joi.object({ + color: this.sample.color.required(), + type: this.sample.type.required(), + batch: this.sample.batch.required(), + condition: this.sample.condition.required(), + material_id: IdValidate.get().required(), + notes: this.sample.notes.required() + }).validate(data); + } + else if (param === 'change') { + return Joi.object({ + color: this.sample.color, + type: this.sample.type, + batch: this.sample.batch, + condition: this.sample.condition, + material_id: IdValidate.get(), + notes: this.sample.notes, + }).validate(data); + } + else if (param === 'new-admin') { + return Joi.object({ + number: this.sample.number, + color: this.sample.color.required(), + type: this.sample.type.required(), + batch: this.sample.batch.required(), + condition: this.sample.condition.required(), + material_id: IdValidate.get().required(), + notes: this.sample.notes.required() + }).validate(data); + } + else { + return{error: 'No parameter specified!', value: {}}; + } + } - // validate output and strip unwanted properties, returns null if not valid - static output (data, param = 'refs+added', additionalParams = []) { - if (param === 'refs+added') { - param = 'refs'; - data.added = data._id.getTimestamp(); - } - data = IdValidate.stringify(data); - let joiObject; - if (param === 'refs') { - joiObject = { - _id: IdValidate.get(), - number: this.sample.number, - color: this.sample.color, - type: this.sample.type, - batch: this.sample.batch, - condition: this.sample.condition, - material_id: IdValidate.get(), - material: MaterialValidate.outputV().append({number: Joi.string().max(128).allow('')}), - note_id: IdValidate.get().allow(null), - notes: this.sample.notes, - user_id: IdValidate.get(), - added: this.sample.added, - status: this.sample.status - }; - } - else if(param === 'details') { - joiObject = { - _id: IdValidate.get(), - number: this.sample.number, - color: this.sample.color, - type: this.sample.type, - batch: this.sample.batch, - condition: this.sample.condition, - material: MaterialValidate.outputV(), - measurements: Joi.array().items(MeasurementValidate.outputV()), - notes: this.sample.notes, - user: UserValidate.username(), - status: this.sample.status - } - } - else { - return null; - } - additionalParams.forEach(param => { - joiObject[param] = Joi.any(); - }); - const {value, error} = Joi.object(joiObject).validate(data, {stripUnknown: true}); - return error !== undefined? null : value; - } + // validate output and strip unwanted properties, returns null if not valid + static output (data, param = 'refs+added', additionalParams = []) { + if (param === 'refs+added') { + param = 'refs'; + data.added = data._id.getTimestamp(); + } + data = IdValidate.stringify(data); + let joiObject; + if (param === 'refs') { + joiObject = { + _id: IdValidate.get(), + number: this.sample.number, + color: this.sample.color, + type: this.sample.type, + batch: this.sample.batch, + condition: this.sample.condition, + material_id: IdValidate.get(), + material: MaterialValidate.outputV().append({number: Joi.string().max(128).allow('')}), + note_id: IdValidate.get().allow(null), + notes: this.sample.notes, + user_id: IdValidate.get(), + added: this.sample.added, + status: this.sample.status + }; + } + else if(param === 'details') { + joiObject = { + _id: IdValidate.get(), + number: this.sample.number, + color: this.sample.color, + type: this.sample.type, + batch: this.sample.batch, + condition: this.sample.condition, + material: MaterialValidate.outputV(), + measurements: Joi.array().items(MeasurementValidate.outputV()), + notes: this.sample.notes, + user: UserValidate.username(), + status: this.sample.status + } + } + else { + return null; + } + additionalParams.forEach(param => { + joiObject[param] = Joi.any(); + }); + const {value, error} = Joi.object(joiObject).validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } - static query (data, dev = false) { - if (data.filters && data.filters.length) { - const filterValidation = Joi.array().items(Joi.string()).validate(data.filters); - if (filterValidation.error) return filterValidation; - try { - for (let i in data.filters) { - try { - data.filters[i] = decodeURIComponent(data.filters[i]); - } - catch (ignore) {} - data.filters[i] = JSON.parse(data.filters[i]); - data.filters[i].values = data.filters[i].values.map(e => { // validate filter values - if (e === null) { // null values are always allowed - return null; - } - let validator; - let field = data.filters[i].field; - if (/material\./.test(field)) { // select right validation model - validator = MaterialValidate.outputV().append({ - number: Joi.string().max(128).allow(''), - properties: Joi.alternatives().try(Joi.number(), Joi.string().max(128).allow('')) - }); - field = field.replace('material.', '').split('.')[0]; - } - else if (/measurements\./.test(field) || /condition\./.test(field)) { - validator = Joi.object({ - value: Joi.alternatives() - .try( - Joi.number(), - Joi.string().max(128).allow(''), - Joi.boolean(), - Joi.array() - ) - .allow(null) - }); - field = 'value'; - } - else if (field === 'measurements') { - validator = Joi.object({ - value: Joi.object({}).allow(null).disallow({}) - }); - field = 'value'; - } - else if (field === 'notes.comment') { - field = 'comment'; - validator = this.sample.notes - } - else { - validator = Joi.object(this.sample); - } - const {value, error} = validator.validate({[field]: e}); - if (error) throw error; // reject invalid values - return value[field]; - }); - } - } - catch (err) { - return {error: {details: [{message: 'Invalid JSON string for filter parameter'}]}, value: null} - } - } - const acceptedStatuses = [globals.status.val, globals.status.new]; - if (dev) { // dev and admin can also access deleted samples - acceptedStatuses.push(globals.status.del) - } - return Joi.object({ - status: Joi.array().items(Joi.string().valid(...acceptedStatuses)).default([globals.status.val]), - 'from-id': IdValidate.get(), - 'to-page': Joi.number().integer(), - 'page-size': Joi.number().integer().min(1), - sort: Joi.string().pattern( - new RegExp('^(' + this.sortKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')-(asc|desc)$', 'm') - ).default('_id-asc'), - output: Joi.string().valid('json', 'flatten', 'csv').default('json'), - fields: Joi.array().items(Joi.string().pattern( - new RegExp('^(' + this.fieldKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')$', 'm') - )).default(['_id','number','type','batch','material_id','color','condition','note_id','user_id','added']) - .messages({'string.pattern.base': 'Invalid field name'}), - filters: Joi.array().items(Joi.object({ - mode: Joi.string().valid('eq', 'ne', 'lt', 'lte', 'gt', 'gte', 'in', 'nin', 'stringin'), - field: Joi.string().pattern( - new RegExp('^(' + this.fieldKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')$', 'm') - ).messages({'string.pattern.base': 'Invalid filter field name'}), - values: Joi.array().items(Joi.alternatives().try( - Joi.string().max(128).allow(''), Joi.number(), Joi.boolean(), Joi.date().iso(), Joi.object(), null - )).min(1) - })).default([]) - }).with('to-page', 'page-size').validate(data); - } -} \ No newline at end of file + static query (data, dev = false) { + if (data.filters && data.filters.length) { + const filterValidation = Joi.array().items(Joi.string()).validate(data.filters); + if (filterValidation.error) return filterValidation; + try { + for (let i in data.filters) { + try { + data.filters[i] = decodeURIComponent(data.filters[i]); + } + catch (ignore) {} + data.filters[i] = JSON.parse(data.filters[i]); + data.filters[i].values = data.filters[i].values.map(e => { // validate filter values + if (e === null) { // null values are always allowed + return null; + } + let validator; + let field = data.filters[i].field; + if (/material\./.test(field)) { // select right validation model + validator = MaterialValidate.outputV().append({ + number: Joi.string().max(128).allow(''), + properties: Joi.alternatives().try(Joi.number(), Joi.string().max(128).allow('')) + }); + field = field.replace('material.', '').split('.')[0]; + } + else if (/measurements\./.test(field) || /condition\./.test(field)) { + validator = Joi.object({ + value: Joi.alternatives() + .try( + Joi.number(), + Joi.string().max(128).allow(''), + Joi.boolean(), + Joi.array() + ) + .allow(null) + }); + field = 'value'; + } + else if (field === 'measurements') { + validator = Joi.object({ + value: Joi.object({}).allow(null).disallow({}) + }); + field = 'value'; + } + else if (field === 'notes.comment') { + field = 'comment'; + validator = this.sample.notes + } + else { + validator = Joi.object(this.sample); + } + const {value, error} = validator.validate({[field]: e}); + if (error) throw error; // reject invalid values + return value[field]; + }); + } + } + catch (err) { + return {error: {details: [{message: 'Invalid JSON string for filter parameter'}]}, value: null} + } + } + const acceptedStatuses = [globals.status.val, globals.status.new]; + if (dev) { // dev and admin can also access deleted samples + acceptedStatuses.push(globals.status.del) + } + return Joi.object({ + status: Joi.array().items(Joi.string().valid(...acceptedStatuses)).default([globals.status.val]), + 'from-id': IdValidate.get(), + 'to-page': Joi.number().integer(), + 'page-size': Joi.number().integer().min(1), + sort: Joi.string().pattern( + new RegExp('^(' + this.sortKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')-(asc|desc)$', 'm') + ).default('_id-asc'), + output: Joi.string().valid('json', 'flatten', 'csv').default('json'), + fields: Joi.array().items(Joi.string().pattern( + new RegExp('^(' + this.fieldKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')$', 'm') + )).default(['_id','number','type','batch','material_id','color','condition','note_id','user_id','added']) + .messages({'string.pattern.base': 'Invalid field name'}), + filters: Joi.array().items(Joi.object({ + mode: Joi.string().valid('eq', 'ne', 'lt', 'lte', 'gt', 'gte', 'in', 'nin', 'stringin'), + field: Joi.string().pattern( + new RegExp('^(' + this.fieldKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')$', 'm') + ).messages({'string.pattern.base': 'Invalid filter field name'}), + values: Joi.array().items(Joi.alternatives().try( + Joi.string().max(128).allow(''), Joi.number(), Joi.boolean(), Joi.date().iso(), Joi.object(), null + )).min(1) + })).default([]) + }).with('to-page', 'page-size').validate(data); + } +} diff --git a/src/routes/validate/template.ts b/src/routes/validate/template.ts index 9de7474..ab84661 100644 --- a/src/routes/validate/template.ts +++ b/src/routes/validate/template.ts @@ -2,70 +2,70 @@ import Joi from 'joi'; import IdValidate from './id'; export default class TemplateValidate { - private static template = { - name: Joi.string() - .max(128), + private static template = { + name: Joi.string() + .max(128), - version: Joi.number() - .min(1), + version: Joi.number() + .min(1), - parameters: Joi.array() - .items( - Joi.object({ - name: Joi.string() - .max(128) - .invalid('condition_template', 'material_template') - .pattern(/^[^.]+$/) - .required() - .messages({'string.pattern.base': 'name must not contain a dot'}), + parameters: Joi.array() + .items( + Joi.object({ + name: Joi.string() + .max(128) + .invalid('condition_template', 'material_template') + .pattern(/^[^.]+$/) + .required() + .messages({'string.pattern.base': 'name must not contain a dot'}), - range: Joi.object({ - values: Joi.array() - .min(1), + range: Joi.object({ + values: Joi.array() + .min(1), - min: Joi.number(), + min: Joi.number(), - max: Joi.number(), + max: Joi.number(), - type: Joi.string() - .valid('string', 'number', 'boolean', 'array'), + type: Joi.string() + .valid('string', 'number', 'boolean', 'array'), - required: Joi.boolean() - }) - .oxor('values', 'min') - .oxor('values', 'max') - .required() - }) - ) - }; + required: Joi.boolean() + }) + .oxor('values', 'min') + .oxor('values', 'max') + .required() + }) + ) + }; - static input (data, param) { // validate input, set param to 'new' to make all attributes required - if (param === 'new') { - return Joi.object({ - name: this.template.name.required(), - parameters: this.template.parameters.required() - }).validate(data); - } - else if (param === 'change') { - return Joi.object({ - name: this.template.name, - parameters: this.template.parameters - }).validate(data); - } - else { - return{error: 'No parameter specified!', value: {}}; - } - } + static input (data, param) { // validate input, set param to 'new' to make all attributes required + if (param === 'new') { + return Joi.object({ + name: this.template.name.required(), + parameters: this.template.parameters.required() + }).validate(data); + } + else if (param === 'change') { + return Joi.object({ + name: this.template.name, + parameters: this.template.parameters + }).validate(data); + } + else { + return{error: 'No parameter specified!', value: {}}; + } + } - 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(), - name: this.template.name, - version: this.template.version, - first_id: IdValidate.get(), - parameters: this.template.parameters - }).validate(data, {stripUnknown: true}); - return error !== undefined? null : value; - } -} \ No newline at end of file + 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(), + name: this.template.name, + version: this.template.version, + first_id: IdValidate.get(), + parameters: this.template.parameters + }).validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } +} diff --git a/src/routes/validate/user.ts b/src/routes/validate/user.ts index c9288d7..6ada465 100644 --- a/src/routes/validate/user.ts +++ b/src/routes/validate/user.ts @@ -4,104 +4,104 @@ import globals from '../../globals'; import IdValidate from './id'; export default class UserValidate { // validate input for user - private static user = { - name: Joi.string() - .lowercase() - .pattern(new RegExp('^[a-z0-9-_.]+$')) - .max(128) - .messages({'string.pattern.base': 'name must only contain a-z0-9_.'}), + private static user = { + name: Joi.string() + .lowercase() + .pattern(new RegExp('^[a-z0-9-_.]+$')) + .max(128) + .messages({'string.pattern.base': 'name must only contain a-z0-9_.'}), - email: Joi.string() - .email({minDomainSegments: 2}) - .lowercase() - .max(128), + email: Joi.string() + .email({minDomainSegments: 2}) + .lowercase() + .max(128), - pass: Joi.string() - .min(8) - .max(128), + pass: Joi.string() + .min(8) + .max(128), - level: Joi.string() - .valid(...Object.values(globals.levels)), + level: Joi.string() + .valid(...Object.values(globals.levels)), - location: Joi.string() - .alphanum() - .max(128), + location: Joi.string() + .alphanum() + .max(128), - devices: Joi.array() - .items(Joi.string() - .allow('') - .max(128) - ), + devices: Joi.array() + .items(Joi.string() + .allow('') + .max(128) + ), - models: Joi.array() - .items(IdValidate.get()), + models: Joi.array() + .items(IdValidate.get()), - status: Joi.string() - .valid(...Object.values(globals.status)) - }; + status: Joi.string() + .valid(...Object.values(globals.status)) + }; - private static specialUsernames: string[] = ['admin', 'user', 'key', 'new', 'passreset']; // names a user cannot take + private static specialUsernames: string[] = ['admin', 'user', 'key', 'new', 'passreset']; // names a user cannot take - 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(), - email: this.user.email.required(), - pass: this.user.pass.required(), - level: this.user.level.required(), - location: this.user.location.required(), - devices: this.user.devices.required(), - models: this.user.models.required() - }).validate(data); - } - else if (param === 'change') { - return Joi.object({ - name: this.user.name, - email: this.user.email, - pass: this.user.pass, - location: this.user.location, - devices: this.user.devices - }).validate(data); - } - else if (param === 'changeadmin') { - return Joi.object({ - name: this.user.name, - email: this.user.email, - pass: this.user.pass, - level: this.user.level, - location: this.user.location, - devices: this.user.devices, - models: this.user.models - }).validate(data); - } - else { - return {error: 'No parameter specified!', value: {}}; - } - } + 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(), + email: this.user.email.required(), + pass: this.user.pass.required(), + level: this.user.level.required(), + location: this.user.location.required(), + devices: this.user.devices.required(), + models: this.user.models.required() + }).validate(data); + } + else if (param === 'change') { + return Joi.object({ + name: this.user.name, + email: this.user.email, + pass: this.user.pass, + location: this.user.location, + devices: this.user.devices + }).validate(data); + } + else if (param === 'changeadmin') { + return Joi.object({ + name: this.user.name, + email: this.user.email, + pass: this.user.pass, + level: this.user.level, + location: this.user.location, + devices: this.user.devices, + models: this.user.models + }).validate(data); + } + else { + return {error: 'No parameter specified!', value: {}}; + } + } - static output (data, param = '') { // validate output and strip unwanted properties, returns null if not valid - data = IdValidate.stringify(data); - const validate: {[key: string]: object} = { - _id: IdValidate.get(), - name: this.user.name, - email: this.user.email, - level: this.user.level, - location: this.user.location, - devices: this.user.devices, - models: this.user.models - } - if (param === 'admin') { - validate.status = this.user.status; - } - const {value, error} = Joi.object(validate).validate(data, {stripUnknown: true}); - return error !== undefined? null : value; - } + static output (data, param = '') { // validate output and strip unwanted properties, returns null if not valid + data = IdValidate.stringify(data); + const validate: {[key: string]: object} = { + _id: IdValidate.get(), + name: this.user.name, + email: this.user.email, + level: this.user.level, + location: this.user.location, + devices: this.user.devices, + models: this.user.models + } + if (param === 'admin') { + validate.status = this.user.status; + } + const {value, error} = Joi.object(validate).validate(data, {stripUnknown: true}); + return error !== undefined? null : value; + } - static isSpecialName (name) { // true if name belongs to special names - return this.specialUsernames.indexOf(name) > -1; - } + static isSpecialName (name) { // true if name belongs to special names + return this.specialUsernames.indexOf(name) > -1; + } - static username() { - return this.user.name; - } + static username() { + return this.user.name; + } } diff --git a/src/test/helper.ts b/src/test/helper.ts index 70dd638..1ef25d0 100644 --- a/src/test/helper.ts +++ b/src/test/helper.ts @@ -7,141 +7,141 @@ import IdValidate from '../routes/validate/id'; export default class TestHelper { - public static auth = { // test user credentials - admin: {pass: 'Abc123!#', key: '000000000000000000001003', id: '000000000000000000000003'}, - janedoe: {pass: 'Xyz890*)', key: '000000000000000000001002', id: '000000000000000000000002'}, - user: {pass: 'Xyz890*)', key: '000000000000000000001001', id: '000000000000000000000001'}, - johnnydoe: {pass: 'Xyz890*)', key: '000000000000000000001004', id: '000000000000000000000004'}, - customer: {pass: 'Xyz890*)', key: '000000000000000000001005', id: '000000000000000000000005'} - } + public static auth = { // test user credentials + admin: {pass: 'Abc123!#', key: '000000000000000000001003', id: '000000000000000000000003'}, + janedoe: {pass: 'Xyz890*)', key: '000000000000000000001002', id: '000000000000000000000002'}, + user: {pass: 'Xyz890*)', key: '000000000000000000001001', id: '000000000000000000000001'}, + johnnydoe: {pass: 'Xyz890*)', key: '000000000000000000001004', id: '000000000000000000000004'}, + customer: {pass: 'Xyz890*)', key: '000000000000000000001005', id: '000000000000000000000005'} + } - public static res = { // default responses - 400: {status: 'Bad request'}, - 401: {status: 'Unauthorized'}, - 403: {status: 'Forbidden'}, - 404: {status: 'Not found'}, - 500: {status: 'Internal server error'} - } + public static res = { // default responses + 400: {status: 'Bad request'}, + 401: {status: 'Unauthorized'}, + 403: {status: 'Forbidden'}, + 404: {status: 'Not found'}, + 500: {status: 'Internal server error'} + } - static before (done) { - process.env.port = '2999'; - process.env.NODE_ENV = 'test'; - db.connect('test', done); - } + static before (done) { + process.env.port = '2999'; + process.env.NODE_ENV = 'test'; + db.connect('test', done); + } - static beforeEach (server, done) { - // delete cached server code except models as these are needed in the testing files as well - Object.keys(require.cache).filter(e => /API\\dist\\(?!(models|db|test))/.test(e)).forEach(key => { - delete require.cache[key]; // prevent loading from cache - }); - server = require('../index'); - db.drop(err => { // reset database - if (err) return done(err); - db.loadJson(require('./db.json'), done); - }); - return server - } + static beforeEach (server, done) { + // delete cached server code except models as these are needed in the testing files as well + Object.keys(require.cache).filter(e => /API\\dist\\(?!(models|db|test))/.test(e)).forEach(key => { + delete require.cache[key]; // prevent loading from cache + }); + server = require('../index'); + db.drop(err => { // reset database + if (err) return done(err); + db.loadJson(require('./db.json'), done); + }); + return server + } - // options in form: {method, url, contentType, auth: {key/basic: 'name' or 'key'/{name, pass}}, httpStatus, req, res, - // default (set to false if you want to dismiss default .end handling)} - static request (server, done, options) { - let st = supertest(server); - 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) { // http method - case 'get': - st = st.get(options.url) - break; - case 'post': - st = st.post(options.url) - break; - case 'put': - st = st.put(options.url) - break; - case 'delete': - st = st.delete(options.url) - break; - } - if (options.hasOwnProperty('reqType')) { // request body - st = st.type(options.reqType); - } - if (options.hasOwnProperty('req')) { // request body - st = st.send(options.req); - } - if (options.hasOwnProperty('reqContentType')) { // request body - st = st.set('Content-Type', options.reqContentType); - } - 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) - } - else { - st = st.auth(options.auth.basic.name, options.auth.basic.pass) - } - } - if (options.hasOwnProperty('contentType')) { - st = st.expect('Content-type', options.contentType).expect(options.httpStatus); - } - else { - st = st.expect('Content-type', /json/).expect(options.httpStatus); - } - 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) { // evaluate default results - return st.end((err, res) => { - if (err) return done (err); - should(res.body).be.eql(this.res[options.httpStatus]); - done(); - }); - } - // check changelog, takes log: {collection, skip, data/(dataAdd, dataIgn)} - else if (options.hasOwnProperty('log')) { - return st.end(err => { - if (err) return done (err); - ChangelogModel.findOne({}).sort({_id: -1}).skip(options.log.skip? options.log.skip : 0) - .lean().exec((err, data) => { // latest entry - if (err) return done(err); - should(data).have.only.keys('_id', 'action', 'collection_name', 'conditions', 'data', 'user_id', '__v'); - should(data).have.property('action', options.method.toUpperCase() + ' ' + options.url); - should(data).have.property('collection_name', options.log.collection); - if (options.log.hasOwnProperty('data')) { - should(data).have.property('data', options.log.data); - } - else { - const ignore = ['_id', '__v']; - if (options.log.hasOwnProperty('dataIgn')) { - ignore.push(...options.log.dataIgn); - } - let tmp = options.req ? options.req : {}; - if (options.log.hasOwnProperty('dataAdd')) { - _.assign(tmp, options.log.dataAdd) - } - should(IdValidate.stringify(_.omit(data.data, ignore))).be.eql(_.omit(tmp, ignore)); - } - if (data.user_id) { - should(data.user_id.toString()).be.eql(this.auth[options.auth.basic].id); - } - done(); - }); - }); - } - else { // return object to do .end() manually - return st; - } - } + // options in form: {method, url, contentType, auth: {key/basic: 'name' or 'key'/{name, pass}}, httpStatus, req, res, + // default (set to false if you want to dismiss default .end handling)} + static request (server, done, options) { + let st = supertest(server); + 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) { // http method + case 'get': + st = st.get(options.url) + break; + case 'post': + st = st.post(options.url) + break; + case 'put': + st = st.put(options.url) + break; + case 'delete': + st = st.delete(options.url) + break; + } + if (options.hasOwnProperty('reqType')) { // request body + st = st.type(options.reqType); + } + if (options.hasOwnProperty('req')) { // request body + st = st.send(options.req); + } + if (options.hasOwnProperty('reqContentType')) { // request body + st = st.set('Content-Type', options.reqContentType); + } + 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) + } + else { + st = st.auth(options.auth.basic.name, options.auth.basic.pass) + } + } + if (options.hasOwnProperty('contentType')) { + st = st.expect('Content-type', options.contentType).expect(options.httpStatus); + } + else { + st = st.expect('Content-type', /json/).expect(options.httpStatus); + } + 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) { // evaluate default results + return st.end((err, res) => { + if (err) return done (err); + should(res.body).be.eql(this.res[options.httpStatus]); + done(); + }); + } + // check changelog, takes log: {collection, skip, data/(dataAdd, dataIgn)} + else if (options.hasOwnProperty('log')) { + return st.end(err => { + if (err) return done (err); + ChangelogModel.findOne({}).sort({_id: -1}).skip(options.log.skip? options.log.skip : 0) + .lean().exec((err, data) => { // latest entry + if (err) return done(err); + should(data).have.only.keys('_id', 'action', 'collection_name', 'conditions', 'data', 'user_id', '__v'); + should(data).have.property('action', options.method.toUpperCase() + ' ' + options.url); + should(data).have.property('collection_name', options.log.collection); + if (options.log.hasOwnProperty('data')) { + should(data).have.property('data', options.log.data); + } + else { + const ignore = ['_id', '__v']; + if (options.log.hasOwnProperty('dataIgn')) { + ignore.push(...options.log.dataIgn); + } + let tmp = options.req ? options.req : {}; + if (options.log.hasOwnProperty('dataAdd')) { + _.assign(tmp, options.log.dataAdd) + } + should(IdValidate.stringify(_.omit(data.data, ignore))).be.eql(_.omit(tmp, ignore)); + } + if (data.user_id) { + should(data.user_id.toString()).be.eql(this.auth[options.auth.basic].id); + } + done(); + }); + }); + } + else { // return object to do .end() manually + return st; + } + } - static afterEach (server, done) { - server.close(done); - } + static afterEach (server, done) { + server.close(done); + } - static after(done) { - db.disconnect(done); - } -} \ No newline at end of file + static after(done) { + db.disconnect(done); + } +} diff --git a/src/test/loadDev.ts b/src/test/loadDev.ts index 15a6868..40bd5c5 100644 --- a/src/test/loadDev.ts +++ b/src/test/loadDev.ts @@ -3,12 +3,12 @@ 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 - console.info('loading data...'); - db.loadJson(require('./db.json'), () => { - console.info('done'); - process.exit(0); - }); - }); + console.info('dropping data...'); + db.drop(() => { // reset database + console.info('loading data...'); + db.loadJson(require('./db.json'), () => { + console.info('done'); + process.exit(0); + }); + }); });