From 0acb9dd6fce4784a7720c07458d0c9937eba965c Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Wed, 27 May 2020 14:31:17 +0200 Subject: [PATCH 1/4] adapted existing /sample methods to condition, removed /condition route --- .idea/dictionaries/VLE2FE.xml | 2 + .idea/libraries/dist.xml | 13 - api/api.yaml | 1 - api/condition.yaml | 111 -- api/sample.yaml | 4 +- api/schemas.yaml | 39 +- api/template.yaml | 34 +- package-lock.json | 976 ++++++++++++++++++ package.json | 5 +- src/globals.ts | 8 +- src/index.ts | 21 +- src/models/condition.ts | 13 - ...ment_template.ts => condition_template.ts} | 5 +- src/models/measurement.ts | 6 +- src/models/note.ts | 2 +- src/models/sample.ts | 3 +- src/routes/condition.spec.ts | 583 ----------- src/routes/condition.ts | 133 --- src/routes/material.spec.ts | 21 +- src/routes/material.ts | 18 +- src/routes/measurement.spec.ts | 9 +- src/routes/measurement.ts | 20 +- src/routes/sample.spec.ts | 436 ++++++-- src/routes/sample.ts | 90 +- src/routes/template.spec.ts | 197 ++-- src/routes/template.ts | 46 +- src/routes/user.ts | 3 - src/routes/validate/condition.ts | 50 - src/routes/validate/material.ts | 41 +- src/routes/validate/sample.ts | 54 +- src/routes/validate/template.ts | 68 +- src/routes/validate/user.ts | 4 + src/test/db.json | 110 +- src/test/helper.ts | 1 + 34 files changed, 1753 insertions(+), 1374 deletions(-) delete mode 100644 .idea/libraries/dist.xml delete mode 100644 api/condition.yaml delete mode 100644 src/models/condition.ts rename src/models/{treatment_template.ts => condition_template.ts} (59%) delete mode 100644 src/routes/condition.spec.ts delete mode 100644 src/routes/condition.ts delete mode 100644 src/routes/validate/condition.ts diff --git a/.idea/dictionaries/VLE2FE.xml b/.idea/dictionaries/VLE2FE.xml index c274b8b..1dd7309 100644 --- a/.idea/dictionaries/VLE2FE.xml +++ b/.idea/dictionaries/VLE2FE.xml @@ -4,6 +4,8 @@ bcrypt cfenv dfopdb + janedoe + testcomment \ No newline at end of file diff --git a/.idea/libraries/dist.xml b/.idea/libraries/dist.xml deleted file mode 100644 index 3d92275..0000000 --- a/.idea/libraries/dist.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/api/api.yaml b/api/api.yaml index f890477..c0a5441 100644 --- a/api/api.yaml +++ b/api/api.yaml @@ -66,7 +66,6 @@ paths: - $ref: 'others.yaml' - $ref: 'sample.yaml' - $ref: 'material.yaml' - - $ref: 'condition.yaml' - $ref: 'measurement.yaml' - $ref: 'template.yaml' - $ref: 'model.yaml' diff --git a/api/condition.yaml b/api/condition.yaml deleted file mode 100644 index ec8b245..0000000 --- a/api/condition.yaml +++ /dev/null @@ -1,111 +0,0 @@ -/condition/{id}: - parameters: - - $ref: 'api.yaml#/components/parameters/Id' - get: - summary: condition by id - description: 'Auth: all, levels: read, write, maintain, dev, admin' - x-doc: status handling (accessible (only for maintain/admin))? # TODO after decision - tags: - - /condition - responses: - 200: - description: condition details - content: - application/json: - schema: - $ref: 'api.yaml#/components/schemas/Condition' - 400: - $ref: 'api.yaml#/components/responses/400' - 401: - $ref: 'api.yaml#/components/responses/401' - 404: - $ref: 'api.yaml#/components/responses/404' - 500: - $ref: 'api.yaml#/components/responses/500' - put: - summary: change condition - description: 'Auth: basic, levels: write, maintain, dev, admin
Only maintain and admin are allowed to reference samples created by another user' - x-doc: status is reset to 0 on any changes - tags: - - /condition - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - allOf: - - $ref: 'api.yaml#/components/schemas/_Id' - properties: - parameters: - type: object - responses: - 200: - description: condition details - content: - application/json: - schema: - $ref: 'api.yaml#/components/schemas/Condition' - 400: - $ref: 'api.yaml#/components/responses/400' - 401: - $ref: 'api.yaml#/components/responses/401' - 403: - $ref: 'api.yaml#/components/responses/403' - 404: - $ref: 'api.yaml#/components/responses/404' - 500: - $ref: 'api.yaml#/components/responses/500' - delete: - summary: delete condition - description: 'Auth: basic, levels: write, maintain, dev, admin' - x-doc: sets status to -1 - tags: - - /condition - security: - - BasicAuth: [] - responses: - 200: - $ref: 'api.yaml#/components/responses/Ok' - 400: - $ref: 'api.yaml#/components/responses/400' - 401: - $ref: 'api.yaml#/components/responses/401' - 403: - $ref: 'api.yaml#/components/responses/403' - 404: - $ref: 'api.yaml#/components/responses/404' - 500: - $ref: 'api.yaml#/components/responses/500' - -/condition/new: - post: - summary: add condition - description: 'Auth: basic, levels: write, maintain, dev, admin
Only maintain and admin are allowed to reference samples created by another user' - x-doc: 'Adds status: 0 automatically' - tags: - - /condition - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: 'api.yaml#/components/schemas/Condition' - responses: - 200: - description: condition details - content: - application/json: - schema: - $ref: 'api.yaml#/components/schemas/Condition' - 400: - $ref: 'api.yaml#/components/responses/400' - 401: - $ref: 'api.yaml#/components/responses/401' - 403: - $ref: 'api.yaml#/components/responses/403' - 500: - $ref: 'api.yaml#/components/responses/500' \ No newline at end of file diff --git a/api/sample.yaml b/api/sample.yaml index c699809..9e830ff 100644 --- a/api/sample.yaml +++ b/api/sample.yaml @@ -19,7 +19,7 @@ 500: $ref: 'api.yaml#/components/responses/500' -/samples{group}: +/samples/{group}: parameters: - $ref: 'api.yaml#/components/parameters/Group' get: @@ -48,7 +48,7 @@ get: summary: TODO sample details description: 'Auth: all, levels: read, write, maintain, dev, admin' - x-doc: status handling (accessible (only for maintain/admin))? # TODO after decision + x-doc: deleted samples are available only for maintain/admin tags: - /sample responses: diff --git a/api/schemas.yaml b/api/schemas.yaml index c872443..6e1eeb7 100644 --- a/api/schemas.yaml +++ b/api/schemas.yaml @@ -24,6 +24,15 @@ SampleProperties: batch: type: string example: 1560237365 + condition: + type: object + properties: + condition_template: + $ref: 'api.yaml#/components/schemas/Id' + example: + condition_template: 5ea0450ed851c30a90e70894 + material: hot air + weeks: 5 SampleRefs: allOf: @@ -55,7 +64,7 @@ Sample: type: array items: properties: - id: + sample_id: $ref: 'api.yaml#/components/schemas/Id' relation: type: string @@ -67,7 +76,8 @@ SampleDetail: - $ref: 'api.yaml#/components/schemas/SampleProperties' properties: material: - $ref: 'api.yaml#/components/schemas/Material' + allOf: + - $ref: 'api.yaml#/components/schemas/Material' notes: type: object properties: @@ -77,10 +87,14 @@ SampleDetail: type: array items: $ref: 'api.yaml#/components/schemas/Id' - conditions: + measurements: type: array items: - $ref: 'api.yaml#/components/schemas/Condition' + allOf: + - $ref: 'api.yaml#/components/schemas/Measurement' + user: + type: string + example: admin Material: allOf: @@ -115,21 +129,6 @@ Material: type: string example: 5514263423 -Condition: - allOf: - - $ref: 'api.yaml#/components/schemas/_Id' - properties: - sample_id: - $ref: 'api.yaml#/components/schemas/Id' - number: - type: string - readOnly: true - example: B1 - parameters: - type: object - treatment_template: - $ref: 'api.yaml#/components/schemas/Id' - Measurement: allOf: - $ref: 'api.yaml#/components/schemas/_Id' @@ -166,7 +165,7 @@ Template: min: 0 max: 2 -TreatmentTemplate: +ConditionTemplate: allOf: - $ref: 'api.yaml#/components/schemas/Template' properties: diff --git a/api/template.yaml b/api/template.yaml index 37f374a..71a282f 100644 --- a/api/template.yaml +++ b/api/template.yaml @@ -1,6 +1,6 @@ -/template/treatments: +/template/conditions: get: - summary: all available treatment methods + summary: all available condition methods description: 'Auth: basic, levels: read, write, maintain, dev, admin' tags: - /template @@ -8,23 +8,23 @@ - BasicAuth: [] responses: 200: - description: list of treatments + description: list of conditions content: application/json: schema: type: array items: - $ref: 'api.yaml#/components/schemas/TreatmentTemplate' + $ref: 'api.yaml#/components/schemas/ConditionTemplate' 401: $ref: 'api.yaml#/components/responses/401' 500: $ref: 'api.yaml#/components/responses/500' -/template/treatment/{id}: +/template/condition/{id}: parameters: - $ref: 'api.yaml#/components/parameters/Id' get: - summary: treatment method details + summary: condition method details description: 'Auth: basic, levels: read, write, maintain, admin' tags: - /template @@ -32,11 +32,11 @@ - BasicAuth: [] responses: 200: - description: treatment details + description: condition details content: application/json: schema: - $ref: 'api.yaml#/components/schemas/TreatmentTemplate' + $ref: 'api.yaml#/components/schemas/ConditionTemplate' 401: $ref: 'api.yaml#/components/responses/401' 404: @@ -44,7 +44,7 @@ 500: $ref: 'api.yaml#/components/responses/500' put: - summary: change treatment method + summary: change condition method description: 'Auth: basic, levels: maintain, admin' x-doc: With a change a new version is set, resulting in a new template with a new id tags: @@ -56,14 +56,14 @@ content: application/json: schema: - $ref: 'api.yaml#/components/schemas/TreatmentTemplate' + $ref: 'api.yaml#/components/schemas/ConditionTemplate' responses: 200: - description: treatment details + description: condition details content: application/json: schema: - $ref: 'api.yaml#/components/schemas/TreatmentTemplate' + $ref: 'api.yaml#/components/schemas/ConditionTemplate' 400: $ref: 'api.yaml#/components/responses/400' 401: @@ -75,9 +75,9 @@ 500: $ref: 'api.yaml#/components/responses/500' -/template/treatment/new: +/template/condition/new: post: - summary: add treatment method + summary: add condition method description: 'Auth: basic, levels: maintain, admin' tags: - /template @@ -88,14 +88,14 @@ content: application/json: schema: - $ref: 'api.yaml#/components/schemas/TreatmentTemplate' + $ref: 'api.yaml#/components/schemas/ConditionTemplate' responses: 200: - description: treatment details + description: condition details content: application/json: schema: - $ref: 'api.yaml#/components/schemas/TreatmentTemplate' + $ref: 'api.yaml#/components/schemas/ConditionTemplate' 400: $ref: 'api.yaml#/components/responses/400' 401: diff --git a/package-lock.json b/package-lock.json index 4c3c77d..6d935ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,169 @@ "@babel/highlight": "^7.8.3" } }, + "@babel/core": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.6.tgz", + "integrity": "sha512-nD3deLvbsApbHAHttzIssYqgb883yU/d9roe4RZymBCDaZryMJDbptVpEpeQuRh4BJ+SYI8le9YGxKvFEvl1Wg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.6", + "@babel/helper-module-transforms": "^7.9.0", + "@babel/helpers": "^7.9.6", + "@babel/parser": "^7.9.6", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.9.6", + "@babel/types": "^7.9.6", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.6.tgz", + "integrity": "sha512-+htwWKJbH2bL72HRluF8zumBxzuX0ZZUFl3JLNyoUjM/Ho8wnVpPXM6aUz8cfKDqQ/h7zHqKt4xzJteUosckqQ==", + "dev": true, + "requires": { + "@babel/types": "^7.9.6", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", + "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.9.5" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz", + "integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-module-imports": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", + "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-module-transforms": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz", + "integrity": "sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.8.3", + "@babel/helper-replace-supers": "^7.8.6", + "@babel/helper-simple-access": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/template": "^7.8.6", + "@babel/types": "^7.9.0", + "lodash": "^4.17.13" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz", + "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-replace-supers": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.9.6.tgz", + "integrity": "sha512-qX+chbxkbArLyCImk3bWV+jB5gTNU/rsze+JlcF6Nf8tVTigPJSI1o1oBow/9Resa1yehUO9lIipsmu9oG4RzA==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.8.3", + "@babel/helper-optimise-call-expression": "^7.8.3", + "@babel/traverse": "^7.9.6", + "@babel/types": "^7.9.6" + } + }, + "@babel/helper-simple-access": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz", + "integrity": "sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==", + "dev": true, + "requires": { + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", + "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==", + "dev": true + }, + "@babel/helpers": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.9.6.tgz", + "integrity": "sha512-tI4bUbldloLcHWoRUMAj4g1bF313M/o6fBKhIsb3QnGVPwRm9JsNf/gqMkQ7zjqReABiffPV6RWj7hEglID5Iw==", + "dev": true, + "requires": { + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.9.6", + "@babel/types": "^7.9.6" + } + }, "@babel/highlight": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", @@ -56,6 +219,68 @@ "js-tokens": "^4.0.0" } }, + "@babel/parser": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.6.tgz", + "integrity": "sha512-AoeIEJn8vt+d/6+PXDRPaksYhnlbMIiejioBZvvMQsOjW/JYK6k/0dKnvvP3EhK5GfMBWDPtrxRtegWdAcdq9Q==", + "dev": true + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/traverse": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.6.tgz", + "integrity": "sha512-b3rAHSjbxy6VEAvlxM8OV/0X4XrG72zoxme6q1MOoe2vd0bEc+TwayhuC1+Dfgqh1QEG+pj7atQqvUprHIccsg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.6", + "@babel/helper-function-name": "^7.9.5", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.6.tgz", + "integrity": "sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.9.5", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, "@hapi/address": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-4.0.1.tgz", @@ -99,6 +324,67 @@ "@hapi/hoek": "^9.0.0" } }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true + }, "@jsdevtools/ono": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.2.tgz", @@ -233,6 +519,16 @@ "negotiator": "0.6.2" } }, + "aggregate-error": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", + "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, "ansi-align": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", @@ -295,6 +591,21 @@ "picomatch": "^2.0.4" } }, + "append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "requires": { + "default-require-extensions": "^3.0.0" + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -528,6 +839,18 @@ } } }, + "caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "requires": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + } + }, "call-me-maybe": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", @@ -578,6 +901,12 @@ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, "cli-boxes": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.0.tgz", @@ -657,6 +986,12 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -699,6 +1034,15 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, "cookie": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", @@ -721,6 +1065,28 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -753,6 +1119,15 @@ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, + "default-require-extensions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", + "dev": true, + "requires": { + "strip-bom": "^4.0.0" + } + }, "defer-to-connect": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", @@ -854,6 +1229,12 @@ "is-symbol": "^1.0.2" } }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, "escape-goat": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", @@ -949,6 +1330,17 @@ "unpipe": "~1.0.0" } }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, "find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", @@ -985,6 +1377,16 @@ } } }, + "foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + } + }, "form-data": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", @@ -1012,6 +1414,12 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, + "fromentries": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.0.tgz", + "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==", + "dev": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1029,12 +1437,24 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "gensync": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "dev": true + }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -1072,6 +1492,12 @@ "ini": "^1.3.5" } }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, "got": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", @@ -1126,12 +1552,28 @@ "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==" }, + "hasha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz", + "integrity": "sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw==", + "dev": true, + "requires": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -1172,6 +1614,12 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1286,6 +1734,12 @@ "has": "^1.0.3" } }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, "is-symbol": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", @@ -1300,6 +1754,12 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, "is-yarn-global": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", @@ -1317,6 +1777,128 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "requires": { + "append-transform": "^2.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-processinfo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.0", + "istanbul-lib-coverage": "^3.0.0-alpha.1", + "make-dir": "^3.0.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^3.3.3" + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1331,6 +1913,12 @@ "esprima": "^4.0.0" } }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, "json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", @@ -1341,6 +1929,15 @@ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.5.tgz", "integrity": "sha512-gWJOWYFrhQ8j7pVm0EM8Slr+EPVq1Phf6lvzvD/WCeqkrx/f2xBI0xOsRRS9xCn3I4vKtP519dvs3TP09r24wQ==" }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, "kareem": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.1.tgz", @@ -1377,6 +1974,12 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -1677,6 +2280,15 @@ "semver": "^5.7.0" } }, + "node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "requires": { + "process-on-spawn": "^1.0.0" + } + }, "nodemon": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.3.tgz", @@ -1727,6 +2339,196 @@ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" }, + "nyc": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.0.1.tgz", + "integrity": "sha512-n0MBXYBYRqa67IVt62qW1r/d9UH/Qtr7SF1w/nQLJ9KxvWF6b2xCHImRAixHN9tnMMYHC2P14uo6KddNGwMgGg==", + "dev": true, + "requires": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.1" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "object-inspect": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", @@ -1805,12 +2607,33 @@ "p-limit": "^2.0.0" } }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, "package-json": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", @@ -1845,6 +2668,12 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", @@ -1860,6 +2689,51 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz", "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==" }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, "ports": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ports/-/ports-1.1.0.tgz", @@ -1876,6 +2750,15 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "requires": { + "fromentries": "^1.2.0" + } + }, "proxy-addr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", @@ -1983,6 +2866,15 @@ "rc": "^1.2.8" } }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2025,6 +2917,15 @@ "lowercase-keys": "^1.0.0" } }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -2113,6 +3014,21 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, "should": { "version": "13.2.3", "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", @@ -2182,6 +3098,12 @@ "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, "sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -2191,6 +3113,31 @@ "memory-pager": "^1.0.2" } }, + "spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "requires": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -2271,6 +3218,12 @@ "ansi-regex": "^3.0.0" } }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -2347,6 +3300,23 @@ "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==" }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, "to-readable-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", @@ -2544,6 +3514,12 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + }, "validator": { "version": "12.2.0", "resolved": "https://registry.npmjs.org/validator/-/validator-12.2.0.tgz", diff --git a/package.json b/package.json index 4ec763a..5763fdc 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,12 @@ "main": "index.js", "scripts": { "tsc": "tsc", + "tsc-full": "del /q dist\\* & (for /d %x in (dist\\*) do @rd /s /q \"%x\") & tsc", "test": "mocha dist/**/**.spec.js", "start": "tsc && node dist/index.js || exit 1", "dev": "nodemon -e ts,yaml --exec \"npm run start\"", - "loadDev": "node dist/test/loadDev.js" + "loadDev": "node dist/test/loadDev.js", + "coverage": "nyc --reporter=html --reporter=tex mocha dist/**/**.spec.js" }, "keywords": [], "author": "", @@ -44,6 +46,7 @@ "devDependencies": { "@types/lodash": "^4.14.150", "mocha": "^7.1.2", + "nyc": "^15.0.1", "should": "^13.2.3", "supertest": "^4.0.2" } diff --git a/src/globals.ts b/src/globals.ts index 0d4ccdb..81f80b8 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -5,7 +5,13 @@ const globals = { 'maintain', 'dev', 'admin' - ] + ], + + status: { // document statuses + deleted: -1, + new: 0, + validated: 10, + } }; export default globals; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 4ce0581..1343442 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import db from './db'; // TODO: validation: VZ, Humidity: min/max value, DPT: filename // TODO: condition values not needed on initial add // TODO: add multiple samples at once +// TODO: coverage // tell if server is running in debug or production environment console.info(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT ====='); @@ -48,14 +49,20 @@ app.use((req, res, next) => { // no database connection error }); app.use(require('./helpers/authorize')); // handle authentication +// redirect /api routes for Angular proxy in development +app.use('/api/:url', (req, res) => { + req.url = '/' + req.params.url; + app.handle(req, res); +}); + + // require routes -app.use('/api', require('./routes/root')); -app.use('/api', require('./routes/sample')); -app.use('/api', require('./routes/material')); -app.use('/api', require('./routes/template')); -app.use('/api', require('./routes/user')); -app.use('/api', require('./routes/condition')); -app.use('/api', require('./routes/measurement')); +app.use('/', require('./routes/root')); +app.use('/', require('./routes/sample')); +app.use('/', require('./routes/material')); +app.use('/', require('./routes/template')); +app.use('/', require('./routes/user')); +app.use('/', require('./routes/measurement')); // static files app.use('/static', express.static('static')); diff --git a/src/models/condition.ts b/src/models/condition.ts deleted file mode 100644 index e0f79da..0000000 --- a/src/models/condition.ts +++ /dev/null @@ -1,13 +0,0 @@ -import mongoose from 'mongoose'; -import SampleModel from './sample'; -import TreatmentTemplateModel from './treatment_template'; - -const ConditionSchema = new mongoose.Schema({ - sample_id: {type: mongoose.Schema.Types.ObjectId, ref: SampleModel}, - number: String, - parameters: mongoose.Schema.Types.Mixed, - treatment_template: {type: mongoose.Schema.Types.ObjectId, ref: TreatmentTemplateModel}, - status: Number -}); - -export default mongoose.model('condition', ConditionSchema); \ No newline at end of file diff --git a/src/models/treatment_template.ts b/src/models/condition_template.ts similarity index 59% rename from src/models/treatment_template.ts rename to src/models/condition_template.ts index 154ae79..20c7234 100644 --- a/src/models/treatment_template.ts +++ b/src/models/condition_template.ts @@ -1,13 +1,12 @@ import mongoose from 'mongoose'; -const TreatmentTemplateSchema = new mongoose.Schema({ +const ConditionTemplateSchema = new mongoose.Schema({ name: String, version: Number, - number_prefix: String, parameters: [{ name: String, range: mongoose.Schema.Types.Mixed }] }, {minimize: false}); // to allow empty objects -export default mongoose.model('treatment_template', TreatmentTemplateSchema); \ No newline at end of file +export default mongoose.model('condition_template', ConditionTemplateSchema); \ No newline at end of file diff --git a/src/models/measurement.ts b/src/models/measurement.ts index ac0ef20..7db0a50 100644 --- a/src/models/measurement.ts +++ b/src/models/measurement.ts @@ -1,9 +1,11 @@ import mongoose from 'mongoose'; -import ConditionModel from './condition'; +import SampleModel from './sample'; import MeasurementTemplateModel from './measurement_template'; +// TODO: change to sample_id + const MeasurementSchema = new mongoose.Schema({ - condition_id: {type: mongoose.Schema.Types.ObjectId, ref: ConditionModel}, + sample_id: {type: mongoose.Schema.Types.ObjectId, ref: SampleModel}, values: mongoose.Schema.Types.Mixed, measurement_template: {type: mongoose.Schema.Types.ObjectId, ref: MeasurementTemplateModel}, status: Number diff --git a/src/models/note.ts b/src/models/note.ts index a13fd6a..cd0847b 100644 --- a/src/models/note.ts +++ b/src/models/note.ts @@ -3,7 +3,7 @@ import mongoose from 'mongoose'; const NoteSchema = new mongoose.Schema({ comment: String, sample_references: [{ - id: mongoose.Schema.Types.ObjectId, + sample_id: mongoose.Schema.Types.ObjectId, relation: String }], custom_fields: mongoose.Schema.Types.Mixed diff --git a/src/models/sample.ts b/src/models/sample.ts index 9e5353b..1338728 100644 --- a/src/models/sample.ts +++ b/src/models/sample.ts @@ -9,10 +9,11 @@ const SampleSchema = new mongoose.Schema({ 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: Number -}); +}, {minimize: false}); export default mongoose.model('sample', SampleSchema); \ No newline at end of file diff --git a/src/routes/condition.spec.ts b/src/routes/condition.spec.ts deleted file mode 100644 index 90c7c43..0000000 --- a/src/routes/condition.spec.ts +++ /dev/null @@ -1,583 +0,0 @@ -import should from 'should/as-function'; -import ConditionModel from '../models/condition'; -import TestHelper from "../test/helper"; - -// TODO: adding conditions allowed only for m/a -// TODO: deleted data only visible for m/a -// TODO: restore deleted -// TODO: remove number_prefix - -describe('/condition', () => { - let server; - before(done => TestHelper.before(done)); - beforeEach(done => server = TestHelper.beforeEach(server, done)); - afterEach(done => TestHelper.afterEach(server, done)); - - describe('GET /condition/{id}', () => { - it('returns the right condition', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/condition/700000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - res: {_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'A1', parameters: {material: 'copper', weeks: 3}, treatment_template: '200000000000000000000001'} - }); - }); - it('returns the right condition for an API key', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/condition/700000000000000000000001', - auth: {key: 'janedoe'}, - httpStatus: 200, - res: {_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'A1', parameters: {material: 'copper', weeks: 3}, treatment_template: '200000000000000000000001'} - }); - }); - it('rejects an invalid id', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/condition/70000000000t000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 404 - }); - }); - it('rejects an unknown id', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/condition/000000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 404 - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'get', - url: '/condition/700000000000000000000001', - httpStatus: 401 - }); - }); - }); - - describe('PUT /condition{id}', () => { - it('returns the right condition', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/condition/700000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {}, - res: {_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'A1', treatment_template: '200000000000000000000001', parameters: {material: 'copper', weeks: 3}} - }); - }); - it('keeps unchanged properties', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/condition/700000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {parameters: {material: 'copper', weeks: 3}} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'A1', treatment_template: '200000000000000000000001', parameters: {material: 'copper', weeks: 3}}); - ConditionModel.findById('700000000000000000000001').lean().exec((err, data) => { - if (err) return done(err); - should(data).have.property('status', 10); - done(); - }); - }); - }); - it('keeps only one unchanged parameter', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/condition/700000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {parameters: {material: 'copper'}} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'A1', treatment_template: '200000000000000000000001', parameters: {material: 'copper', weeks: 3}}); - ConditionModel.findById('700000000000000000000001').lean().exec((err, data) => { - if (err) return done(err); - should(data).have.property('status', 10); - done(); - }); - }); - }); - it('changes the given properties', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/condition/700000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {parameters: {material: 'hot air', weeks: 10}} - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'A1', treatment_template: '200000000000000000000001', parameters: {material: 'hot air', weeks: 10}}); - ConditionModel.findById('700000000000000000000001').lean().exec((err, data: any) => { - if (err) return done(err); - should(data).have.only.keys('_id', 'sample_id', 'number', 'parameters', 'treatment_template', 'status', '__v'); - should(data.sample_id.toString()).be.eql('400000000000000000000001'); - should(data).have.property('number', 'A1'); - should(data.treatment_template.toString()).be.eql('200000000000000000000001'); - should(data).have.property('status', 0); - should(data).have.property('parameters'); - should(data.parameters).have.property('material', 'hot air'); - should(data.parameters).have.property('weeks', 10); - done(); - }); - }); - }); - it('allows changing only one parameter', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/condition/700000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {parameters: {weeks: 8}}, - res: {_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'A1', treatment_template: '200000000000000000000001', parameters: {material: 'copper', weeks: 8}} - }); - }); - it('rejects changing the condition number', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/condition/700000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {number: 'C2'}, - res: {status: 'Invalid body format', details: '"number" is not allowed'} - }); - }); - it('rejects not specified parameters', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/condition/700000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {parameters: {xx: 13}}, - res: {status: 'Invalid body format', details: '"xx" is not allowed'} - }); - }); - it('rejects a parameter not in the value range', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/condition/700000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {parameters: {material: 'xxx'}}, - res: {status: 'Invalid body format', details: '"material" must be one of [copper, hot air]'} - }); - }); - it('rejects a parameter below minimum range', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/condition/700000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {parameters: {weeks: -10}}, - res: {status: 'Invalid body format', details: '"weeks" must be larger than or equal to 1'} - }); - }); - it('rejects a parameter above maximum range', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/condition/700000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {parameters: {weeks: 11}}, - res: {status: 'Invalid body format', details: '"weeks" must be less than or equal to 10'} - }); - }); - it('rejects a new treatment_template', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/condition/700000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {treatment_template: '200000000000000000000002'}, - res: {status: 'Invalid body format', details: '"treatment_template" is not allowed'} - }); - }); - it('rejects editing a condition for a write user who did not create this condition', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/condition/700000000000000000000003', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {parameters: {weeks: 8}} - }); - }); - it('accepts editing a condition of another user for a maintain/admin user', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/condition/700000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {parameters: {material: 'hot air', weeks: 10}}, - res: {_id: '700000000000000000000001', sample_id: '400000000000000000000001', number: 'A1', treatment_template: '200000000000000000000001', parameters: {material: 'hot air', weeks: 10}} - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/condition/700000000000000000000001', - auth: {key: 'janedoe'}, - httpStatus: 401, - req: {parameters: {material: 'hot air', weeks: 10}} - }); - }); - it('rejects requests from a read user', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/condition/700000000000000000000001', - auth: {basic: 'user'}, - httpStatus: 403, - req: {parameters: {material: 'hot air', weeks: 10}} - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/condition/700000000000000000000001', - httpStatus: 401, - req: {parameters: {material: 'hot air', weeks: 10}} - }); - }); - }); - - describe('DELETE /condition/{id}', () => { - it('sets the status to deleted', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/condition/700000000000000000000004', - auth: {basic: 'janedoe'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - ConditionModel.findById('700000000000000000000004').lean().exec((err, data: any) => { - if (err) return done(err); - should(data).have.only.keys('_id', 'sample_id', 'number', 'parameters', 'treatment_template', 'status', '__v'); - should(data.sample_id.toString()).be.eql('400000000000000000000001'); - should(data).have.property('number', 'A2'); - should(data.treatment_template.toString()).be.eql('200000000000000000000001'); - should(data).have.property('status', -1); - should(data).have.property('parameters'); - should(data.parameters).have.property('material', 'hot air'); - should(data.parameters).have.property('weeks', 5); - done(); - }); - }); - }); - it('rejects deleting a condition referenced by measurements'/*, done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/condition/700000000000000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 200, - res: {status: 'Condition still in use'} - }); - }*/); // TODO after decision - it('rejects an invalid id', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/condition/70000000000w000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 404 - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/condition/700000000000000000000004', - auth: {key: 'janedoe'}, - httpStatus: 401 - }); - }); - it('rejects requests from a read user', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/condition/700000000000000000000004', - auth: {basic: 'user'}, - httpStatus: 403 - }); - }); - it('rejects a write user deleting a condition belonging to a sample of another user', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/condition/700000000000000000000003', - auth: {basic: 'janedoe'}, - httpStatus: 403 - }); - }); - it('accepts an maintain/admin user deleting a condition belonging to a sample of another user', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/condition/700000000000000000000004', - auth: {basic: 'admin'}, - httpStatus: 200 - }).end((err, res) => { - if (err) return done(err); - should(res.body).be.eql({status: 'OK'}); - done(); - }); - }); - it('returns 404 for an unknown id', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/condition/000000000000000000000002', - auth: {basic: 'janedoe'}, - httpStatus: 404 - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'delete', - url: '/condition/700000000000000000000004', - httpStatus: 401 - }); - }); - }); - - describe('POST /condition/new', () => { - it('returns the right condition', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'} - }).end((err, res) => { - if (err) return done(err); - should(res.body).have.only.keys('_id', 'sample_id', 'number', 'parameters', 'treatment_template'); - should(res.body).have.property('_id').be.type('string'); - should(res.body).have.property('sample_id', '400000000000000000000002'); - should(res.body).have.property('number', 'A2'); - should(res.body).have.property('treatment_template', '200000000000000000000001'); - should(res.body).have.property('parameters'); - should(res.body.parameters).have.property('material', 'hot air'); - should(res.body.parameters).have.property('weeks', 10); - done(); - }); - }); - it('stores the condition', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'} - }).end((err, res) => { - if (err) return done(err); - ConditionModel.findById(res.body._id).lean().exec((err, data: any) => { - if (err) return done(err); - should(data).have.only.keys('_id', 'sample_id', 'number', 'parameters', 'treatment_template', 'status', '__v'); - should(data.sample_id.toString()).be.eql('400000000000000000000002'); - should(data).have.property('number', 'A2'); - should(data.treatment_template.toString()).be.eql('200000000000000000000001'); - should(data).have.property('status', 0); - should(data).have.property('parameters'); - should(data.parameters).have.property('material', 'hot air'); - should(data.parameters).have.property('weeks', 10); - done(); - }); - }); - }); - it('stores the first condition as 1', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {sample_id: '400000000000000000000003', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'} - }).end((err, res) => { - if (err) return done(err); - ConditionModel.findById(res.body._id).lean().exec((err, data: any) => { - if (err) return done(err); - should(data).have.only.keys('_id', 'sample_id', 'number', 'parameters', 'treatment_template', 'status', '__v'); - should(data.sample_id.toString()).be.eql('400000000000000000000003'); - should(data).have.property('number', 'A1'); - should(data.treatment_template.toString()).be.eql('200000000000000000000001'); - should(data).have.property('status', 0); - should(data).have.property('parameters'); - should(data.parameters).have.property('material', 'hot air'); - should(data.parameters).have.property('weeks', 10); - done(); - }); - }); - }); - it('rejects an invalid sample id', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '4000000000h0000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'}, - res: {status: 'Invalid body format', details: '"sample_id" with value "4000000000h0000000000002" fails to match the required pattern: /[0-9a-f]{24}/'} - }); - }); - it('rejects a sample id not available', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '000000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'}, - res: {status: 'Sample id not available'} - }); - }); - it('rejects an invalid treatment_template id', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000h00000000001'}, - res: {status: 'Invalid body format', details: '"treatment_template" with value "200000000000h00000000001" fails to match the required pattern: /[0-9a-f]{24}/'} - }); - }); - it('rejects a treatment_template which does not exist', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '000000000000000000000001'}, - res: {status: 'Treatment template not available'} - }); - }); - it('rejects setting a condition number', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000000000000001', number: 'A7', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'}, - res: {status: 'Invalid body format', details: '"number" is not allowed'} - }); - }); - it('rejects not specified parameters', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10, xx: 12}, treatment_template: '200000000000000000000001'}, - res: {status: 'Invalid body format', details: '"xx" is not allowed'} - }); - }); - it('rejects missing parameters', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air'}, treatment_template: '200000000000000000000001'}, - res: {status: 'Invalid body format', details: '"weeks" is required'} - }); - }); - it('rejects a parameter not in the value range', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000000000000002', parameters: {material: 'xxx', weeks: 10}, treatment_template: '200000000000000000000001'}, - res: {status: 'Invalid body format', details: '"material" must be one of [copper, hot air]'} - }); - }); - it('rejects a parameter below minimum range', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: -10}, treatment_template: '200000000000000000000001'}, - res: {status: 'Invalid body format', details: '"weeks" must be larger than or equal to 1'} - }); - }); - it('rejects a parameter above maximum range', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 11}, treatment_template: '200000000000000000000001'}, - res: {status: 'Invalid body format', details: '"weeks" must be less than or equal to 10'} - }); - }); - it('rejects a missing sample id', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'}, - res: {status: 'Invalid body format', details: '"sample_id" is required'} - }); - }); - it('rejects a missing treatment_template', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}}, - res: {status: 'Invalid body format', details: '"treatment_template" is required'} - }); - }); - it('rejects adding a condition to the sample of an other user for a write user', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {basic: 'janedoe'}, - httpStatus: 403, - req: {sample_id: '400000000000000000000003', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'} - }); - }); - it('accepts adding a condition to the sample of an other user for a maintain/admin user', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {basic: 'admin'}, - httpStatus: 200, - req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'} - }).end((err, res) => { - if (err) return done(err); - should(res.body).have.only.keys('_id', 'sample_id', 'number', 'parameters', 'treatment_template'); - should(res.body).have.property('_id').be.type('string'); - should(res.body).have.property('sample_id', '400000000000000000000002'); - should(res.body).have.property('number', 'A2'); - should(res.body).have.property('treatment_template', '200000000000000000000001'); - should(res.body).have.property('parameters'); - should(res.body.parameters).have.property('material', 'hot air'); - should(res.body.parameters).have.property('weeks', 10); - done(); - }); - }); - it('rejects an API key', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {key: 'janedoe'}, - httpStatus: 401, - req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'} - }); - }); - it('rejects requests from a read user', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - auth: {basic: 'user'}, - httpStatus: 403, - req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'} - }); - }); - it('rejects unauthorized requests', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/condition/new', - httpStatus: 401, - req: {sample_id: '400000000000000000000002', parameters: {material: 'hot air', weeks: 10}, treatment_template: '200000000000000000000001'} - }); - }); - }); -}); \ No newline at end of file diff --git a/src/routes/condition.ts b/src/routes/condition.ts deleted file mode 100644 index f66d10a..0000000 --- a/src/routes/condition.ts +++ /dev/null @@ -1,133 +0,0 @@ -import express from 'express'; -import _ from 'lodash'; - -import ConditionValidate from './validate/condition'; -import ParametersValidate from './validate/parameters'; -import res400 from './validate/res400'; -import SampleModel from '../models/sample'; -import ConditionModel from '../models/condition'; -import TreatmentTemplateModel from '../models/treatment_template'; -import IdValidate from './validate/id'; - - -const router = express.Router(); - -router.get('/condition/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; - - ConditionModel.findById(req.params.id).lean().exec((err, data) => { - if (err) return next(err); - if (data) { - res.json(ConditionValidate.output(data)); - } - else { - res.status(404).json({status: 'Not found'}); - } - }); -}); - -router.put('/condition/' + IdValidate.parameter(), async (req, res, next) => { - if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; - - const {error, value: condition} = ConditionValidate.input(req.body, 'change'); - if (error) return res400(error, res); - - const data = await ConditionModel.findById(req.params.id).lean().exec().catch(err => {next(err);}) as any; - if (data instanceof Error) return; - if (!data) { - res.status(404).json({status: 'Not found'}); - } - - // add properties needed for sampleIdCheck - condition.treatment_template = data.treatment_template; - condition.sample_id = data.sample_id; - if (!await sampleIdCheck(condition, req, res, next)) return; - if (condition.parameters) { - condition.parameters = _.assign({}, data.parameters, condition.parameters); - if (!_.isEqual(condition.parameters, data.parameters)) { // parameters did not change - condition.status = 0; - } - } - if (!await treatmentCheck(condition, 'change', res, next)) return; - - await ConditionModel.findByIdAndUpdate(req.params.id, condition, {new: true}).lean().exec((err, data) => { - if (err) return next(err); - res.json(ConditionValidate.output(data)); - }); -}); - -router.delete('/condition/' + IdValidate.parameter(), (req, res, next) => { - if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; - - ConditionModel.findById(req.params.id).lean().exec(async (err, data: any) => { - if (err) return next(err); - if (!data) { - res.status(404).json({status: 'Not found'}); - } - if (!await sampleIdCheck(data, req, res, next)) return; - await ConditionModel.findByIdAndUpdate(req.params.id, {status: -1}).lean().exec(err => { - if (err) return next(err); - res.json({status: 'OK'}); - }); - }); -}); - -router.post('/condition/new', async (req, res, next) => { - if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; - - const {error, value: condition} = ConditionValidate.input(req.body, 'new'); - if (error) return res400(error, res); - - if (!await sampleIdCheck(condition, req, res, next)) return; - const treatmentData = await treatmentCheck(condition, 'new', res, next) - if (!treatmentData) return; - - condition.number = await numberGenerate(condition, treatmentData, next); - if (!condition.number) return; - condition.status = 0; // set status to new - await new ConditionModel(condition).save((err, data) => { - if (err) return next(err); - res.json(ConditionValidate.output(data.toObject())); - }); -}) - - -module.exports = router; - - -async function sampleIdCheck (condition, req, res, next) { // validate sample_id, returns false if invalid - const sampleData = await SampleModel.findById(condition.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 - } - - if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return false; // sample does not belong to user - return true; -} - -async function numberGenerate (condition, treatmentData, next) { // generate number, returns false on error - const conditionData = await ConditionModel // find condition with highest number belonging to the same sample - .find({sample_id: condition.sample_id, number: new RegExp('^' + treatmentData.number_prefix + '[0-9]+$', 'm')}) - .sort({number: -1}) - .limit(1) - .lean() - .exec() - .catch(err => next(err)) as any; - if (conditionData instanceof Error) return false; - return treatmentData.number_prefix + (conditionData.length > 0 ? Number(conditionData[0].number.replace(/[^0-9]+/g, '')) + 1 : 1); // return new number -} - -async function treatmentCheck (condition, param, res, next) { // validate treatment template, returns false if invalid, otherwise template data - const treatmentData = await TreatmentTemplateModel.findById(condition.treatment_template).lean().exec().catch(err => next(err)) as any; - if (treatmentData instanceof Error) return false; - if (!treatmentData) { // template not found - res.status(400).json({status: 'Treatment template not available'}); - return false; - } - - // validate parameters - const {error, value: ignore} = ParametersValidate.input(condition.parameters, treatmentData.parameters, param); - if (error) {res400(error, res); return false;} - return treatmentData; -} \ No newline at end of file diff --git a/src/routes/material.spec.ts b/src/routes/material.spec.ts index 21a278b..344642d 100644 --- a/src/routes/material.spec.ts +++ b/src/routes/material.spec.ts @@ -2,6 +2,7 @@ import should from 'should/as-function'; import _ from 'lodash'; import MaterialModel from '../models/material'; import TestHelper from "../test/helper"; +import globals from '../globals'; // TODO: color name must be unique to get color number // TODO: separate supplier/ material name into own collections @@ -22,7 +23,7 @@ describe('/material', () => { }).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 === 10).length); + should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === globals.status.validated).length); should(res.body).matchEach(material => { should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); should(material).have.property('_id').be.type('string'); @@ -50,7 +51,7 @@ describe('/material', () => { }).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 === 10).length); + should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === globals.status.validated).length); should(res.body).matchEach(material => { should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); should(material).have.property('_id').be.type('string'); @@ -89,7 +90,7 @@ describe('/material', () => { if (err) return done(err); const json = require('../test/db.json'); let asyncCounter = res.body.length; - should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === 0).length); + should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status ===globals.status.new).length); should(res.body).matchEach(material => { should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); should(material).have.property('_id').be.type('string'); @@ -105,7 +106,7 @@ describe('/material', () => { should(number).have.property('number').be.type('string'); }); MaterialModel.findById(material._id).lean().exec((err, data) => { - should(data).have.property('status', 0); + should(data).have.property('status',globals.status.new); if (--asyncCounter === 0) { done(); } @@ -123,7 +124,7 @@ describe('/material', () => { if (err) return done(err); const json = require('../test/db.json'); let asyncCounter = res.body.length; - should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === -1).length); + should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status ===globals.status.deleted).length); should(res.body).matchEach(material => { should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); should(material).have.property('_id').be.type('string'); @@ -139,7 +140,7 @@ describe('/material', () => { should(number).have.property('number').be.type('string'); }); MaterialModel.findById(material._id).lean().exec((err, data) => { - should(data).have.property('status', -1); + should(data).have.property('status',globals.status.deleted); if (--asyncCounter === 0) { done(); } @@ -249,7 +250,7 @@ describe('/material', () => { should(res.body).be.eql({_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: '5514263423'}, {color: 'natural', number: '5514263422'}]}); MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { if (err) return done(err); - should(data).have.property('status', 10); + should(data).have.property('status',globals.status.validated); done(); }); }); @@ -266,7 +267,7 @@ describe('/material', () => { should(res.body).be.eql({_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: '5514263423'}, {color: 'natural', number: '5514263422'}]}); MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { if (err) return done(err); - should(data).have.property('status', 10); + should(data).have.property('status',globals.status.validated); done(); }); }); @@ -513,7 +514,7 @@ describe('/material', () => { should(data[0]).have.property('mineral', '0'); should(data[0]).have.property('glass_fiber', '30'); should(data[0]).have.property('carbon_fiber', '0'); - should(data[0]).have.property('status', 0); + should(data[0]).have.property('status',globals.status.new); should(data[0].numbers).have.lengthOf(0); done(); }); @@ -552,7 +553,7 @@ describe('/material', () => { should(data[0]).have.property('mineral', '0'); should(data[0]).have.property('glass_fiber', '30'); should(data[0]).have.property('carbon_fiber', '0'); - should(data[0]).have.property('status', 0); + should(data[0]).have.property('status',globals.status.new); should(_.omit(data[0].numbers[0], '_id')).be.eql({color: 'black', number: ''}); done(); }); diff --git a/src/routes/material.ts b/src/routes/material.ts index dd89985..4a1adb8 100644 --- a/src/routes/material.ts +++ b/src/routes/material.ts @@ -7,6 +7,7 @@ import SampleModel from '../models/sample'; import IdValidate from './validate/id'; import res400 from './validate/res400'; import mongoose from 'mongoose'; +import globals from '../globals'; @@ -15,7 +16,7 @@ const router = express.Router(); router.get('/materials', (req, res, next) => { if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; - MaterialModel.find({status: 10}).lean().exec((err, data) => { + MaterialModel.find({status:globals.status.validated}).lean().exec((err, data) => { if (err) return next(err); res.json(_.compact(data.map(e => MaterialValidate.output(e)))); // validate all and filter null values from validation errors }); @@ -24,14 +25,7 @@ router.get('/materials', (req, res, next) => { router.get('/materials/:group(new|deleted)', (req, res, next) => { if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; - let status; - switch (req.params.group) { - case 'new': status = 0; - break; - case 'deleted': status = -1; - break; - } - MaterialModel.find({status: status}).lean().exec((err, data) => { + MaterialModel.find({status: globals.status[req.params.group]}).lean().exec((err, data) => { if (err) return next(err); res.json(_.compact(data.map(e => MaterialValidate.output(e)))); // validate all and filter null values from validation errors }); @@ -67,7 +61,7 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => { // check for changes if (!_.isEqual(_.pick(IdValidate.stringify(materialData), _.keys(material)), material)) { - material.status = 0; // set status to new + material.status = globals.status.new; // set status to new } await MaterialModel.findByIdAndUpdate(req.params.id, material, {new: true}).lean().exec((err, data) => { @@ -86,7 +80,7 @@ router.delete('/material/' + IdValidate.parameter(), (req, res, next) => { if (data.length) { return res.status(400).json({status: 'Material still in use'}); } - MaterialModel.findByIdAndUpdate(req.params.id, {status: -1}).lean().exec((err, data) => { + MaterialModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).lean().exec((err, data) => { if (err) return next(err); if (data) { res.json({status: 'OK'}); @@ -106,7 +100,7 @@ router.post('/material/new', async (req, res, next) => { if (!await nameCheck(material, res, next)) return; - material.status = 0; // set status to new + material.status = globals.status.new; // set status to new await new MaterialModel(material).save((err, data) => { if (err) return next(err); res.json(MaterialValidate.output(data.toObject())); diff --git a/src/routes/measurement.spec.ts b/src/routes/measurement.spec.ts index 7fe4b7f..8ca49ed 100644 --- a/src/routes/measurement.spec.ts +++ b/src/routes/measurement.spec.ts @@ -1,6 +1,7 @@ import should from 'should/as-function'; import MeasurementModel from '../models/measurement'; import TestHelper from "../test/helper"; +import globals from '../globals'; // TODO: allow empty values @@ -78,7 +79,7 @@ describe('/measurement', () => { should(res.body).be.eql({_id: '800000000000000000000001', condition_id: '700000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}, measurement_template: '300000000000000000000001'}); MeasurementModel.findById('800000000000000000000001').lean().exec((err, data: any) => { if (err) return done(err); - should(data).have.property('status', 10); + should(data).have.property('status',globals.status.validated); done(); }); }); @@ -95,7 +96,7 @@ describe('/measurement', () => { should(res.body).be.eql({_id: '800000000000000000000002', condition_id: '700000000000000000000002', 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', 10); + should(data).have.property('status',globals.status.validated); done(); }); }); @@ -114,7 +115,7 @@ describe('/measurement', () => { should(data).have.only.keys('_id', 'condition_id', 'values', 'measurement_template', 'status', '__v'); should(data.condition_id.toString()).be.eql('700000000000000000000001'); should(data.measurement_template.toString()).be.eql('300000000000000000000001'); - should(data).have.property('status', 0); + should(data).have.property('status',globals.status.new); should(data).have.property('values'); should(data.values).have.property('dpt', [[1,2],[3,4],[5,6]]); done(); @@ -256,7 +257,7 @@ describe('/measurement', () => { should(res.body).be.eql({status: 'OK'}); MeasurementModel.findById('800000000000000000000001').lean().exec((err, data) => { if (err) return done(err); - should(data).have.property('status', -1); + should(data).have.property('status',globals.status.deleted); done(); }); }); diff --git a/src/routes/measurement.ts b/src/routes/measurement.ts index eda839e..b9af125 100644 --- a/src/routes/measurement.ts +++ b/src/routes/measurement.ts @@ -2,12 +2,12 @@ import express from 'express'; import _ from 'lodash'; import MeasurementModel from '../models/measurement'; -import ConditionModel from '../models/condition'; import MeasurementTemplateModel from '../models/measurement_template'; import MeasurementValidate from './validate/measurement'; import IdValidate from './validate/id'; import res400 from './validate/res400'; import ParametersValidate from './validate/parameters'; +import globals from '../globals'; const router = express.Router(); @@ -46,7 +46,7 @@ router.put('/measurement/' + IdValidate.parameter(), async (req, res, next) => { if (measurement.values) { measurement.values = _.assign({}, data.values, measurement.values); if (!_.isEqual(measurement.values, data.values)) { - measurement.status = 0; // set status to new + measurement.status = globals.status.new; // set status to new } } @@ -66,7 +66,7 @@ router.delete('/measurement/' + IdValidate.parameter(), (req, res, next) => { res.status(404).json({status: 'Not found'}); } if (!await conditionIdCheck(data, req, res, next)) return; - await MeasurementModel.findByIdAndUpdate(req.params.id, {status: -1}).lean().exec(err => { + await MeasurementModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).lean().exec(err => { if (err) return next(err); res.json({status: 'OK'}); }); @@ -93,14 +93,14 @@ router.post('/measurement/new', async (req, res, next) => { module.exports = router; -async function conditionIdCheck (measurement, req, res, next) { // validate condition_id, returns false if invalid - const sampleData = await ConditionModel.findById(measurement.condition_id).populate('sample_id').lean().exec().catch(err => {next(err); return false;}) as any; - if (!sampleData) { // sample_id not found - res.status(400).json({status: 'Condition id not available'}); +async function conditionIdCheck (measurement, req, res, next) { // validate condition_id, returns false if invalid // TODO + // const sampleData = await ConditionModel.findById(measurement.condition_id).populate('sample_id').lean().exec().catch(err => {next(err); return false;}) as any; + // if (!sampleData) { // sample_id not found + // res.status(400).json({status: 'Condition id not available'}); return false - } - if (sampleData.sample_id.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return false; // sample does not belong to user - return true; + // } + // if (sampleData.sample_id.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return false; // sample does not belong to user + // return true; } async function templateCheck (measurement, param, res, next) { // validate measurement_template and values, param for new/change diff --git a/src/routes/sample.spec.ts b/src/routes/sample.spec.ts index df1ad05..f54f5b4 100644 --- a/src/routes/sample.spec.ts +++ b/src/routes/sample.spec.ts @@ -3,10 +3,16 @@ import SampleModel from '../models/sample'; import NoteModel from '../models/note'; import NoteFieldModel from '../models/note_field'; import TestHelper from "../test/helper"; +import globals from '../globals'; // TODO: generate output for ML in format DPT -> data, implement filtering, field selection +// TODO: filter by not completely filled/no measurements // TODO: write script for data import // TODO: delete everything (measurements, condition) with sample +// TODO: allow adding sample numbers for existing samples + +// TODO: Do not allow validation or measurement entry without condition + describe('/sample', () => { let server; @@ -24,14 +30,16 @@ describe('/sample', () => { }).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 === 10).length); + should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status ===globals.status.validated).length); should(res.body).matchEach(sample => { - should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id'); + should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'condition', 'material_id', 'note_id', 'user_id'); should(sample).have.property('_id').be.type('string'); should(sample).have.property('number').be.type('string'); should(sample).have.property('type').be.type('string'); should(sample).have.property('color').be.type('string'); should(sample).have.property('batch').be.type('string'); + should(sample).have.property('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'); @@ -48,17 +56,19 @@ describe('/sample', () => { }).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 === 10).length); - should(res.body).matchEach(material => { - should(material).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id'); - should(material).have.property('_id').be.type('string'); - should(material).have.property('number').be.type('string'); - should(material).have.property('type').be.type('string'); - should(material).have.property('color').be.type('string'); - should(material).have.property('batch').be.type('string'); - should(material).have.property('material_id').be.type('string'); - should(material).have.property('note_id'); - should(material).have.property('user_id').be.type('string'); + should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status ===globals.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'); + 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'); }); done(); }); @@ -83,25 +93,28 @@ describe('/sample', () => { if (err) return done(err); const json = require('../test/db.json'); let asyncCounter = res.body.length; - should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status === 0).length); + should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status ===globals.status.new).length); should(res.body).matchEach(sample => { - should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id'); + should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'condition', 'material_id', 'note_id', 'user_id'); should(sample).have.property('_id').be.type('string'); should(sample).have.property('number').be.type('string'); should(sample).have.property('type').be.type('string'); should(sample).have.property('color').be.type('string'); should(sample).have.property('batch').be.type('string'); + should(sample).have.property('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'); SampleModel.findById(sample._id).lean().exec((err, data) => { - should(data).have.property('status', 0); + should(data).have.property('status',globals.status.new); if (--asyncCounter === 0) { done(); } }); }); - done(); }); }); it('returns all deleted samples', done => { @@ -116,23 +129,26 @@ describe('/sample', () => { let asyncCounter = res.body.length; should(res.body).have.lengthOf(json.collections.samples.filter(e => e.status === -1).length); should(res.body).matchEach(sample => { - should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id'); + should(sample).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'condition', 'material_id', 'note_id', 'user_id'); should(sample).have.property('_id').be.type('string'); should(sample).have.property('number').be.type('string'); should(sample).have.property('type').be.type('string'); should(sample).have.property('color').be.type('string'); should(sample).have.property('batch').be.type('string'); + should(sample).have.property('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'); SampleModel.findById(sample._id).lean().exec((err, data) => { - should(data).have.property('status', -1); + should(data).have.property('status',globals.status.deleted); if (--asyncCounter === 0) { done(); } }); }); - done(); }); }); it('rejects requests from a write user', done => { @@ -160,6 +176,73 @@ describe('/sample', () => { }); }); + 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: 'part', 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', mineral: 0, glass_fiber: 33, carbon_fiber: 0, numbers: [{color: 'black', number: '5514262406'}]}, notes: {comment: '', sample_references: [{sample_id: '400000000000000000000004', relation: 'granulate to sample'}], custom_fields: {'not allowed for new applications': true}}, 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: 'part', 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', mineral: 0, glass_fiber: 33, carbon_fiber: 0, numbers: [{color: 'black', number: '5514262406'}]}, notes: {comment: '', sample_references: [{sample_id: '400000000000000000000004', relation: 'granulate to sample'}], custom_fields: {'not allowed for new applications': true}}, user: 'admin'} + }); + }); + + it('returns a deleted sample for a maintain/admin user', done => { // TODO: make tests work + TestHelper.request(server, done, { + method: 'get', + url: '/sample/400000000000000000000005', + auth: {basic: 'admin'}, + httpStatus: 200, + res: {_id: '400000000000000000000005', number: 'Rng33', type: 'granulate', color: 'black', batch: '1653000308', condition: {condition_template: '200000000000000000000003'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', mineral: 0, glass_fiber: 33, carbon_fiber: 0, numbers: [{color: 'black', number: '5514262406'}]}, notes: {}, 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, { @@ -168,7 +251,7 @@ describe('/sample', () => { auth: {basic: 'janedoe'}, httpStatus: 200, req: {}, - res: {_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'} + res: {_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'} }); }); it('keeps unchanged properties', done => { @@ -177,21 +260,22 @@ describe('/sample', () => { url: '/sample/400000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 200, - req: {type: 'granulate', color: 'black', batch: '', material_id: '100000000000000000000004', notes: {}} + req: {type: 'granulate', 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: 'granulate', color: 'black', batch: '', material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'}); + should(res.body).be.eql({_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'}); SampleModel.findById('400000000000000000000001').lean().exec((err, data: any) => { if (err) return done (err); - should(data).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'material_id', 'note_id', 'user_id', 'status', '__v'); + should(data).have.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', 'granulate'); 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', 10); + should(data).have.property('status',globals.status.validated); should(data).have.property('note_id', null); done(); }); @@ -206,10 +290,27 @@ describe('/sample', () => { req: {type: 'granulate'} }).end((err, res) => { if (err) return done(err); - should(res.body).be.eql({_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'}); + should(res.body).be.eql({_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'}); SampleModel.findById('400000000000000000000001').lean().exec((err, data: any) => { if (err) return done (err); - should(data).have.property('status', 10); + should(data).have.property('status',globals.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: 'granulate', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'}); + SampleModel.findById('400000000000000000000001').lean().exec((err, data: any) => { + if (err) return done (err); + should(data).have.property('status',globals.status.validated); done(); }); }); @@ -223,18 +324,21 @@ describe('/sample', () => { 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: 'granulate', color: 'natural', batch: '1560237365', material_id: '100000000000000000000001', note_id: '500000000000000000000001', user_id: '000000000000000000000002'}); + should(res.body).be.eql({_id: '400000000000000000000002', number: '21', type: 'granulate', color: 'natural', batch: '1560237365', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', note_id: '500000000000000000000001', user_id: '000000000000000000000002'}); SampleModel.findById('400000000000000000000002').lean().exec((err, data: any) => { if (err) return done (err); - should(data).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'material_id', 'note_id', 'user_id', 'status', '__v'); + should(data).have.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', 'granulate'); 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', 10); + should(data).have.property('status',globals.status.validated); should(data.note_id.toString()).be.eql('500000000000000000000001'); done(); }); @@ -246,20 +350,21 @@ describe('/sample', () => { url: '/sample/400000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 200, - req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} + req: {type: 'part', color: 'signalviolet', batch: '114531', 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', 'material_id', 'note_id', 'user_id', 'status', '__v'); + 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', 'part'); 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', 0); + should(data).have.property('status',globals.status.new); should(data).have.property('note_id'); NoteModel.findById(data.note_id).lean().exec((err, data: any) => { if (err) return done (err); @@ -267,7 +372,7 @@ describe('/sample', () => { should(data).have.property('comment', 'Testcomment'); should(data).have.property('sample_references'); should(data.sample_references).have.lengthOf(1); - should(data.sample_references[0].id.toString()).be.eql('400000000000000000000003'); + should(data.sample_references[0].sample_id.toString()).be.eql('400000000000000000000003'); should(data.sample_references[0]).have.property('relation', 'part to this sample'); done(); }); @@ -350,7 +455,7 @@ describe('/sample', () => { url: '/sample/400000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, res: {status: 'Color not available for material'} }); }); @@ -360,7 +465,7 @@ describe('/sample', () => { url: '/sample/400000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '000000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '000000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, res: {status: 'Material not available'} }); }); @@ -370,7 +475,7 @@ describe('/sample', () => { url: '/sample/400000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {number: 25, type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + req: {number: 25, type: 'part', 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'} }); }); @@ -380,7 +485,7 @@ describe('/sample', () => { url: '/sample/400000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '000000000000000000000003', relation: 'part to this sample'}]}}, + req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '000000000000000000000003', relation: 'part to this sample'}]}}, res: {status: 'Sample reference not available'} }); }); @@ -390,7 +495,7 @@ describe('/sample', () => { url: '/sample/400000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, res: {status: 'Invalid body format', details: '"material_id" with value "10000000000h000000000001" fails to match the required pattern: /[0-9a-f]{24}/'} }); }); @@ -400,7 +505,87 @@ describe('/sample', () => { url: '/sample/10000000000h000000000001', auth: {basic: 'janedoe'}, httpStatus: 404, - req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{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 larger 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 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('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: 'granulate', color: 'black', batch: '', condition: {}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'} + }); + }); + it('rejects an 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 an API key', done => { @@ -409,7 +594,7 @@ describe('/sample', () => { url: '/sample/400000000000000000000001', auth: {key: 'janedoe'}, httpStatus: 401, - req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, }); }); it('rejects changes for samples from another user for a write user', done => { @@ -428,7 +613,7 @@ describe('/sample', () => { auth: {basic: 'admin'}, httpStatus: 200, req: {}, - res: {_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'} + res: {_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', condition: {condition_template: '200000000000000000000001', material: 'copper', weeks: 3}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'} }); }); it('rejects requests from a read user', done => { @@ -437,7 +622,7 @@ describe('/sample', () => { url: '/sample/400000000000000000000001', auth: {basic: 'user'}, httpStatus: 403, - req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, }); }); it('returns 404 for an unknown sample', done => { @@ -446,7 +631,7 @@ describe('/sample', () => { url: '/sample/000000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 404, - req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} + req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}} }); }) it('rejects unauthorized requests', done => { @@ -454,7 +639,7 @@ describe('/sample', () => { method: 'put', url: '/sample/400000000000000000000001', httpStatus: 401, - req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, }); }); }); @@ -471,15 +656,18 @@ describe('/sample', () => { 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', 'material_id', 'note_id', 'user_id', 'status', '__v'); + 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', 'granulate'); 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', -1); + should(data).have.property('status',globals.status.deleted); should(data).have.property('note_id', null); done(); }); @@ -536,7 +724,7 @@ describe('/sample', () => { 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].id.toString()).be.eql('400000000000000000000003'); + should(data.sample_references[0].sample_id.toString()).be.eql('400000000000000000000003'); should(data.sample_references[0]).have.property('relation', 'part to sample'); done(); }); @@ -555,7 +743,7 @@ describe('/sample', () => { should(res.body).be.eql({status: 'OK'}); SampleModel.findById('400000000000000000000001').lean().exec((err, data) => { if (err) return done(err); - should(data).have.property('status', -1); + should(data).have.property('status',globals.status.deleted); done(); }); }); @@ -617,15 +805,16 @@ describe('/sample', () => { url: '/sample/new', auth: {basic: 'janedoe'}, httpStatus: 200, - req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} + req: {color: 'black', type: 'granulate', batch: '1560237365', 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', 'material_id', 'note_id', 'user_id'); + should(res.body).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'condition', 'material_id', 'note_id', 'user_id'); should(res.body).have.property('_id').be.type('string'); - should(res.body).have.property('number', 'Rng34'); + should(res.body).have.property('number', 'Rng37'); should(res.body).have.property('color', 'black'); should(res.body).have.property('type', 'granulate'); should(res.body).have.property('batch', '1560237365'); + should(res.body).have.property('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'); @@ -638,21 +827,22 @@ describe('/sample', () => { url: '/sample/new', auth: {basic: 'janedoe'}, httpStatus: 200, - req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} + req: {color: 'black', type: 'granulate', batch: '1560237365', 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: 'Rng34'}).lean().exec((err, data: any) => { + 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', 'material_id', 'note_id', 'user_id', 'status', '__v'); + 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', 'Rng34'); + should(data[0]).have.property('number', 'Rng37'); should(data[0]).have.property('color', 'black'); should(data[0]).have.property('type', 'granulate'); 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', 0); + should(data[0]).have.property('status',globals.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); @@ -660,7 +850,7 @@ describe('/sample', () => { should(data).have.property('comment', 'Testcomment'); should(data).have.property('sample_references'); should(data.sample_references).have.lengthOf(1); - should(data.sample_references[0].id.toString()).be.eql('400000000000000000000003'); + should(data.sample_references[0].sample_id.toString()).be.eql('400000000000000000000003'); should(data.sample_references[0]).have.property('relation', 'part to this sample'); done(); }); @@ -710,10 +900,10 @@ describe('/sample', () => { url: '/sample/new', auth: {basic: 'johnnydoe'}, httpStatus: 200, - req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} + req: {color: 'black', type: 'granulate', 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', 'material_id', 'note_id', 'user_id'); + should(res.body).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'condition', 'material_id', 'note_id', 'user_id'); should(res.body).have.property('_id').be.type('string'); should(res.body).have.property('number', 'Fe1'); should(res.body).have.property('color', 'black'); @@ -725,13 +915,35 @@ describe('/sample', () => { 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: 'granulate', 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'); + 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', 'granulate'); + 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'); + done(); + }); + }); it('rejects a color not defined for the material', done => { TestHelper.request(server, done, { method: 'post', url: '/sample/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {color: 'green', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + req: {color: 'green', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, res: {status: 'Color not available for material'} }); }); @@ -741,7 +953,7 @@ describe('/sample', () => { url: '/sample/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '000000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '000000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, res: {status: 'Material not available'} }); }); @@ -751,7 +963,7 @@ describe('/sample', () => { url: '/sample/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {number: 'Rng34', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + req: {number: 'Rng34', color: 'black', type: 'granulate', 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'} }); }); @@ -761,17 +973,97 @@ describe('/sample', () => { url: '/sample/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '000000000000000000000003', relation: 'part to this sample'}]}}, + req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{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: 'granulate', 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: 'granulate', 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: 'granulate', batch: '1560237365', condition: {material: 'copper', weeks: 3, xxx: 23, condition_template: '20000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Condition template not available'} + }); + }); + it('rejects missing condition parameters', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'granulate', batch: '1560237365', condition: {material: 'copper', condition_template: '20000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, + res: {status: 'Condition template not available'} + }); + }); + 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: 'granulate', batch: '1560237365', condition: {material: 'xxx', weeks: 3, condition_template: '20000000000000000000001'}, 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 condition parameter below minimum range', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'granulate', batch: '1560237365', condition: {material: 'copper', weeks: 0, condition_template: '20000000000000000000001'}, 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 condition parameter above maximum range', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'granulate', batch: '1560237365', condition: {material: 'copper', weeks: 11, condition_template: '20000000000000000000001'}, 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 condition without condition template', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/sample/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {color: 'black', type: 'granulate', 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 a missing color', done => { TestHelper.request(server, done, { method: 'post', url: '/sample/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + req: {type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, res: {status: 'Invalid body format', details: '"color" is required'} }); }); @@ -781,7 +1073,7 @@ describe('/sample', () => { url: '/sample/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {color: 'black', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + req: {color: 'black', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, res: {status: 'Invalid body format', details: '"type" is required'} }); }); @@ -791,7 +1083,7 @@ describe('/sample', () => { url: '/sample/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {color: 'black', type: 'granulate', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + req: {color: 'black', type: 'granulate', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, res: {status: 'Invalid body format', details: '"batch" is required'} }); }); @@ -801,7 +1093,7 @@ describe('/sample', () => { url: '/sample/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {color: 'black', type: 'granulate', batch: '1560237365', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + req: {color: 'black', type: 'granulate', batch: '1560237365', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, res: {status: 'Invalid body format', details: '"material_id" is required'} }); }); @@ -811,7 +1103,7 @@ describe('/sample', () => { url: '/sample/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, + req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, res: {status: 'Invalid body format', details: '"material_id" with value "10000000000h000000000001" fails to match the required pattern: /[0-9a-f]{24}/'} }); }); @@ -821,7 +1113,7 @@ describe('/sample', () => { url: '/sample/new', auth: {key: 'janedoe'}, httpStatus: 401, - req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} + req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}} }); }); it('rejects requests from a read user', done => { @@ -830,7 +1122,7 @@ describe('/sample', () => { url: '/sample/new', auth: {basic: 'user'}, httpStatus: 403, - req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} + req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}} }); }); it('rejects unauthorized requests', done => { @@ -838,7 +1130,7 @@ describe('/sample', () => { method: 'post', url: '/sample/new', httpStatus: 401, - req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}} + req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}} }); }); }); diff --git a/src/routes/sample.ts b/src/routes/sample.ts index 43acd6e..ed1afb3 100644 --- a/src/routes/sample.ts +++ b/src/routes/sample.ts @@ -5,10 +5,15 @@ import SampleValidate from './validate/sample'; import NoteFieldValidate from './validate/note_field'; import res400 from './validate/res400'; import SampleModel from '../models/sample' +import MeasurementModel from '../models/measurement'; import MaterialModel from '../models/material'; import NoteModel from '../models/note'; import NoteFieldModel from '../models/note_field'; import IdValidate from './validate/id'; +import mongoose from "mongoose"; +import ConditionTemplateModel from '../models/condition_template'; +import ParametersValidate from './validate/parameters'; +import globals from '../globals'; const router = express.Router(); @@ -16,7 +21,7 @@ const router = express.Router(); router.get('/samples', (req, res, next) => { if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; - SampleModel.find({status: 10}).lean().exec((err, data) => { + SampleModel.find({status: globals.status.validated}).lean().exec((err, data) => { if (err) return next(err); res.json(_.compact(data.map(e => SampleValidate.output(e)))); // validate all and filter null values from validation errors }) @@ -25,17 +30,32 @@ router.get('/samples', (req, res, next) => { router.get('/samples/:group(new|deleted)', (req, res, next) => { if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; - let status; - switch (req.params.group) { - case 'new': status = 0; - break; - case 'deleted': status = -1; - break; - } - SampleModel.find({status: status}).lean().exec((err, data) => { + SampleModel.find({status: globals.status[req.params.group]}).lean().exec((err, data) => { if (err) return next(err); res.json(_.compact(data.map(e => SampleValidate.output(e)))); // validate all and filter null values from validation errors - }) + }); +}); + +router.get('/sample/' + IdValidate.parameter(), (req, res, next) => { + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + + SampleModel.findById(req.params.id).populate('material_id').populate('user_id', 'name').populate('note_id').lean().exec((err, sampleData: any) => { + if (err) return next(err); + + if (sampleData) { + if (sampleData.status ===globals.status.deleted && !req.auth(res, ['maintain', 'admin'], 'all')) return; // deleted samples only available for maintain/admin + sampleData.material = sampleData.material_id; // map data to right keys + sampleData.user = sampleData.user_id.name; + sampleData.notes = sampleData.note_id ? sampleData.note_id : {}; + MeasurementModel.find({sample_id: mongoose.Types.ObjectId(req.params.id)}).lean().exec((err, data) => { + sampleData.measurements = data; + res.json(SampleValidate.output(sampleData, 'details')); + }); + } + else { + res.status(404).json({status: 'Not found'}); + } + }); }); router.put('/sample/' + IdValidate.parameter(), (req, res, next) => { @@ -60,6 +80,10 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => { if (!await materialCheck(sample, res, next, sampleData.material_id)) return; } + if (sample.hasOwnProperty('condition') && !(_.isEmpty(sample.condition) && _.isEmpty(sampleData.condition))) { // do not execute check if condition is and was empty + if (!await conditionCheck(sample.condition, 'change', res, next)) return; + } + if (sample.hasOwnProperty('notes')) { let newNotes = true; if (sampleData.note_id !== null) { // old notes data exists @@ -89,10 +113,10 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => { // check for changes if (!_.isEqual(_.pick(IdValidate.stringify(sampleData), _.keys(sample)), _.omit(sample, ['notes']))) { - sample.status = 0; + sample.status = globals.status.new; } - await SampleModel.findByIdAndUpdate(req.params.id, sample, {new: true}).lean().exec((err, data) => { + await SampleModel.findByIdAndUpdate(req.params.id, sample, {new: true}).lean().exec((err, data: any) => { if (err) return next(err); res.json(SampleValidate.output(data)); }); @@ -112,7 +136,7 @@ router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => { // only maintain and admin are allowed to edit other user's data if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return; - await SampleModel.findByIdAndUpdate(req.params.id, {status: -1}).lean().exec(err => { // set sample status + await SampleModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).lean().exec(err => { // set sample status 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 @@ -133,6 +157,10 @@ router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => { router.post('/sample/new', async (req, res, next) => { if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; + if (!req.body.hasOwnProperty('condition')) { // add empty condition if not specified + req.body.condition = {}; + } + const {error, value: sample} = SampleValidate.input(req.body, 'new'); if (error) return res400(error, res); @@ -143,7 +171,11 @@ router.post('/sample/new', async (req, res, next) => { customFieldsChange(Object.keys(sample.notes.custom_fields), 1); } - sample.status = 0; // set status to new + if (!_.isEmpty(sample.condition)) { // do not execute check if condition is empty + if (!await conditionCheck(sample.condition, 'change', res, next)) return; + } + + sample.status = globals.status.new; // set status to new sample.number = await numberGenerate(sample, req, res, next); if (!sample.number) return; @@ -152,6 +184,7 @@ router.post('/sample/new', async (req, res, next) => { 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); res.json(SampleValidate.output(data.toObject())); @@ -172,14 +205,15 @@ router.get('/sample/notes/fields', (req, res, next) => { module.exports = router; -async function numberGenerate (sample, req, res, next) { // generate number, returns false on error +async function numberGenerate (sample, req, res, next) { // generate number in format Location32, returns false on error const sampleData = await SampleModel - .find({number: new RegExp('^' + req.authDetails.location + '[0-9]+$', 'm')}) + .findOne({number: new RegExp('^' + req.authDetails.location + '[0-9]+$', 'm')}) + .sort({number: -1}) .lean() .exec() .catch(err => next(err)); if (sampleData instanceof Error) return false; - return req.authDetails.location + (sampleData.length > 0 ? Number(sampleData[0].number.replace(/[^0-9]+/g, '')) + 1 : 1); + return req.authDetails.location + (sampleData ? Number(sampleData.number.replace(/[^0-9]+/g, '')) + 1 : 1); } async function materialCheck (sample, res, next, id = sample.material_id) { // validate material_id and color, returns false if invalid @@ -196,13 +230,31 @@ async function materialCheck (sample, res, next, id = sample.material_id) { // return true; } +async function conditionCheck (condition, param, res, next) { // validate treatment template, returns false if invalid, otherwise template data + 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; + } + + // validate parameters + const {error, value: ignore} = ParametersValidate.input(_.omit(condition, 'condition_template'), conditionData.parameters, param); + if (error) {res400(error, res); return false;} + return conditionData; +} + function sampleRefCheck (sample, res, next) { // validate sample_references, resolves false for invalid reference return new Promise(resolve => { if (sample.notes.sample_references.length > 0) { // there are sample_references let referencesCount = sample.notes.sample_references.length; // count to keep track of running async operations sample.notes.sample_references.forEach(reference => { - SampleModel.findById(reference.id).lean().exec((err, data) => { + 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'}); @@ -230,7 +282,7 @@ function customFieldsChange (fields, amount) { // update custom_fields and resp if (err) return console.error(err); }) } - else if (data.qty <= 0) { + else if (data.qty <= 0) { // delete document if field is not used anymore NoteFieldModel.findOneAndDelete({name: field}).lean().exec(err => { if (err) return console.error(err); }); diff --git a/src/routes/template.spec.ts b/src/routes/template.spec.ts index 878b778..2ac09ae 100644 --- a/src/routes/template.spec.ts +++ b/src/routes/template.spec.ts @@ -1,10 +1,12 @@ import should from 'should/as-function'; import _ from 'lodash'; -import TemplateTreatmentModel from '../models/treatment_template'; +import TemplateConditionModel from '../models/condition_template'; import TemplateMeasurementModel from '../models/measurement_template'; import TestHelper from "../test/helper"; // TODO: do not allow usage of old templates for new samples +// TODO: remove number_prefix +// TODO: template parameters are not allowed to be condition_template describe('/template', () => { let server; @@ -12,25 +14,24 @@ describe('/template', () => { beforeEach(done => server = TestHelper.beforeEach(server, done)); afterEach(done => TestHelper.afterEach(server, done)); - describe('/template/treatment', () => { - describe('GET /template/treatments', () => { - it('returns all treatment templates', done => { + describe('/template/condition', () => { + describe('GET /template/conditions', () => { + it('returns all condition templates', done => { TestHelper.request(server, done, { method: 'get', - url: '/template/treatments', + 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.treatment_templates.length); - should(res.body).matchEach(treatment => { - should(treatment).have.only.keys('_id', 'name', 'version', 'parameters', 'number_prefix'); - should(treatment).have.property('_id').be.type('string'); - should(treatment).have.property('name').be.type('string'); - should(treatment).have.property('version').be.type('number'); - should(treatment).have.property('number_prefix').be.type('string'); - should(treatment.parameters).matchEach(number => { + should(res.body).have.lengthOf(json.collections.condition_templates.length); + should(res.body).matchEach(condition => { + should(condition).have.only.keys('_id', 'name', 'version', '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.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'); @@ -42,7 +43,7 @@ describe('/template', () => { it('rejects an API key', done => { TestHelper.request(server, done, { method: 'get', - url: '/template/treatments', + url: '/template/conditions', auth: {key: 'janedoe'}, httpStatus: 401 }); @@ -50,26 +51,26 @@ describe('/template', () => { it('rejects unauthorized requests', done => { TestHelper.request(server, done, { method: 'get', - url: '/template/treatments', + url: '/template/conditions', httpStatus: 401 }); }); }); - describe('GET /template/treatment/{id}', () => { - it('returns the right treatment template', done => { + describe('GET /template/condition/{id}', () => { + it('returns the right condition template', done => { TestHelper.request(server, done, { method: 'get', - url: '/template/treatment/200000000000000000000001', + url: '/template/condition/200000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 200, - res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, number_prefix: 'A', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]} + res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]} }); }); it('rejects an API key', done => { TestHelper.request(server, done, { method: 'get', - url: '/template/treatment/200000000000000000000001', + url: '/template/condition/200000000000000000000001', auth: {key: 'janedoe'}, httpStatus: 401 }); @@ -77,7 +78,7 @@ describe('/template', () => { it('rejects an unknown id', done => { TestHelper.request(server, done, { method: 'get', - url: '/template/treatment/000000000000000000000001', + url: '/template/condition/000000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 404 }); @@ -85,58 +86,57 @@ describe('/template', () => { it('rejects unauthorized requests', done => { TestHelper.request(server, done, { method: 'get', - url: '/template/treatment/200000000000000000000001', + url: '/template/condition/200000000000000000000001', httpStatus: 401 }); }); }); - describe('PUT /template/treatment/{name}', () => { - it('returns the right treatment template', done => { + describe('PUT /template/condition/{name}', () => { + it('returns the right condition template', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/treatment/200000000000000000000001', + url: '/template/condition/200000000000000000000001', auth: {basic: 'admin'}, httpStatus: 200, req: {}, - res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, number_prefix: 'A', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]} + res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]} }); }); it('keeps unchanged properties', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/treatment/200000000000000000000001', + 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}}]}, - res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, number_prefix: 'A', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]} + res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]} }); }); it('keeps only one unchanged property', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/treatment/200000000000000000000001', + url: '/template/condition/200000000000000000000001', auth: {basic: 'admin'}, httpStatus: 200, req: {name: 'heat treatment'}, - res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, number_prefix: 'A', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]} + res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]} }); }); it('changes the given properties', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/treatment/200000000000000000000001', + 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); - TemplateTreatmentModel.findById(res.body._id).lean().exec((err, data:any) => { + TemplateConditionModel.findById(res.body._id).lean().exec((err, data:any) => { if (err) return done(err); - should(data).have.only.keys('_id', 'name', 'version', 'number_prefix', 'parameters', '__v'); + should(data).have.only.keys('_id', 'name', 'version', 'parameters', '__v'); should(data).have.property('name', 'heat aging'); should(data).have.property('version', 2); - should(data).have.property('number_prefix', 'A'); should(data).have.property('parameters').have.lengthOf(1); should(data.parameters[0]).have.property('name', 'time'); should(data.parameters[0]).have.property('range'); @@ -148,18 +148,17 @@ describe('/template', () => { it('allows changing only one property', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/treatment/200000000000000000000001', + url: '/template/condition/200000000000000000000001', auth: {basic: 'admin'}, httpStatus: 200, req: {name: 'heat aging'} }).end((err, res) => { if (err) return done(err); - TemplateTreatmentModel.findById(res.body._id).lean().exec((err, data:any) => { + TemplateConditionModel.findById(res.body._id).lean().exec((err, data:any) => { if (err) return done(err); - should(data).have.only.keys('_id', 'name', 'version', 'number_prefix', 'parameters', '__v'); + should(data).have.only.keys('_id', 'name', 'version', 'parameters', '__v'); should(data).have.property('name', 'heat aging'); should(data).have.property('version', 2); - should(data).have.property('number_prefix', 'A'); should(data).have.property('parameters').have.lengthOf(2); should(data.parameters[0]).have.property('name', 'material'); should(data.parameters[1]).have.property('name', 'weeks'); @@ -170,59 +169,59 @@ describe('/template', () => { it('supports values ranges', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/treatment/200000000000000000000001', + 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, number_prefix: 'A', parameters: [{name: 'time', range: {values: [1, 2, 5]}}]}); + should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, parameters: [{name: 'time', range: {values: [1, 2, 5]}}]}); done(); }); }); it('supports min max ranges', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/treatment/200000000000000000000001', + 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, number_prefix: 'A', parameters: [{name: 'time', range: {min: 1, max: 11}}]}); + should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, parameters: [{name: 'time', range: {min: 1, max: 11}}]}); done(); }); }); it('supports array type ranges', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/treatment/200000000000000000000001', + 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, number_prefix: 'A', parameters: [{name: 'time', range: {type: 'array'}}]}); + should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, parameters: [{name: 'time', range: {type: 'array'}}]}); done(); }); }); it('supports empty ranges', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/treatment/200000000000000000000001', + 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, number_prefix: 'A', parameters: [{name: 'time', range: {}}]}); + should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, parameters: [{name: 'time', range: {}}]}); done(); }); }); it('rejects not specified parameters', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/treatment/200000000000000000000001', + url: '/template/condition/200000000000000000000001', auth: {basic: 'admin'}, httpStatus: 400, req: {name: 'heat treatment', parameters: [{name: 'material', range: {xx: 5}}]}, @@ -232,7 +231,7 @@ describe('/template', () => { it('rejects an invalid id', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/treatment/2000000000h0000000000001', + url: '/template/condition/2000000000h0000000000001', auth: {basic: 'admin'}, httpStatus: 404, req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} @@ -241,26 +240,16 @@ describe('/template', () => { it('rejects an unknown id', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/treatment/000000000000000000000001', + url: '/template/condition/000000000000000000000001', auth: {basic: 'admin'}, httpStatus: 404, req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} }); }); - it('rejects already existing number prefixes', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/template/treatment/200000000000000000000001', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {number_prefix: 'B', parameters: [{name: 'time', range: {min: 1}}]}, - res: {status: 'Number prefix already taken'} - }); - }); it('rejects an API key', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/treatment/200000000000000000000001', + url: '/template/condition/200000000000000000000001', auth: {key: 'admin'}, httpStatus: 401, req: {} @@ -269,7 +258,7 @@ describe('/template', () => { it('rejects requests from a write user', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/treatment/200000000000000000000001', + url: '/template/condition/200000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 403, req: {} @@ -278,27 +267,26 @@ describe('/template', () => { it('rejects unauthorized requests', done => { TestHelper.request(server, done, { method: 'put', - url: '/template/treatment/200000000000000000000001', + url: '/template/condition/200000000000000000000001', httpStatus: 401, req: {} }); }); }); - describe('POST /template/treatment/new', () => { - it('returns the right treatment template', done => { + describe('POST /template/condition/new', () => { + it('returns the right condition template', done => { TestHelper.request(server, done, { method: 'post', - url: '/template/treatment/new', + url: '/template/condition/new', auth: {basic: 'admin'}, httpStatus: 200, - req: {name: 'heat treatment3', number_prefix: 'C', parameters: [{name: 'material', range: {values: ['copper']}}]} + 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', 'number_prefix', 'parameters'); + should(res.body).have.only.keys('_id', 'name', 'version', 'parameters'); should(res.body).have.property('name', 'heat treatment3'); should(res.body).have.property('version', 1); - should(res.body).have.property('number_prefix', 'C'); 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'); @@ -310,18 +298,17 @@ describe('/template', () => { it('stores the template', done => { TestHelper.request(server, done, { method: 'post', - url: '/template/treatment/new', + url: '/template/condition/new', auth: {basic: 'admin'}, httpStatus: 200, - req: {name: 'heat aging', number_prefix: 'C', parameters: [{name: 'time', range: {min: 1}}]} + req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} }).end((err, res) => { if (err) return done(err); - TemplateTreatmentModel.findById(res.body._id).lean().exec((err, data:any) => { + TemplateConditionModel.findById(res.body._id).lean().exec((err, data:any) => { if (err) return done(err); - should(data).have.only.keys('_id', 'name', 'version', 'number_prefix', 'parameters', '__v'); + should(data).have.only.keys('_id', 'name', 'version', 'parameters', '__v'); should(data).have.property('name', 'heat aging'); should(data).have.property('version', 1); - should(data).have.property('number_prefix', 'C'); should(data).have.property('parameters').have.lengthOf(1); should(data.parameters[0]).have.property('name', 'time'); should(data.parameters[0]).have.property('range'); @@ -333,117 +320,97 @@ describe('/template', () => { it('rejects a missing name', done => { TestHelper.request(server, done, { method: 'post', - url: '/template/treatment/new', + url: '/template/condition/new', auth: {basic: 'admin'}, httpStatus: 400, - req: {number_prefix: 'C', parameters: [{name: 'time', range: {min: 1}}]}, + req: {parameters: [{name: 'time', range: {min: 1}}]}, res: {status: 'Invalid body format', details: '"name" is required'} }); }); - it('rejects a missing number prefix', done => { + it('rejects a number prefix', done => { TestHelper.request(server, done, { method: 'post', - url: '/template/treatment/new', + url: '/template/condition/new', auth: {basic: 'admin'}, httpStatus: 400, - req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]}, - res: {status: 'Invalid body format', details: '"number_prefix" is required'} + req: {name: 'heat aging', number_prefix: '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/treatment/new', + url: '/template/condition/new', auth: {basic: 'admin'}, httpStatus: 400, - req: {name: 'heat aging', number_prefix: 'C'}, + 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/treatment/new', + url: '/template/condition/new', auth: {basic: 'admin'}, httpStatus: 400, - req: {name: 'heat aging', number_prefix: 'C', parameters: [{range: {min: 1}}]}, + req: {name: 'heat aging', parameters: [{range: {min: 1}}]}, res: {status: 'Invalid body format', details: '"parameters[0].name" is required'} }); }); - it('rejects a number prefix containing numbers', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/template/treatment/new', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {name: 'heat aging', number_prefix: 'AB5', parameters: [{name: 'time', range: {min: 1}}]}, - res: {status: 'Invalid body format', details: '"number_prefix" with value "AB5" fails to match the required pattern: /^[a-zA-Z]+$/'} - }); - }); it('rejects a missing parameter range', done => { TestHelper.request(server, done, { method: 'post', - url: '/template/treatment/new', + url: '/template/condition/new', auth: {basic: 'admin'}, httpStatus: 400, - req: {name: 'heat aging', number_prefix: 'C', parameters: [{name: 'time'}]}, + 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/treatment/new', + url: '/template/condition/new', auth: {basic: 'admin'}, httpStatus: 400, - req: {name: 'heat aging', number_prefix: 'C', parameters: [{name: 'time', range: {xx: 1}}]}, + 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/treatment/new', + url: '/template/condition/new', auth: {basic: 'admin'}, httpStatus: 400, - req: {name: 'heat aging', number_prefix: 'C', parameters: [{name: 'time', range: {}}], xx: 33}, + req: {name: 'heat aging', parameters: [{name: 'time', range: {}}], xx: 33}, res: {status: 'Invalid body format', details: '"xx" is not allowed'} }); }); - it('rejects already existing number prefixes', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/template/treatment/new', - auth: {basic: 'admin'}, - httpStatus: 400, - req: {name: 'heat aging', number_prefix: 'B', parameters: [{name: 'time', range: {min: 1}}]}, - res: {status: 'Number prefix already taken'} - }); - }); it('rejects an API key', done => { TestHelper.request(server, done, { method: 'post', - url: '/template/treatment/new', + url: '/template/condition/new', auth: {key: 'admin'}, httpStatus: 401, - req: {name: 'heat aging', number_prefix: 'C', parameters: [{name: 'time', range: {min: 1}}]} + 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/treatment/new', + url: '/template/condition/new', auth: {basic: 'janedoe'}, httpStatus: 403, - req: {name: 'heat aging', number_prefix: 'C', parameters: [{name: 'time', range: {min: 1}}]} + req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} }); }); it('rejects unauthorized requests', done => { TestHelper.request(server, done, { method: 'post', - url: '/template/treatment/new', + url: '/template/condition/new', httpStatus: 401, - req: {name: 'heat aging', number_prefix: 'C', parameters: [{name: 'time', range: {min: 1}}]} + req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]} }); }); }); diff --git a/src/routes/template.ts b/src/routes/template.ts index a8f7413..f4054c1 100644 --- a/src/routes/template.ts +++ b/src/routes/template.ts @@ -2,8 +2,8 @@ import express from 'express'; import _ from 'lodash'; import TemplateValidate from './validate/template'; -import TemplateTreatmentModel from '../models/treatment_template'; -import TemplateMeasurementModel from '../models/measurement_template'; +import ConditionTemplateModel from '../models/condition_template'; +import MeasurementTemplateModel from '../models/measurement_template'; import res400 from './validate/res400'; import IdValidate from './validate/id'; @@ -11,23 +11,23 @@ import IdValidate from './validate/id'; const router = express.Router(); -router.get('/template/:collection(measurements|treatments)', (req, res, next) => { +router.get('/template/:collection(measurements|conditions)', (req, res, next) => { if (!req.auth(res, ['read', 'write', 'maintain', '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); - res.json(_.compact(data.map(e => TemplateValidate.output(e, req.params.collection)))); // validate all and filter null values from validation errors + res.json(_.compact(data.map(e => TemplateValidate.output(e)))); // validate all and filter null values from validation errors }); }); -router.get('/template/:collection(measurement|treatment)/' + IdValidate.parameter(), (req, res, next) => { +router.get('/template/:collection(measurement|condition)/' + IdValidate.parameter(), (req, res, next) => { if (!req.auth(res, ['read', 'write', 'maintain', '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, req.params.collection)); + res.json(TemplateValidate.output(data)); } else { res.status(404).json({status: 'Not found'}); @@ -35,10 +35,10 @@ router.get('/template/:collection(measurement|treatment)/' + IdValidate.paramete }); }); -router.put('/template/:collection(measurement|treatment)/' + IdValidate.parameter(), async (req, res, next) => { +router.put('/template/:collection(measurement|condition)/' + IdValidate.parameter(), async (req, res, next) => { if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; - const {error, value: template} = TemplateValidate.input(req.body, 'change', req.params.collection); + const {error, value: template} = TemplateValidate.input(req.body, 'change'); if (error) return res400(error, res); const templateData = await model(req).findById(req.params.id).lean().exec().catch(err => {next(err);}) as any; @@ -47,52 +47,34 @@ router.put('/template/:collection(measurement|treatment)/' + IdValidate.paramete res.status(404).json({status: 'Not found'}); } - if (_.has(template, 'number_prefix') && template.number_prefix !== templateData.number_prefix) { // got new number_prefix - if (!await numberPrefixCheck(template, req, res, next)) return; - } - if (!_.isEqual(_.pick(templateData, _.keys(template)), template)) { // data was changed template.version = templateData.version + 1; // increase version await new (model(req))(_.assign({}, _.omit(templateData, ['_id', '__v']), template)).save((err, data) => { // save new template, fill with old properties if (err) next (err); - res.json(TemplateValidate.output(data.toObject(), req.params.collection)); + res.json(TemplateValidate.output(data.toObject())); }); } else { - res.json(TemplateValidate.output(templateData, req.params.collection)); + res.json(TemplateValidate.output(templateData)); } }); -router.post('/template/:collection(measurement|treatment)/new', async (req, res, next) => { +router.post('/template/:collection(measurement|condition)/new', async (req, res, next) => { if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; - const {error, value: template} = TemplateValidate.input(req.body, 'new', req.params.collection); + const {error, value: template} = TemplateValidate.input(req.body, 'new'); if (error) return res400(error, res); - if (_.has(template, 'number_prefix')) { // got number_prefix - if (!await numberPrefixCheck(template, req, res, next)) return; - } - template.version = 1; // set template version await new (model(req))(template).save((err, data) => { if (err) next (err); - res.json(TemplateValidate.output(data.toObject(), req.params.collection)); + res.json(TemplateValidate.output(data.toObject())); }); }); module.exports = router; - -async function numberPrefixCheck (template, req, res, next) { // check if number_prefix is available - const data = await model(req).findOne({number_prefix: template.number_prefix}).lean().exec().catch(err => {next(err); return false;}) as any; - if (data) { - res.status(400).json({status: 'Number prefix already taken'}); - return false - } - return true; -} - function model (req) { // return right template model - return req.params.collection === 'treatment' ? TemplateTreatmentModel : TemplateMeasurementModel; + return req.params.collection === 'condition' ? ConditionTemplateModel : MeasurementTemplateModel; } \ No newline at end of file diff --git a/src/routes/user.ts b/src/routes/user.ts index 4fb2c0f..6ebed4b 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -40,7 +40,6 @@ router.put('/user:username([/](?!key|new).?*|/?)', async (req, res, next) => { const username = getUsername(req, res); if (!username) return; - console.log(username); const {error, value: user} = UserValidate.input(req.body, 'change' + (req.authDetails.level === 'admin'? 'admin' : '')); if (error) return res400(error, res); @@ -154,8 +153,6 @@ function getUsername (req, res) { // returns username or false if action is not async function usernameCheck (name, res, next) { // check if username is already taken const userData = await UserModel.findOne({name: name}).lean().exec().catch(err => next(err)) as any; if (userData instanceof Error) return false; - console.log(userData); - console.log(UserValidate.isSpecialName(name)); if (userData || UserValidate.isSpecialName(name)) { res.status(400).json({status: 'Username already taken'}); return false; diff --git a/src/routes/validate/condition.ts b/src/routes/validate/condition.ts deleted file mode 100644 index d752ff3..0000000 --- a/src/routes/validate/condition.ts +++ /dev/null @@ -1,50 +0,0 @@ -import Joi from '@hapi/joi'; - -import IdValidate from './id'; - -export default class ConditionValidate { - private static condition = { - number: Joi.string() - .max(128), - - parameters: Joi.object() - .pattern(/.*/, Joi.alternatives() - .try( - Joi.string().max(128), - Joi.number(), - Joi.boolean(), - Joi.array() - ) - ) - } - - 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(), - parameters: this.condition.parameters.required(), - treatment_template: IdValidate.get().required() - }).validate(data); - } - else if (param === 'change') { - return Joi.object({ - parameters: this.condition.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(), - sample_id: IdValidate.get(), - number: this.condition.number, - parameters: this.condition.parameters, - treatment_template: IdValidate.get() - }).validate(data, {stripUnknown: true}); - return error !== undefined? null : value; - } -} \ No newline at end of file diff --git a/src/routes/validate/material.ts b/src/routes/validate/material.ts index c92f440..805ccd2 100644 --- a/src/routes/validate/material.ts +++ b/src/routes/validate/material.ts @@ -1,39 +1,39 @@ -import joi from '@hapi/joi'; +import Joi from '@hapi/joi'; import IdValidate from './id'; export default class MaterialValidate { // validate input for material private static material = { - name: joi.string() + name: Joi.string() .max(128), - supplier: joi.string() + supplier: Joi.string() .max(128), - group: joi.string() + group: Joi.string() .max(128), - mineral: joi.number() + mineral: Joi.number() .integer() .min(0) .max(100), - glass_fiber: joi.number() + glass_fiber: Joi.number() .integer() .min(0) .max(100), - carbon_fiber: joi.number() + carbon_fiber: Joi.number() .integer() .min(0) .max(100), - numbers: joi.array() - .items(joi.object({ - color: joi.string() + numbers: Joi.array() + .items(Joi.object({ + color: Joi.string() .max(128) .required(), - number: joi.string() + number: Joi.string() .max(128) .allow('') .required() @@ -42,7 +42,7 @@ export default class MaterialValidate { // validate input for material static input (data, param) { // validate input, set param to 'new' to make all attributes required if (param === 'new') { - return joi.object({ + return Joi.object({ name: this.material.name.required(), supplier: this.material.supplier.required(), group: this.material.group.required(), @@ -53,7 +53,7 @@ export default class MaterialValidate { // validate input for material }).validate(data); } else if (param === 'change') { - return joi.object({ + return Joi.object({ name: this.material.name, supplier: this.material.supplier, group: this.material.group, @@ -70,7 +70,7 @@ export default class MaterialValidate { // validate input for material static output (data) { // validate output and strip unwanted properties, returns null if not valid data = IdValidate.stringify(data); - const {value, error} = joi.object({ + const {value, error} = Joi.object({ _id: IdValidate.get(), name: this.material.name, supplier: this.material.supplier, @@ -82,4 +82,17 @@ export default class MaterialValidate { // validate input for material }).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, + mineral: this.material.mineral, + glass_fiber: this.material.glass_fiber, + carbon_fiber: this.material.carbon_fiber, + numbers: this.material.numbers + }); + } } \ No newline at end of file diff --git a/src/routes/validate/sample.ts b/src/routes/validate/sample.ts index 1b23cb1..93b86b1 100644 --- a/src/routes/validate/sample.ts +++ b/src/routes/validate/sample.ts @@ -1,6 +1,8 @@ import Joi from '@hapi/joi'; import IdValidate from './id'; +import UserValidate from './user'; +import MaterialValidate from './material'; export default class SampleValidate { private static sample = { @@ -17,13 +19,16 @@ export default class SampleValidate { .max(128) .allow(''), + condition: Joi.object(), + notes: Joi.object({ comment: Joi.string() - .max(512), + .max(512) + .allow(''), sample_references: Joi.array() .items(Joi.object({ - id: IdValidate.get(), + sample_id: IdValidate.get(), relation: Joi.string() .max(128) @@ -47,6 +52,7 @@ export default class SampleValidate { 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); @@ -56,6 +62,7 @@ export default class SampleValidate { 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); @@ -65,18 +72,39 @@ export default class SampleValidate { } } - static output (data) { // validate output and strip unwanted properties, returns null if not valid + static output (data, param = 'refs') { // validate output and strip unwanted properties, returns null if not valid data = IdValidate.stringify(data); - const {value, error} = Joi.object({ - _id: IdValidate.get(), - number: this.sample.number, - color: this.sample.color, - type: this.sample.type, - batch: this.sample.batch, - material_id: IdValidate.get(), - note_id: IdValidate.get().allow(null), - user_id: IdValidate.get() - }).validate(data, {stripUnknown: true}); + 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(), + note_id: IdValidate.get().allow(null), + user_id: IdValidate.get() + }; + } + 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(), + notes: this.sample.notes, + user: UserValidate.username() + } + } + else { + return null; + } + const {value, error} = Joi.object(joiObject).validate(data, {stripUnknown: true}); return error !== undefined? null : value; } } \ No newline at end of file diff --git a/src/routes/validate/template.ts b/src/routes/validate/template.ts index 571f48c..6b96a42 100644 --- a/src/routes/validate/template.ts +++ b/src/routes/validate/template.ts @@ -9,11 +9,6 @@ export default class TemplateValidate { version: Joi.number() .min(1), - number_prefix: Joi.string() - .pattern(/^[a-zA-Z]+$/) - .min(1) - .max(16), - parameters: Joi.array() .min(1) .items( @@ -43,63 +38,32 @@ export default class TemplateValidate { ) }; - static input (data, param, template) { // validate input, set param to 'new' to make all attributes required + static input (data, param) { // validate input, set param to 'new' to make all attributes required if (param === 'new') { - if (template === 'treatment') { - return Joi.object({ - name: this.template.name.required(), - number_prefix: this.template.number_prefix.required(), - parameters: this.template.parameters.required() - }).validate(data); - } - else { - return Joi.object({ - name: this.template.name.required(), - parameters: this.template.parameters.required() - }).validate(data); - } + return Joi.object({ + name: this.template.name.required(), + parameters: this.template.parameters.required() + }).validate(data); } else if (param === 'change') { - if (template === 'treatment') { - return Joi.object({ - name: this.template.name, - number_prefix: this.template.number_prefix, - parameters: this.template.parameters - }).validate(data); - } - else { - return Joi.object({ - name: this.template.name, - parameters: this.template.parameters - }).validate(data); - } + return Joi.object({ + name: this.template.name, + parameters: this.template.parameters + }).validate(data); } else { return{error: 'No parameter specified!', value: {}}; } } - static output (data, template) { // validate output and strip unwanted properties, returns null if not valid + static output (data) { // validate output and strip unwanted properties, returns null if not valid data = IdValidate.stringify(data); - let joiObject; - if (template === 'treatment') { // differentiate between measurement and treatment (has number_prefix) template - joiObject = { - _id: IdValidate.get(), - name: this.template.name, - version: this.template.version, - number_prefix: this.template.number_prefix, - parameters: this.template.parameters - }; - } - else { - joiObject = { - _id: IdValidate.get(), - name: this.template.name, - version: this.template.version, - parameters: this.template.parameters - }; - } - const {value, error} = Joi.object(joiObject).validate(data, {stripUnknown: true}); + const {value, error} = Joi.object({ + _id: IdValidate.get(), + name: this.template.name, + version: this.template.version, + parameters: this.template.parameters + }).validate(data, {stripUnknown: true}); return error !== undefined? null : value; } } \ No newline at end of file diff --git a/src/routes/validate/user.ts b/src/routes/validate/user.ts index bd4dfbd..9c0c7d1 100644 --- a/src/routes/validate/user.ts +++ b/src/routes/validate/user.ts @@ -84,4 +84,8 @@ export default class UserValidate { // validate input for user static isSpecialName (name) { // true if name belongs to special names return this.specialUsernames.indexOf(name) > -1; } + + static username() { + return this.user.name; + } } diff --git a/src/test/db.json b/src/test/db.json index b78f8e7..7760208 100644 --- a/src/test/db.json +++ b/src/test/db.json @@ -7,6 +7,11 @@ "type": "granulate", "color": "black", "batch": "", + "condition": { + "material": "copper", + "weeks": 3, + "condition_template": {"$oid":"200000000000000000000001"} + }, "material_id": {"$oid":"100000000000000000000004"}, "note_id": null, "user_id": {"$oid":"000000000000000000000002"}, @@ -19,6 +24,11 @@ "type": "granulate", "color": "natural", "batch": "1560237365", + "condition": { + "material": "copper", + "weeks": 3, + "condition_template": {"$oid":"200000000000000000000001"} + }, "material_id": {"$oid":"100000000000000000000001"}, "note_id": {"$oid":"500000000000000000000001"}, "user_id": {"$oid":"000000000000000000000002"}, @@ -31,6 +41,11 @@ "type": "part", "color": "black", "batch": "1704-005", + "condition": { + "material": "copper", + "weeks": 3, + "condition_template": {"$oid":"200000000000000000000001"} + }, "material_id": {"$oid":"100000000000000000000005"}, "note_id": {"$oid":"500000000000000000000002"}, "user_id": {"$oid":"000000000000000000000003"}, @@ -43,6 +58,11 @@ "type": "granulate", "color": "black", "batch": "1653000308", + "condition": { + "material": "hot air", + "weeks": 5, + "condition_template": {"$oid":"200000000000000000000001"} + }, "material_id": {"$oid":"100000000000000000000005"}, "note_id": {"$oid":"500000000000000000000003"}, "user_id": {"$oid":"000000000000000000000003"}, @@ -55,11 +75,27 @@ "type": "granulate", "color": "black", "batch": "1653000308", + "condition": { + "condition_template": {"$oid":"200000000000000000000003"} + }, "material_id": {"$oid":"100000000000000000000005"}, - "note_id": {"$oid":"500000000000000000000003"}, + "note_id": null, "user_id": {"$oid":"000000000000000000000003"}, "status": -1, "__v": 0 + }, + { + "_id": {"$oid":"400000000000000000000006"}, + "number": "Rng36", + "type": "granulate", + "color": "black", + "batch": "", + "condition": {}, + "material_id": {"$oid":"100000000000000000000004"}, + "note_id": null, + "user_id": {"$oid":"000000000000000000000002"}, + "status": 0, + "__v": 0 } ], "notes": [ @@ -73,7 +109,7 @@ "_id": {"$oid":"500000000000000000000002"}, "comment": "", "sample_references": [{ - "id": {"$oid":"400000000000000000000004"}, + "sample_id": {"$oid":"400000000000000000000004"}, "relation": "granulate to sample" }], "custom_fields": { @@ -85,7 +121,7 @@ "_id": {"$oid":"500000000000000000000003"}, "comment": "", "sample_references": [{ - "id": {"$oid":"400000000000000000000003"}, + "sample_id": {"$oid":"400000000000000000000003"}, "relation": "part to sample" }], "custom_fields": { @@ -234,60 +270,10 @@ "__v": 0 } ], - "conditions": [ - { - "_id": {"$oid":"700000000000000000000001"}, - "sample_id": {"$oid":"400000000000000000000001"}, - "number": "A1", - "parameters": { - "material": "copper", - "weeks": 3 - }, - "treatment_template": {"$oid":"200000000000000000000001"}, - "status": 10, - "__v": 0 - }, - { - "_id": {"$oid":"700000000000000000000002"}, - "sample_id": {"$oid":"400000000000000000000002"}, - "number": "A1", - "parameters": { - "material": "copper", - "weeks": 3 - }, - "treatment_template": {"$oid":"200000000000000000000001"}, - "status": 10, - "__v": 0 - }, - { - "_id": {"$oid":"700000000000000000000003"}, - "sample_id": {"$oid":"400000000000000000000004"}, - "number": "A1", - "parameters": { - "material": "copper", - "weeks": 3 - }, - "treatment_template": {"$oid":"200000000000000000000001"}, - "status": 10, - "__v": 0 - }, - { - "_id": {"$oid":"700000000000000000000004"}, - "sample_id": {"$oid":"400000000000000000000001"}, - "number": "A2", - "parameters": { - "material": "hot air", - "weeks": 5 - }, - "treatment_template": {"$oid":"200000000000000000000001"}, - "status": 10, - "__v": 0 - } - ], "measurements": [ { "_id": {"$oid":"800000000000000000000001"}, - "condition_id": {"$oid":"700000000000000000000001"}, + "sample_id": {"$oid":"400000000000000000000001"}, "values": { "dpt": [ [3997.12558,98.00555], @@ -301,7 +287,7 @@ }, { "_id": {"$oid":"800000000000000000000002"}, - "condition_id": {"$oid":"700000000000000000000002"}, + "sample_id": {"$oid":"400000000000000000000002"}, "values": { "weight %": 0.5, "standard deviation": 0.2 @@ -312,7 +298,7 @@ }, { "_id": {"$oid":"800000000000000000000003"}, - "condition_id": {"$oid":"700000000000000000000003"}, + "sample_id": {"$oid":"400000000000000000000003"}, "values": { "val1": 1 }, @@ -321,12 +307,11 @@ "__v": 0 } ], - "treatment_templates": [ + "condition_templates": [ { "_id": {"$oid":"200000000000000000000001"}, "name": "heat treatment", "version": 1, - "number_prefix": "A", "parameters": [ { "name": "material", @@ -351,7 +336,6 @@ "_id": {"$oid":"200000000000000000000002"}, "name": "heat treatment 2", "version": 2, - "number_prefix": "B", "parameters": [ { "name": "material", @@ -359,6 +343,14 @@ } ], "__v": 0 + }, + { + "_id": {"$oid":"200000000000000000000003"}, + "name": "raw material", + "version": 1, + "parameters": [ + ], + "__v": 0 } ], "measurement_templates": [ diff --git a/src/test/helper.ts b/src/test/helper.ts index 3983959..c724d14 100644 --- a/src/test/helper.ts +++ b/src/test/helper.ts @@ -10,6 +10,7 @@ export default class TestHelper { user: {pass: 'Xyz890*)', key: '000000000000000000001001'}, johnnydoe: {pass: 'Xyz890*)', key: '000000000000000000001004'} } + public static res = { // default responses 400: {status: 'Bad request'}, 401: {status: 'Unauthorized'}, From d004a01b69cad56a93625cd446916595daa4b659 Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Wed, 27 May 2020 17:03:03 +0200 Subject: [PATCH 2/4] adapted /measurements to use sample_id --- api/api.yaml | 1 - api/schemas.yaml | 2 +- src/index.ts | 1 + src/models/measurement.ts | 2 +- src/routes/measurement.spec.ts | 136 +++++++++++++++++++---------- src/routes/measurement.ts | 54 ++++++++---- src/routes/user.spec.ts | 4 +- src/routes/validate/measurement.ts | 5 +- src/routes/validate/parameters.ts | 5 +- src/routes/validate/template.ts | 1 - src/test/db.json | 21 +++++ 11 files changed, 160 insertions(+), 72 deletions(-) diff --git a/api/api.yaml b/api/api.yaml index c0a5441..9090378 100644 --- a/api/api.yaml +++ b/api/api.yaml @@ -54,7 +54,6 @@ tags: - name: / - name: /sample - name: /material - - name: /condition - name: /measurement - name: /template - name: /model diff --git a/api/schemas.yaml b/api/schemas.yaml index 6e1eeb7..e76cfb0 100644 --- a/api/schemas.yaml +++ b/api/schemas.yaml @@ -133,7 +133,7 @@ Measurement: allOf: - $ref: 'api.yaml#/components/schemas/_Id' properties: - condition_id: + sample_id: $ref: 'api.yaml#/components/schemas/Id' values: type: object diff --git a/src/index.ts b/src/index.ts index 1343442..55ca5ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import db from './db'; // TODO: condition values not needed on initial add // TODO: add multiple samples at once // TODO: coverage +// TODO: think about the display of deleted/new samples and validation in data and UI // tell if server is running in debug or production environment console.info(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT ====='); diff --git a/src/models/measurement.ts b/src/models/measurement.ts index 7db0a50..4282b29 100644 --- a/src/models/measurement.ts +++ b/src/models/measurement.ts @@ -9,6 +9,6 @@ const MeasurementSchema = new mongoose.Schema({ values: mongoose.Schema.Types.Mixed, measurement_template: {type: mongoose.Schema.Types.ObjectId, ref: MeasurementTemplateModel}, status: Number -}); +}, {minimize: false}); export default mongoose.model('measurement', MeasurementSchema); \ No newline at end of file diff --git a/src/routes/measurement.spec.ts b/src/routes/measurement.spec.ts index 8ca49ed..6e58290 100644 --- a/src/routes/measurement.spec.ts +++ b/src/routes/measurement.spec.ts @@ -3,7 +3,7 @@ import MeasurementModel from '../models/measurement'; import TestHelper from "../test/helper"; import globals from '../globals'; -// TODO: allow empty values +// TODO: restore measurements for m/a describe('/measurement', () => { @@ -19,7 +19,7 @@ describe('/measurement', () => { url: '/measurement/800000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 200, - res: {_id: '800000000000000000000001', condition_id: '700000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}, measurement_template: '300000000000000000000001'} + res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}, measurement_template: '300000000000000000000001'} }); }); it('returns the measurement for an API key', done => { @@ -28,7 +28,24 @@ describe('/measurement', () => { url: '/measurement/800000000000000000000001', auth: {key: 'janedoe'}, httpStatus: 200, - res: {_id: '800000000000000000000001', condition_id: '700000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}, measurement_template: '300000000000000000000001'} + res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}, measurement_template: '300000000000000000000001'} + }); + }); + it('returns deleted measurements for a maintain/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 => { @@ -64,7 +81,7 @@ describe('/measurement', () => { auth: {basic: 'janedoe'}, httpStatus: 200, req: {}, - res: {_id: '800000000000000000000001', condition_id: '700000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}, measurement_template: '300000000000000000000001'} + res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}, measurement_template: '300000000000000000000001'} }); }); it('keeps unchanged values', done => { @@ -76,7 +93,7 @@ describe('/measurement', () => { req: {values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}} }).end((err, res) => { if (err) return done(err); - should(res.body).be.eql({_id: '800000000000000000000001', condition_id: '700000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}, measurement_template: '300000000000000000000001'}); + should(res.body).be.eql({_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}, measurement_template: '300000000000000000000001'}); MeasurementModel.findById('800000000000000000000001').lean().exec((err, data: any) => { if (err) return done(err); should(data).have.property('status',globals.status.validated); @@ -93,7 +110,7 @@ describe('/measurement', () => { req: {values: {'weight %': 0.5}} }).end((err, res) => { if (err) return done(err); - should(res.body).be.eql({_id: '800000000000000000000002', condition_id: '700000000000000000000002', values: {'weight %': 0.5, 'standard deviation': 0.2}, measurement_template: '300000000000000000000002'}); + 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',globals.status.validated); @@ -110,10 +127,10 @@ describe('/measurement', () => { req: {values: {dpt: [[1,2],[3,4],[5,6]]}} }).end((err, res) => { if (err) return done(err); - should(res.body).be.eql({_id: '800000000000000000000001', condition_id: '700000000000000000000001', values: {dpt: [[1,2],[3,4],[5,6]]}, measurement_template: '300000000000000000000001'}); + should(res.body).be.eql({_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[1,2],[3,4],[5,6]]}, measurement_template: '300000000000000000000001'}); MeasurementModel.findById('800000000000000000000001').lean().exec((err, data: any) => { - should(data).have.only.keys('_id', 'condition_id', 'values', 'measurement_template', 'status', '__v'); - should(data.condition_id.toString()).be.eql('700000000000000000000001'); + 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',globals.status.new); should(data).have.property('values'); @@ -129,7 +146,17 @@ describe('/measurement', () => { auth: {basic: 'janedoe'}, httpStatus: 200, req: {values: {'weight %': 0.9}}, - res: {_id: '800000000000000000000002', condition_id: '700000000000000000000002', values: {'weight %': 0.9, 'standard deviation': 0.2}, measurement_template: '300000000000000000000002'} + 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 => { @@ -149,7 +176,7 @@ describe('/measurement', () => { auth: {basic: 'admin'}, httpStatus: 400, req: {values: {val1: 4}}, - res: {status: 'Invalid body format', details: '"val1" must be one of [1, 2, 3]'} + res: {status: 'Invalid body format', details: '"val1" must be one of [1, 2, 3, null]'} }); }); it('rejects a value below minimum range', done => { @@ -182,6 +209,16 @@ describe('/measurement', () => { 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', @@ -198,7 +235,7 @@ describe('/measurement', () => { auth: {basic: 'admin'}, httpStatus: 200, req: {values: {'weight %': 0.9, 'standard deviation': 0.3}}, - res: {_id: '800000000000000000000002', condition_id: '700000000000000000000002', values: {'weight %': 0.9, 'standard deviation': 0.3}, measurement_template: '300000000000000000000002'} + res: {_id: '800000000000000000000002', sample_id: '400000000000000000000002', values: {'weight %': 0.9, 'standard deviation': 0.3}, measurement_template: '300000000000000000000002'} }); }); it('rejects an invalid id', done => { @@ -327,12 +364,12 @@ describe('/measurement', () => { url: '/measurement/new', auth: {basic: 'janedoe'}, httpStatus: 200, - req: {condition_id: '700000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} + 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', 'condition_id', 'values', 'measurement_template'); + 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('condition_id', '700000000000000000000001'); + 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); @@ -346,13 +383,13 @@ describe('/measurement', () => { url: '/measurement/new', auth: {basic: 'janedoe'}, httpStatus: 200, - req: {condition_id: '700000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} + 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', 'condition_id', 'values', 'measurement_template', 'status', '__v'); - should(data.condition_id.toString()).be.eql('700000000000000000000001'); + 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', 0); should(data).have.property('values'); @@ -362,24 +399,24 @@ describe('/measurement', () => { }); }); }); - it('rejects an invalid condition id', done => { + it('rejects an invalid sample id', done => { TestHelper.request(server, done, { method: 'post', url: '/measurement/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {condition_id: '700000000000h00000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}, - res: {status: 'Invalid body format', details: '"condition_id" with value "700000000000h00000000001" fails to match the required pattern: /[0-9a-f]{24}/'} + req: {sample_id: '400000000000h00000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}, + res: {status: 'Invalid body format', details: '"sample_id" with value "400000000000h00000000001" fails to match the required pattern: /[0-9a-f]{24}/'} }); }); - it('rejects a condition id not available', done => { + it('rejects a sample id not available', done => { TestHelper.request(server, done, { method: 'post', url: '/measurement/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {condition_id: '000000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}, - res: {status: 'Condition id not available'} + 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 => { @@ -388,7 +425,7 @@ describe('/measurement', () => { url: '/measurement/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {condition_id: '700000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '30000000000h000000000002'}, + req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '30000000000h000000000002'}, res: {status: 'Invalid body format', details: '"measurement_template" with value "30000000000h000000000002" fails to match the required pattern: /[0-9a-f]{24}/'} }); }); @@ -398,7 +435,7 @@ describe('/measurement', () => { url: '/measurement/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {condition_id: '700000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '000000000000000000000002'}, + req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '000000000000000000000002'}, res: {status: 'Measurement template not available'} }); }); @@ -408,18 +445,27 @@ describe('/measurement', () => { url: '/measurement/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {condition_id: '700000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1, xx: 44}, measurement_template: '300000000000000000000002'}, + 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('rejects missing values', done => { + it('accepts missing values', done => { TestHelper.request(server, done, { method: 'post', url: '/measurement/new', auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {condition_id: '700000000000000000000001', values: {'weight %': 0.8}, measurement_template: '300000000000000000000002'}, - res: {status: 'Invalid body format', details: '"standard deviation" is required'} + 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 a value not in the value range', done => { @@ -428,8 +474,8 @@ describe('/measurement', () => { url: '/measurement/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {condition_id: '700000000000000000000001', values: {val1: 4}, measurement_template: '300000000000000000000003'}, - res: {status: 'Invalid body format', details: '"val1" must be one of [1, 2, 3]'} + req: {sample_id: '400000000000000000000001', values: {val1: 4}, measurement_template: '300000000000000000000003'}, + res: {status: 'Invalid body format', details: '"val1" must be one of [1, 2, 3, null]'} }); }); it('rejects a value below minimum range', done => { @@ -438,7 +484,7 @@ describe('/measurement', () => { url: '/measurement/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {condition_id: '700000000000000000000001', values: {'weight %': -1, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}, + req: {sample_id: '400000000000000000000001', values: {'weight %': -1, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}, res: {status: 'Invalid body format', details: '"weight %" must be larger than or equal to 0'} }); }); @@ -448,18 +494,18 @@ describe('/measurement', () => { url: '/measurement/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {condition_id: '700000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 2}, measurement_template: '300000000000000000000002'}, + 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 condition id', done => { + 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: '"condition_id" is required'} + res: {status: 'Invalid body format', details: '"sample_id" is required'} }); }); it('rejects a missing measurement_template', done => { @@ -468,7 +514,7 @@ describe('/measurement', () => { url: '/measurement/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {condition_id: '700000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}}, + req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}}, res: {status: 'Invalid body format', details: '"measurement_template" is required'} }); }); @@ -478,7 +524,7 @@ describe('/measurement', () => { url: '/measurement/new', auth: {basic: 'janedoe'}, httpStatus: 403, - req: {condition_id: '700000000000000000000003', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} + 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 maintain/admin user', done => { @@ -487,12 +533,12 @@ describe('/measurement', () => { url: '/measurement/new', auth: {basic: 'admin'}, httpStatus: 200, - req: {condition_id: '700000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} + 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', 'condition_id', 'values', 'measurement_template'); + 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('condition_id', '700000000000000000000001'); + 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); @@ -506,7 +552,7 @@ describe('/measurement', () => { url: '/measurement/new', auth: {key: 'janedoe'}, httpStatus: 401, - req: {condition_id: '700000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} + req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} }); }); it('rejects requests from a read user', done => { @@ -515,7 +561,7 @@ describe('/measurement', () => { url: '/measurement/new', auth: {basic: 'user'}, httpStatus: 403, - req: {condition_id: '700000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} + req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} }); }); it('rejects unauthorized requests', done => { @@ -523,7 +569,7 @@ describe('/measurement', () => { method: 'post', url: '/measurement/new', httpStatus: 401, - req: {condition_id: '700000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} + 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 b9af125..78b7ec1 100644 --- a/src/routes/measurement.ts +++ b/src/routes/measurement.ts @@ -3,6 +3,7 @@ import _ from 'lodash'; import MeasurementModel from '../models/measurement'; import MeasurementTemplateModel from '../models/measurement_template'; +import SampleModel from '../models/sample'; import MeasurementValidate from './validate/measurement'; import IdValidate from './validate/id'; import res400 from './validate/res400'; @@ -15,11 +16,12 @@ const router = express.Router(); router.get('/measurement/' + IdValidate.parameter(), (req, res, next) => { if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; - MeasurementModel.findById(req.params.id).lean().exec((err, data) => { + 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'}); } + if (data.status ===globals.status.deleted && !req.auth(res, ['maintain', 'admin'], 'all')) return; // deleted measurements only available for maintain/admin res.json(MeasurementValidate.output(data)); }); @@ -37,13 +39,13 @@ router.put('/measurement/' + IdValidate.parameter(), async (req, res, next) => { res.status(404).json({status: 'Not found'}); } - // add properties needed for conditionIdCheck + // add properties needed for sampleIdCheck measurement.measurement_template = data.measurement_template; - measurement.condition_id = data.condition_id; - if (!await conditionIdCheck(measurement, req, res, next)) return; + measurement.sample_id = data.sample_id; + if (!await sampleIdCheck(measurement, req, res, next)) return; // check for changes - if (measurement.values) { + 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 @@ -53,6 +55,7 @@ router.put('/measurement/' + IdValidate.parameter(), async (req, res, next) => { if (!await templateCheck(measurement, 'change', res, next)) return; await MeasurementModel.findByIdAndUpdate(req.params.id, measurement, {new: true}).lean().exec((err, data) => { if (err) return next(err); + console.log(data); res.json(MeasurementValidate.output(data)); }); }); @@ -65,7 +68,7 @@ router.delete('/measurement/' + IdValidate.parameter(), (req, res, next) => { if (!data) { res.status(404).json({status: 'Not found'}); } - if (!await conditionIdCheck(data, req, res, next)) return; + if (!await sampleIdCheck(data, req, res, next)) return; await MeasurementModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).lean().exec(err => { if (err) return next(err); res.json({status: 'OK'}); @@ -79,12 +82,14 @@ router.post('/measurement/new', async (req, res, next) => { const {error, value: measurement} = MeasurementValidate.input(req.body, 'new'); if (error) return res400(error, res); - if (!await conditionIdCheck(measurement, req, res, next)) return; - if (!await templateCheck(measurement, 'new', res, next)) return; + if (!await sampleIdCheck(measurement, req, res, next)) return; + measurement.values = await templateCheck(measurement, 'new', res, next); + if (!measurement.values) return; measurement.status = 0; await new MeasurementModel(measurement).save((err, data) => { if (err) return next(err); + console.log(data); res.json(MeasurementValidate.output(data.toObject())); }); }); @@ -93,25 +98,38 @@ router.post('/measurement/new', async (req, res, next) => { module.exports = router; -async function conditionIdCheck (measurement, req, res, next) { // validate condition_id, returns false if invalid // TODO - // const sampleData = await ConditionModel.findById(measurement.condition_id).populate('sample_id').lean().exec().catch(err => {next(err); return false;}) as any; - // if (!sampleData) { // sample_id not found - // res.status(400).json({status: 'Condition id not available'}); +async function sampleIdCheck (measurement, req, res, next) { // validate sample_id, returns false if invalid or user has no access for this sample + 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 - // } - // if (sampleData.sample_id.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return false; // sample does not belong to user - // return true; + } + if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return false; // sample does not belong to user + return true; } -async function templateCheck (measurement, param, res, next) { // validate measurement_template and values, param for new/change +async function templateCheck (measurement, param, res, next) { // validate measurement_template and values, returns values, true if values are {} or false if invalid, param for 'new'/'change' const templateData = await MeasurementTemplateModel.findById(measurement.measurement_template).lean().exec().catch(err => {next(err); return false;}) as any; if (!templateData) { // template not found res.status(400).json({status: 'Measurement template not available'}); return false } + // fill not given values for new measurements + if (param === 'new') { + 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: ignore} = ParametersValidate.input(measurement.values, templateData.parameters, param); + const {error, value} = ParametersValidate.input(measurement.values, templateData.parameters, 'null'); if (error) {res400(error, res); return false;} - return true; + return value || true; } \ No newline at end of file diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts index 6a7d69e..a7a2ddb 100644 --- a/src/routes/user.spec.ts +++ b/src/routes/user.spec.ts @@ -288,7 +288,7 @@ describe('/user', () => { auth: {basic: 'admin'}, httpStatus: 400, req: {pass: 'password'}, - res: {status: 'Invalid body format', details: '"pass" with value "password" fails to match the required pattern: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$/'} + res: {status: 'Invalid body format', details: '"pass" with value "password" fails to match the required pattern: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$)[a-zA-Z0-9!"#%&\'()*+,\\-.\\/:;<=>?@[\\]^_`{|}~]{8,}$/'} }); }); it('rejects requests from non-admins for another user', done => { @@ -546,7 +546,7 @@ describe('/user', () => { auth: {basic: 'admin'}, httpStatus: 400, req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'password', level: 'read', location: 'Rng', device_name: 'Alpha II'}, - res: {status: 'Invalid body format', details: '"pass" with value "password" fails to match the required pattern: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$/'} + res: {status: 'Invalid body format', details: '"pass" with value "password" fails to match the required pattern: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$)[a-zA-Z0-9!"#%&\'()*+,\\-.\\/:;<=>?@[\\]^_`{|}~]{8,}$/'} }); }); it('rejects requests from non-admins', done => { diff --git a/src/routes/validate/measurement.ts b/src/routes/validate/measurement.ts index 21b38a2..74c2409 100644 --- a/src/routes/validate/measurement.ts +++ b/src/routes/validate/measurement.ts @@ -12,13 +12,14 @@ export default class MeasurementValidate { Joi.boolean(), 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({ - condition_id: IdValidate.get().required(), + sample_id: IdValidate.get().required(), values: this.measurement.values.required(), measurement_template: IdValidate.get().required() }).validate(data); @@ -37,7 +38,7 @@ export default class MeasurementValidate { data = IdValidate.stringify(data); const {value, error} = Joi.object({ _id: IdValidate.get(), - condition_id: IdValidate.get(), + sample_id: IdValidate.get(), values: this.measurement.values, measurement_template: IdValidate.get() }).validate(data, {stripUnknown: true}); diff --git a/src/routes/validate/parameters.ts b/src/routes/validate/parameters.ts index 79e62ef..e6070b0 100644 --- a/src/routes/validate/parameters.ts +++ b/src/routes/validate/parameters.ts @@ -1,7 +1,7 @@ import Joi from '@hapi/joi'; export default class ParametersValidate { - static input (data, parameters, param) { // data to validate, parameters from template, param: 'new', 'change' + static input (data, parameters, param) { // data to validate, parameters from template, param: 'new', 'change', 'null'(null values are allowed) let joiObject = {}; parameters.forEach(parameter => { if (parameter.range.hasOwnProperty('values')) { // append right validation method according to parameter @@ -39,6 +39,9 @@ export default class ParametersValidate { if (param === 'new') { joiObject[parameter.name] = joiObject[parameter.name].required() } + else if (param === 'null') { + joiObject[parameter.name] = joiObject[parameter.name].allow(null) + } }); return Joi.object(joiObject).validate(data); } diff --git a/src/routes/validate/template.ts b/src/routes/validate/template.ts index 6b96a42..111951e 100644 --- a/src/routes/validate/template.ts +++ b/src/routes/validate/template.ts @@ -10,7 +10,6 @@ export default class TemplateValidate { .min(1), parameters: Joi.array() - .min(1) .items( Joi.object({ name: Joi.string() diff --git a/src/test/db.json b/src/test/db.json index 7760208..372b09a 100644 --- a/src/test/db.json +++ b/src/test/db.json @@ -305,6 +305,27 @@ "status": 0, "measurement_template": {"$oid":"300000000000000000000003"}, "__v": 0 + }, + { + "_id": {"$oid":"800000000000000000000004"}, + "sample_id": {"$oid":"400000000000000000000003"}, + "values": { + "val1": 1 + }, + "status": -1, + "measurement_template": {"$oid":"300000000000000000000003"}, + "__v": 0 + }, + { + "_id": {"$oid":"800000000000000000000005"}, + "sample_id": {"$oid":"400000000000000000000002"}, + "values": { + "weight %": 0.5, + "standard deviation":null + }, + "status": 10, + "measurement_template": {"$oid":"300000000000000000000002"}, + "__v": 0 } ], "condition_templates": [ From 0ea28fa50a376d5b7525380409b7005223645ac4 Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Thu, 28 May 2020 11:47:51 +0200 Subject: [PATCH 3/4] implemented code coverage --- package.json | 2 +- src/db.ts | 25 +++++++++++++++------ src/index.ts | 1 + src/routes/material.spec.ts | 2 +- src/routes/measurement.spec.ts | 5 ++++- src/routes/measurement.ts | 8 +++---- src/routes/root.spec.ts | 40 +++++++++++++++++++++++++++++++++- src/routes/sample.spec.ts | 3 ++- src/routes/template.spec.ts | 1 + src/routes/template.ts | 2 +- src/routes/user.spec.ts | 1 + src/test/helper.ts | 7 ++++++ 12 files changed, 79 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 5763fdc..6e7f289 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "start": "tsc && node dist/index.js || exit 1", "dev": "nodemon -e ts,yaml --exec \"npm run start\"", "loadDev": "node dist/test/loadDev.js", - "coverage": "nyc --reporter=html --reporter=tex mocha dist/**/**.spec.js" + "coverage": "tsc && nyc --reporter=html --reporter=text mocha dist/**/**.spec.js --timeout 5000" }, "keywords": [], "author": "", diff --git a/src/db.ts b/src/db.ts index c1d1fbb..fb5d424 100644 --- a/src/db.ts +++ b/src/db.ts @@ -42,15 +42,18 @@ export default class db { }); mongoose.connection.on('error', console.error.bind(console, 'connection error:')); mongoose.connection.on('disconnected', () => { // reset state on disconnect - console.info('Database disconnected'); - this.state.db = 0; - done(); + if (process.env.NODE_ENV !== 'test') { // Do not interfere with testing + console.info('Database disconnected'); + this.state.db = 0; + } }); process.on('SIGINT', () => { // close connection when app is terminated - mongoose.connection.close(() => { - console.info('Mongoose default connection disconnected through app termination'); - process.exit(0); - }); + if (!this.state.db) { // database still connected + mongoose.connection.close(() => { + console.info('Mongoose default connection disconnected through app termination'); + process.exit(0); + }); + } }); mongoose.connection.once('open', () => { mongoose.set('useFindAndModify', false); @@ -60,6 +63,14 @@ export default class db { }); } + static disconnect (done) { + mongoose.connection.close(() => { + console.info(process.env.NODE_ENV === 'test' ? '' : `Disconnected from database`); + this.state.db = 0; + done(); + }); + } + static getState () { return this.state; } diff --git a/src/index.ts b/src/index.ts index 55ca5ff..c007ca9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ import db from './db'; // TODO: add multiple samples at once // TODO: coverage // TODO: think about the display of deleted/new samples and validation in data and UI +// TODO: improve error coverage // tell if server is running in debug or production environment console.info(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT ====='); diff --git a/src/routes/material.spec.ts b/src/routes/material.spec.ts index 344642d..ae8d305 100644 --- a/src/routes/material.spec.ts +++ b/src/routes/material.spec.ts @@ -12,6 +12,7 @@ describe('/material', () => { 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 => { @@ -146,7 +147,6 @@ describe('/material', () => { } }); }); - done(); }); }); it('rejects requests from a write user', done => { diff --git a/src/routes/measurement.spec.ts b/src/routes/measurement.spec.ts index 6e58290..e1f36a4 100644 --- a/src/routes/measurement.spec.ts +++ b/src/routes/measurement.spec.ts @@ -5,14 +5,17 @@ import globals from '../globals'; // TODO: restore measurements for m/a +// TODO: coverage!!! + 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)); - describe('GET /mesurement/{id}', () => { + describe('GET /measurement/{id}', () => { it('returns the right measurement', done => { TestHelper.request(server, done, { method: 'get', diff --git a/src/routes/measurement.ts b/src/routes/measurement.ts index 78b7ec1..0d0f0f6 100644 --- a/src/routes/measurement.ts +++ b/src/routes/measurement.ts @@ -36,7 +36,7 @@ router.put('/measurement/' + IdValidate.parameter(), async (req, res, next) => { const data = await MeasurementModel.findById(req.params.id).lean().exec().catch(err => {next(err);}) as any; if (data instanceof Error) return; if (!data) { - res.status(404).json({status: 'Not found'}); + return res.status(404).json({status: 'Not found'}); } // add properties needed for sampleIdCheck @@ -55,7 +55,6 @@ router.put('/measurement/' + IdValidate.parameter(), async (req, res, next) => { if (!await templateCheck(measurement, 'change', res, next)) return; await MeasurementModel.findByIdAndUpdate(req.params.id, measurement, {new: true}).lean().exec((err, data) => { if (err) return next(err); - console.log(data); res.json(MeasurementValidate.output(data)); }); }); @@ -66,12 +65,12 @@ router.delete('/measurement/' + IdValidate.parameter(), (req, res, next) => { MeasurementModel.findById(req.params.id).lean().exec(async (err, data) => { if (err) return next(err); if (!data) { - res.status(404).json({status: 'Not found'}); + 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.deleted}).lean().exec(err => { if (err) return next(err); - res.json({status: 'OK'}); + return res.json({status: 'OK'}); }); }); }); @@ -89,7 +88,6 @@ router.post('/measurement/new', async (req, res, next) => { measurement.status = 0; await new MeasurementModel(measurement).save((err, data) => { if (err) return next(err); - console.log(data); res.json(MeasurementValidate.output(data.toObject())); }); }); diff --git a/src/routes/root.spec.ts b/src/routes/root.spec.ts index f8a803f..a5f8f8b 100644 --- a/src/routes/root.spec.ts +++ b/src/routes/root.spec.ts @@ -1,4 +1,5 @@ import TestHelper from "../test/helper"; +import db from '../db'; describe('/', () => { @@ -6,6 +7,7 @@ describe('/', () => { 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 => { @@ -40,7 +42,15 @@ describe('/', () => { TestHelper.request(server, done, { method: 'get', url: '/authorized', - auth: {name: 'admin', pass: 'Abc123!!'}, + 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 }); }); @@ -66,4 +76,32 @@ describe('/', () => { }); }); }); + + 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', () => { + it('resolves to an 500 error', done => { + db.disconnect(() => { + TestHelper.request(server, done, { + method: 'get', + url: '/', + httpStatus: 500 + }); + }); + }); + }); + + // describe('API') // TODO not in production }); \ No newline at end of file diff --git a/src/routes/sample.spec.ts b/src/routes/sample.spec.ts index f54f5b4..cfeeb7c 100644 --- a/src/routes/sample.spec.ts +++ b/src/routes/sample.spec.ts @@ -19,6 +19,7 @@ describe('/sample', () => { 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 => { @@ -197,7 +198,7 @@ describe('/sample', () => { }); }); - it('returns a deleted sample for a maintain/admin user', done => { // TODO: make tests work + it('returns a deleted sample for a maintain/admin user', done => { TestHelper.request(server, done, { method: 'get', url: '/sample/400000000000000000000005', diff --git a/src/routes/template.spec.ts b/src/routes/template.spec.ts index 2ac09ae..54adfcb 100644 --- a/src/routes/template.spec.ts +++ b/src/routes/template.spec.ts @@ -13,6 +13,7 @@ describe('/template', () => { 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', () => { diff --git a/src/routes/template.ts b/src/routes/template.ts index f4054c1..849cf59 100644 --- a/src/routes/template.ts +++ b/src/routes/template.ts @@ -44,7 +44,7 @@ router.put('/template/:collection(measurement|condition)/' + IdValidate.paramete const templateData = await model(req).findById(req.params.id).lean().exec().catch(err => {next(err);}) as any; if (templateData instanceof Error) return; if (!templateData) { - res.status(404).json({status: 'Not found'}); + return res.status(404).json({status: 'Not found'}); } if (!_.isEqual(_.pick(templateData, _.keys(template)), template)) { // data was changed diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts index a7a2ddb..a0d67a5 100644 --- a/src/routes/user.spec.ts +++ b/src/routes/user.spec.ts @@ -9,6 +9,7 @@ describe('/user', () => { 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 => { diff --git a/src/test/helper.ts b/src/test/helper.ts index c724d14..539eba3 100644 --- a/src/test/helper.ts +++ b/src/test/helper.ts @@ -39,6 +39,10 @@ export default class TestHelper { server.close(done); } + static after(done) { + db.disconnect(done); + } + static request (server, done, options) { // options in form: {method, url, auth: {key/basic: 'name' or 'key'/{name, pass}}, httpStatus, req, res} let st = supertest(server); if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('key')) { // resolve API key @@ -58,6 +62,9 @@ export default class TestHelper { 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); } From 8276e5108c86e92739ffd830b056a3884ce35cf3 Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Thu, 28 May 2020 12:18:38 +0200 Subject: [PATCH 4/4] /api/ subroutes only available in dev/test --- src/index.ts | 10 +++++---- src/models/measurement.ts | 2 +- src/routes/measurement.spec.ts | 2 -- src/routes/root.spec.ts | 37 ++++++++++++++++++++++++++++++++-- src/test/helper.ts | 2 +- 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index c007ca9..0de6ff4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,10 +52,12 @@ app.use((req, res, next) => { // no database connection error app.use(require('./helpers/authorize')); // handle authentication // redirect /api routes for Angular proxy in development -app.use('/api/:url', (req, res) => { - req.url = '/' + req.params.url; - app.handle(req, res); -}); +if (process.env.NODE_ENV !== 'production') { + app.use('/api/:url', (req, res) => { + req.url = '/' + req.params.url; + app.handle(req, res); + }); +} // require routes diff --git a/src/models/measurement.ts b/src/models/measurement.ts index 4282b29..d003ea5 100644 --- a/src/models/measurement.ts +++ b/src/models/measurement.ts @@ -2,7 +2,7 @@ import mongoose from 'mongoose'; import SampleModel from './sample'; import MeasurementTemplateModel from './measurement_template'; -// TODO: change to sample_id + const MeasurementSchema = new mongoose.Schema({ sample_id: {type: mongoose.Schema.Types.ObjectId, ref: SampleModel}, diff --git a/src/routes/measurement.spec.ts b/src/routes/measurement.spec.ts index e1f36a4..5af91a3 100644 --- a/src/routes/measurement.spec.ts +++ b/src/routes/measurement.spec.ts @@ -5,8 +5,6 @@ import globals from '../globals'; // TODO: restore measurements for m/a -// TODO: coverage!!! - describe('/measurement', () => { let server; diff --git a/src/routes/root.spec.ts b/src/routes/root.spec.ts index a5f8f8b..569af8b 100644 --- a/src/routes/root.spec.ts +++ b/src/routes/root.spec.ts @@ -91,7 +91,7 @@ describe('/', () => { }); }); - describe('A not connected database', () => { + describe('A not connected database', () => { // RUN AS LAST OR RECONNECT DATABASE!! it('resolves to an 500 error', done => { db.disconnect(() => { TestHelper.request(server, done, { @@ -102,6 +102,39 @@ describe('/', () => { }); }); }); +}); - // describe('API') // TODO not in production +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)); + + + 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'} + }); + }); + 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 diff --git a/src/test/helper.ts b/src/test/helper.ts index 539eba3..fbb45ff 100644 --- a/src/test/helper.ts +++ b/src/test/helper.ts @@ -43,7 +43,7 @@ export default class TestHelper { db.disconnect(done); } - static request (server, done, options) { // options in form: {method, url, auth: {key/basic: 'name' or 'key'/{name, pass}}, httpStatus, req, res} + static request (server, done, options) { // options in form: {method, url, auth: {key/basic: 'name' or 'key'/{name, pass}}, httpStatus, req, res, default (set to false if you want to dismiss default .end handling)} 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);