From 7fd378fe027443e2ffbe180bf86ae750a6ae8c3c Mon Sep 17 00:00:00 2001 From: Antariksh Mahajan Date: Tue, 24 Nov 2020 14:05:01 +0800 Subject: [PATCH 01/34] docs: add script for unlisting array of forms (#714) --- docs/MONGODB.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/MONGODB.md b/docs/MONGODB.md index 6d908bdd6b..70548bca43 100644 --- a/docs/MONGODB.md +++ b/docs/MONGODB.md @@ -35,6 +35,34 @@ function unlistFormFromExamples(id) { // unlistFormFromExamples("form id") ``` +```javascript +/** + * Unlist an array of forms from the examples page + * @param {String[]} ids of the forms. These can be any string + * where the first sequence of 24 consecutive alphanumerics is the + * form ID, e.g. the form link. + */ +function unlistFormArrayFromExamples(ids) { + const formIdRegex = /[0-9a-fA-F]{24}/ + // Wrap all ids in ObjectId + const objectIds = [] + for (let id of ids) { + const parsed = formIdRegex.exec(id) + if (parsed) { + objectIds.push(ObjectId(parsed[0])) + } else { + throw new Error(`${id} does not contain a valid form ID.`) + } + } + const result = db + .getCollection('forms') + .updateMany({ _id: { $in: objectIds } }, { $set: { isListed: false } }) + return `${ids.length} forms given, ${result.matchedCount} matched, ${result.modifiedCount} modified` +} +// const ids = ['https://some.url/5f8817b7bde9d4002a800d78', 'some.url/#!/5f9a587b119c07002b56124f'] +// unlistFormArrayFromExamples(ids) +``` + - **Create new agency** ```javascript From 8e13a248e5646af45540b964bc4ac6b181a751bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Nov 2020 18:10:37 +0800 Subject: [PATCH 02/34] chore(deps-dev): bump lint-staged from 10.5.1 to 10.5.2 (#722) Bumps [lint-staged](https://github.com/okonet/lint-staged) from 10.5.1 to 10.5.2. - [Release notes](https://github.com/okonet/lint-staged/releases) - [Commits](https://github.com/okonet/lint-staged/compare/v10.5.1...v10.5.2) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index d8013a3670..1bfe397d97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16862,9 +16862,9 @@ "dev": true }, "lint-staged": { - "version": "10.5.1", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.5.1.tgz", - "integrity": "sha512-fTkTGFtwFIJJzn/PbUO3RXyEBHIhbfYBE7+rJyLcOXabViaO/h6OslgeK6zpeUtzkDrzkgyAYDTLAwx6JzDTHw==", + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.5.2.tgz", + "integrity": "sha512-e8AYR1TDlzwB8VVd38Xu2lXDZf6BcshVqKVuBQThDJRaJLobqKnpbm4dkwJ2puypQNbLr9KF/9mfA649mAGvjA==", "dev": true, "requires": { "chalk": "^4.1.0", @@ -16949,9 +16949,9 @@ } }, "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { "ms": "2.1.2" diff --git a/package.json b/package.json index e96c0f98c6..d99ee86b07 100644 --- a/package.json +++ b/package.json @@ -226,7 +226,7 @@ "jasmine-sinon": "^0.4.0", "jasmine-spec-reporter": "^6.0.0", "jest": "^26.6.2", - "lint-staged": "^10.5.1", + "lint-staged": "^10.5.2", "maildev": "^1.1.0", "mini-css-extract-plugin": "^0.5.0", "mockdate": "^3.0.2", From a99fd945225dfdf2ca4b0d11f8f8dd2865a9f708 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Nov 2020 18:10:56 +0800 Subject: [PATCH 03/34] chore(deps-dev): bump stylelint from 13.6.1 to 13.8.0 (#721) Bumps [stylelint](https://github.com/stylelint/stylelint) from 13.6.1 to 13.8.0. - [Release notes](https://github.com/stylelint/stylelint/releases) - [Changelog](https://github.com/stylelint/stylelint/blob/master/CHANGELOG.md) - [Commits](https://github.com/stylelint/stylelint/compare/13.6.1...13.8.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 803 ++++++++++++++++++++++++++-------------------- package.json | 2 +- 2 files changed, 453 insertions(+), 352 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1bfe397d97..1149753068 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4238,22 +4238,22 @@ "integrity": "sha512-s/wTc/3+vYSalh4gfayJrupzhT7SDBqNtiYOeEMlkSDqL/8cExh5FAeTzLpmYq+7BLLv36EjBL5xrb0bUHWJWQ==" }, "@stylelint/postcss-css-in-js": { - "version": "0.37.1", - "resolved": "https://registry.npmjs.org/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.1.tgz", - "integrity": "sha512-UMf2Rni3JGKi3ZwYRGMYJ5ipOA5ENJSKMtYA/pE1ZLURwdh7B5+z2r73RmWvub+N0UuH1Lo+TGfCgYwPvqpXNw==", + "version": "0.37.2", + "resolved": "https://registry.npmjs.org/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz", + "integrity": "sha512-nEhsFoJurt8oUmieT8qy4nk81WRHmJynmVwn/Vts08PL9fhgIsMhk1GId5yAN643OzqEEb5S/6At2TZW7pqPDA==", "dev": true, "requires": { "@babel/core": ">=7.9.0" } }, "@stylelint/postcss-markdown": { - "version": "0.36.1", - "resolved": "https://registry.npmjs.org/@stylelint/postcss-markdown/-/postcss-markdown-0.36.1.tgz", - "integrity": "sha512-iDxMBWk9nB2BPi1VFQ+Dc5+XpvODBHw2n3tYpaBZuEAFQlbtF9If0Qh5LTTwSi/XwdbJ2jt+0dis3i8omyggpw==", + "version": "0.36.2", + "resolved": "https://registry.npmjs.org/@stylelint/postcss-markdown/-/postcss-markdown-0.36.2.tgz", + "integrity": "sha512-2kGbqUVJUGE8dM+bMzXG/PYUWKkjLIkRLWNh39OaADkiabDRdw8ATFCgbMz5xdIcvwspPAluSL7uY+ZiTWdWmQ==", "dev": true, "requires": { - "remark": "^12.0.0", - "unist-util-find-all-after": "^3.0.1" + "remark": "^13.0.0", + "unist-util-find-all-after": "^3.0.2" } }, "@types/angular": { @@ -4599,6 +4599,15 @@ "integrity": "sha512-8gacRfEqLrmZ6KofpFfxyjsm/LYepeWUWUJGaf5A9W9J5B2/dRZMdkDqFDL6YDa9IweH12IO76jO7mpsK2B3wg==", "dev": true }, + "@types/mdast": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.3.tgz", + "integrity": "sha512-SXPBMnFVQg1s00dlMCc/jCdvPqdE4mXaMMCeRlxLDmTAEoegHT53xKtkDnzDTOcmMHUfcjyf36/YYZ6SxRdnsw==", + "dev": true, + "requires": { + "@types/unist": "*" + } + }, "@types/mime": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.2.tgz", @@ -4611,9 +4620,9 @@ "dev": true }, "@types/minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz", + "integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==", "dev": true }, "@types/mkdirp": { @@ -5724,9 +5733,9 @@ "dev": true }, "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, "asap": { @@ -5905,20 +5914,26 @@ } }, "autoprefixer": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.0.tgz", - "integrity": "sha512-D96ZiIHXbDmU02dBaemyAg53ez+6F5yZmapmgKcjm35yEe1uVDYI8hGW3VYoGRaG290ZFf91YxHrR518vC0u/A==", + "version": "9.8.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", + "integrity": "sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==", "dev": true, "requires": { "browserslist": "^4.12.0", - "caniuse-lite": "^1.0.30001061", - "chalk": "^2.4.2", + "caniuse-lite": "^1.0.30001109", + "colorette": "^1.2.1", "normalize-range": "^0.1.2", "num2fraction": "^1.2.2", - "postcss": "^7.0.30", + "postcss": "^7.0.32", "postcss-value-parser": "^4.1.0" }, "dependencies": { + "caniuse-lite": { + "version": "1.0.30001159", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001159.tgz", + "integrity": "sha512-w9Ph56jOsS8RL20K9cLND3u/+5WASWdhC/PPrf+V3/HsM3uHOavWOR1Xzakbv4Puo/srmPHudkmCRWM7Aq+/UA==", + "dev": true + }, "postcss-value-parser": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", @@ -8002,12 +8017,6 @@ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, - "ccount": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.0.5.tgz", - "integrity": "sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw==", - "dev": true - }, "celebrate": { "version": "13.0.3", "resolved": "https://registry.npmjs.org/celebrate/-/celebrate-13.0.3.tgz", @@ -8063,12 +8072,6 @@ "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", "dev": true }, - "character-entities-html4": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz", - "integrity": "sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==", - "dev": true - }, "character-entities-legacy": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", @@ -8432,12 +8435,6 @@ "integrity": "sha512-J2jRPX0eeFh5VKyVnoLrfVFgLZtnnmp96WQSLAS8OrLm2wtQLcnikYKe1gViJKDH7vucjuhHvBKKBP3rKcD1tQ==", "dev": true }, - "collapse-white-space": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", - "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==", - "dev": true - }, "collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -8485,6 +8482,12 @@ "simple-swizzle": "^0.2.2" } }, + "colorette": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", + "dev": true + }, "colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", @@ -9150,27 +9153,27 @@ } }, "cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", "dev": true, "requires": { "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", + "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", - "yaml": "^1.7.2" + "yaml": "^1.10.0" }, "dependencies": { "parse-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", - "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1", + "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, @@ -10342,6 +10345,41 @@ } } }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "requires": { + "domelementtype": "1" + }, + "dependencies": { + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + } + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + }, + "dependencies": { + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + } + } + }, "dot-prop": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", @@ -11878,6 +11916,12 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" }, + "fastest-levenshtein": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", + "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", + "dev": true + }, "fastparse": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", @@ -13236,6 +13280,45 @@ } } }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -13773,12 +13856,6 @@ "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", "dev": true }, - "is-alphanumeric": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz", - "integrity": "sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ=", - "dev": true - }, "is-alphanumerical": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", @@ -14106,24 +14183,12 @@ "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", "dev": true }, - "is-whitespace-character": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", - "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==", - "dev": true - }, "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-word-character": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz", - "integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==", - "dev": true - }, "is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -16797,9 +16862,9 @@ "dev": true }, "known-css-properties": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.19.0.tgz", - "integrity": "sha512-eYboRV94Vco725nKMlpkn3nV2+96p9c3gKXRsYqAJSswSENvBhN7n5L+uDhY58xQa0UukWsDMTGELzmD8Q+wTA==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.20.0.tgz", + "integrity": "sha512-URvsjaA9ypfreqJ2/ylDr5MUERhJZ+DhguoWRr2xgS5C7aGCalXo+ewL+GixgKBfhT2vuL02nbIgNGqVWgTOYw==", "dev": true }, "kuler": { @@ -17683,21 +17748,6 @@ "object-visit": "^1.0.0" } }, - "markdown-escapes": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz", - "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==", - "dev": true - }, - "markdown-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", - "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", - "dev": true, - "requires": { - "repeat-string": "^1.0.0" - } - }, "match-url-wildcard": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/match-url-wildcard/-/match-url-wildcard-0.0.4.tgz", @@ -17730,15 +17780,46 @@ "safe-buffer": "^5.1.2" } }, - "mdast-util-compact": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-2.0.1.tgz", - "integrity": "sha512-7GlnT24gEwDrdAwEHrU4Vv5lLWrEer4KOkAiKT9nYstsTad7Oc1TwqT2zIMKRdZF7cTuaf+GA1E4Kv7jJh8mPA==", + "mdast-util-from-markdown": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.1.tgz", + "integrity": "sha512-qJXNcFcuCSPqUF0Tb0uYcFDIq67qwB3sxo9RPdf9vG8T90ViKnksFqdB/Coq2a7sTnxL/Ify2y7aIQXDkQFH0w==", "dev": true, "requires": { - "unist-util-visit": "^2.0.0" + "@types/mdast": "^3.0.0", + "mdast-util-to-string": "^1.0.0", + "micromark": "~2.10.0", + "parse-entities": "^2.0.0" } }, + "mdast-util-to-markdown": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.5.4.tgz", + "integrity": "sha512-0jQTkbWYx0HdEA/h++7faebJWr5JyBoBeiRf0u3F4F3QtnyyGaWIsOwo749kRb1ttKrLLr+wRtOkfou9yB0p6A==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0", + "longest-streak": "^2.0.0", + "mdast-util-to-string": "^2.0.0", + "parse-entities": "^2.0.0", + "repeat-string": "^1.0.0", + "zwitch": "^1.0.0" + }, + "dependencies": { + "mdast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "dev": true + } + } + }, + "mdast-util-to-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz", + "integrity": "sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==", + "dev": true + }, "mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -17767,36 +17848,70 @@ "optional": true }, "meow": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-7.0.1.tgz", - "integrity": "sha512-tBKIQqVrAHqwit0vfuFPY3LlzJYkEOFyKa3bPgxzNl6q/RtN8KQ+ALYEASYuFayzSAsjlhXj/JZ10rH85Q6TUw==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.0.0.tgz", + "integrity": "sha512-nbsTRz2fwniJBFgUkcdISq8y/q9n9VbiHYbfwklFh5V4V2uAcxtKQkDc0yCLPM/kP0d+inZBewn3zJqewHE7kg==", "dev": true, "requires": { "@types/minimist": "^1.2.0", - "arrify": "^2.0.1", - "camelcase": "^6.0.0", "camelcase-keys": "^6.2.2", "decamelize-keys": "^1.1.0", "hard-rejection": "^2.1.0", - "minimist-options": "^4.0.2", - "normalize-package-data": "^2.5.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", "read-pkg-up": "^7.0.1", "redent": "^3.0.0", "trim-newlines": "^3.0.0", - "type-fest": "^0.13.1", - "yargs-parser": "^18.1.3" + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" }, "dependencies": { - "camelcase": { + "hosted-git-info": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.7.tgz", + "integrity": "sha512-fWqc0IcuXs+BmE9orLDyVykAG9GJtGLGuZAAqgcckPgv5xad4AcXGIv8galtQvlwutxSlaMcdw7BUtq2EIvqCQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "lru-cache": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.0.0.tgz", - "integrity": "sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "normalize-package-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.0.tgz", + "integrity": "sha512-6lUjEI0d3v6kFrtgA/lOx4zHCWULXsFNIjHolnZCKCTLA6m/G625cdn3O7eNmT0iD3jfo6HZ9cdImGZwf21prw==", + "dev": true, + "requires": { + "hosted-git-info": "^3.0.6", + "resolve": "^1.17.0", + "semver": "^7.3.2", + "validate-npm-package-license": "^3.0.1" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", "dev": true }, - "type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", "dev": true } } @@ -17823,6 +17938,27 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, + "micromark": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.10.1.tgz", + "integrity": "sha512-fUuVF8sC1X7wsCS29SYQ2ZfIZYbTymp0EYr6sab3idFjigFFjGa5UwoniPlV9tAgntjuapW1t9U+S0yDYeGKHQ==", + "dev": true, + "requires": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } + } + }, "micromatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", @@ -17962,12 +18098,6 @@ "kind-of": "^6.0.3" }, "dependencies": { - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", @@ -20313,64 +20443,6 @@ "dev": true, "requires": { "htmlparser2": "^3.10.0" - }, - "dependencies": { - "domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true - }, - "domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, - "domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true - }, - "htmlparser2": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", - "dev": true, - "requires": { - "domelementtype": "^1.3.1", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.1.1" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } } }, "postcss-less": { @@ -20662,29 +20734,6 @@ "postcss-value-parser": "^3.0.0" } }, - "postcss-reporter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-6.0.1.tgz", - "integrity": "sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "lodash": "^4.17.11", - "log-symbols": "^2.2.0", - "postcss": "^7.0.7" - }, - "dependencies": { - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "dev": true, - "requires": { - "chalk": "^2.0.1" - } - } - } - }, "postcss-resolve-nested-selector": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", @@ -21664,60 +21713,32 @@ "dev": true }, "remark": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/remark/-/remark-12.0.0.tgz", - "integrity": "sha512-oX4lMIS0csgk8AEbzY0h2jdR0ngiCHOpwwpxjmRa5TqAkeknY+tkhjRJGZqnCmvyuWh55/0SW5WY3R3nn3PH9A==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/remark/-/remark-13.0.0.tgz", + "integrity": "sha512-HDz1+IKGtOyWN+QgBiAT0kn+2s6ovOxHyPAFGKVE81VSzJ+mq7RwHFledEvB5F1p4iJvOah/LOKdFuzvRnNLCA==", "dev": true, "requires": { - "remark-parse": "^8.0.0", - "remark-stringify": "^8.0.0", - "unified": "^9.0.0" + "remark-parse": "^9.0.0", + "remark-stringify": "^9.0.0", + "unified": "^9.1.0" } }, "remark-parse": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-8.0.2.tgz", - "integrity": "sha512-eMI6kMRjsAGpMXXBAywJwiwAse+KNpmt+BK55Oofy4KvBZEqUDj6mWbGLJZrujoPIPPxDXzn3T9baRlpsm2jnQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", + "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", "dev": true, "requires": { - "ccount": "^1.0.0", - "collapse-white-space": "^1.0.2", - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "is-word-character": "^1.0.0", - "markdown-escapes": "^1.0.0", - "parse-entities": "^2.0.0", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "trim": "0.0.1", - "trim-trailing-lines": "^1.0.0", - "unherit": "^1.0.4", - "unist-util-remove-position": "^2.0.0", - "vfile-location": "^3.0.0", - "xtend": "^4.0.1" + "mdast-util-from-markdown": "^0.8.0" } }, "remark-stringify": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-8.1.0.tgz", - "integrity": "sha512-FSPZv1ds76oAZjurhhuV5qXSUSoz6QRPuwYK38S41sLHwg4oB7ejnmZshj7qwjgYLf93kdz6BOX9j5aidNE7rA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-9.0.0.tgz", + "integrity": "sha512-8x29DpTbVzEc6Dwb90qhxCtbZ6hmj3BxWWDpMhA+1WM4dOEGH5U5/GFe3Be5Hns5MvPSFAr1e2KSVtKZkK5nUw==", "dev": true, "requires": { - "ccount": "^1.0.0", - "is-alphanumeric": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "longest-streak": "^2.0.1", - "markdown-escapes": "^1.0.0", - "markdown-table": "^2.0.0", - "mdast-util-compact": "^2.0.0", - "parse-entities": "^2.0.0", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "stringify-entities": "^3.0.0", - "unherit": "^1.0.4", - "xtend": "^4.0.1" + "mdast-util-to-markdown": "^0.5.0" } }, "remove-trailing-separator": { @@ -23006,12 +23027,6 @@ "integrity": "sha1-M6qE8Rd6VUjIk1Uzy/6zQgl19aQ=", "dev": true }, - "state-toggle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", - "integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==", - "dev": true - }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -23166,19 +23181,6 @@ } } }, - "stringify-entities": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-3.0.1.tgz", - "integrity": "sha512-Lsk3ISA2++eJYqBMPKcr/8eby1I6L0gP0NlxF8Zja6c05yr/yCYyb2c9PwXjd08Ib3If1vn1rbs1H5ZtVuOfvQ==", - "dev": true, - "requires": { - "character-entities-html4": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.2", - "is-hexadecimal": "^1.0.0" - } - }, "stringify-object": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", @@ -23261,20 +23263,22 @@ } }, "stylelint": { - "version": "13.6.1", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-13.6.1.tgz", - "integrity": "sha512-XyvKyNE7eyrqkuZ85Citd/Uv3ljGiuYHC6UiztTR6sWS9rza8j3UeQv/eGcQS9NZz/imiC4GKdk1EVL3wst5vw==", + "version": "13.8.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-13.8.0.tgz", + "integrity": "sha512-iHH3dv3UI23SLDrH4zMQDjLT9/dDIz/IpoFeuNxZmEx86KtfpjDOscxLTFioQyv+2vQjPlRZnK0UoJtfxLICXQ==", "dev": true, "requires": { - "@stylelint/postcss-css-in-js": "^0.37.1", - "@stylelint/postcss-markdown": "^0.36.1", - "autoprefixer": "^9.8.0", + "@stylelint/postcss-css-in-js": "^0.37.2", + "@stylelint/postcss-markdown": "^0.36.2", + "autoprefixer": "^9.8.6", "balanced-match": "^1.0.0", "chalk": "^4.1.0", - "cosmiconfig": "^6.0.0", - "debug": "^4.1.1", + "cosmiconfig": "^7.0.0", + "debug": "^4.2.0", "execall": "^2.0.0", - "file-entry-cache": "^5.0.1", + "fast-glob": "^3.2.4", + "fastest-levenshtein": "^1.0.12", + "file-entry-cache": "^6.0.0", "get-stdin": "^8.0.0", "global-modules": "^2.0.0", "globby": "^11.0.1", @@ -23283,24 +23287,22 @@ "ignore": "^5.1.8", "import-lazy": "^4.0.0", "imurmurhash": "^0.1.4", - "known-css-properties": "^0.19.0", - "leven": "^3.1.0", - "lodash": "^4.17.15", + "known-css-properties": "^0.20.0", + "lodash": "^4.17.20", "log-symbols": "^4.0.0", "mathml-tag-names": "^2.1.3", - "meow": "^7.0.1", + "meow": "^8.0.0", "micromatch": "^4.0.2", "normalize-selector": "^0.2.0", - "postcss": "^7.0.32", + "postcss": "^7.0.35", "postcss-html": "^0.36.0", "postcss-less": "^3.1.4", "postcss-media-query-parser": "^0.2.3", - "postcss-reporter": "^6.0.1", "postcss-resolve-nested-selector": "^0.1.1", "postcss-safe-parser": "^4.0.2", "postcss-sass": "^0.4.4", "postcss-scss": "^2.1.1", - "postcss-selector-parser": "^6.0.2", + "postcss-selector-parser": "^6.0.4", "postcss-syntax": "^0.36.2", "postcss-value-parser": "^4.1.0", "resolve-from": "^5.0.0", @@ -23311,8 +23313,8 @@ "style-search": "^0.1.0", "sugarss": "^2.0.0", "svg-tags": "^1.0.0", - "table": "^5.4.6", - "v8-compile-cache": "^2.1.1", + "table": "^6.0.3", + "v8-compile-cache": "^2.2.0", "write-file-atomic": "^3.0.3" }, "dependencies": { @@ -23323,15 +23325,20 @@ "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==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, "chalk": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", @@ -23358,14 +23365,39 @@ "dev": true }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, + "file-entry-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz", + "integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.0.tgz", + "integrity": "sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==", + "dev": true + }, "get-stdin": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", @@ -23384,6 +23416,92 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, + "postcss": { + "version": "7.0.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", + "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-selector-parser": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", + "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1", + "util-deprecate": "^1.0.2" + } + }, "postcss-value-parser": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", @@ -23396,6 +23514,23 @@ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "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 + }, "string-width": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", @@ -23417,13 +23552,31 @@ } }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" } + }, + "table": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/table/-/table-6.0.4.tgz", + "integrity": "sha512-sBT4xRLdALd+NFBvwOz8bw4b15htyythha+q+DVZqy2RS08PPC8O2sZFgJYEY7bJvbCFKccs+WIZ/cd+xxTWCw==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "lodash": "^4.17.20", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0" + } + }, + "v8-compile-cache": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", + "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", + "dev": true } } }, @@ -25120,12 +25273,6 @@ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true }, - "trim": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", - "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", - "dev": true - }, "trim-newlines": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz", @@ -25138,12 +25285,6 @@ "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "dev": true }, - "trim-trailing-lines": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz", - "integrity": "sha512-4ku0mmjXifQcTVfYDfR5lpgV7zVqPg6zV9rdZmwOPqq0+Zq19xDqEgagqVbc4pOOShbncuAOIs59R3+3gcF3ZA==", - "dev": true - }, "triple-beam": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", @@ -25506,16 +25647,6 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=" }, - "unherit": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", - "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==", - "dev": true, - "requires": { - "inherits": "^2.0.0", - "xtend": "^4.0.0" - } - }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", @@ -25545,9 +25676,9 @@ "dev": true }, "unified": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/unified/-/unified-9.0.0.tgz", - "integrity": "sha512-ssFo33gljU3PdlWLjNp15Inqb77d6JnJSfyplGJPT/a+fNRNyCBeveBAYJdO5khKdF6WVHa/yYCC7Xl6BDwZUQ==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.0.tgz", + "integrity": "sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg==", "dev": true, "requires": { "bail": "^1.0.0", @@ -25601,29 +25732,20 @@ } }, "unist-util-find-all-after": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/unist-util-find-all-after/-/unist-util-find-all-after-3.0.1.tgz", - "integrity": "sha512-0GICgc++sRJesLwEYDjFVJPJttBpVQaTNgc6Jw0Jhzvfs+jtKePEMu+uD+PqkRUrAvGQqwhpDwLGWo1PK8PDEw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/unist-util-find-all-after/-/unist-util-find-all-after-3.0.2.tgz", + "integrity": "sha512-xaTC/AGZ0rIM2gM28YVRAFPIZpzbpDtU3dRmp7EXlNVA8ziQc4hY3H7BHXM1J49nEmiqc3svnqMReW+PGqbZKQ==", "dev": true, "requires": { "unist-util-is": "^4.0.0" } }, "unist-util-is": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.0.2.tgz", - "integrity": "sha512-Ofx8uf6haexJwI1gxWMGg6I/dLnF2yE+KibhD3/diOqY2TinLcqHXCV6OI5gFVn3xQqDH+u0M625pfKwIwgBKQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.0.3.tgz", + "integrity": "sha512-bTofCFVx0iQM8Jqb1TBDVRIQW03YkD3p66JOd/aCWuqzlLyUtx1ZAGw/u+Zw+SttKvSVcvTiKYbfrtLoLefykw==", "dev": true }, - "unist-util-remove-position": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz", - "integrity": "sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA==", - "dev": true, - "requires": { - "unist-util-visit": "^2.0.0" - } - }, "unist-util-stringify-position": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", @@ -25633,27 +25755,6 @@ "@types/unist": "^2.0.2" } }, - "unist-util-visit": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.2.tgz", - "integrity": "sha512-HoHNhGnKj6y+Sq+7ASo2zpVdfdRifhTgX2KTU3B/sO/TTlZchp7E3S4vjRzDJ7L60KmrCPsQkVK3lEF3cz36XQ==", - "dev": true, - "requires": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0", - "unist-util-visit-parents": "^3.0.0" - } - }, - "unist-util-visit-parents": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.0.2.tgz", - "integrity": "sha512-yJEfuZtzFpQmg1OSCyS9M5NJRrln/9FbYosH3iW0MG402QbdbaB8ZESwUv9RO6nRfLAKvWcMxCwdLWOov36x/g==", - "dev": true, - "requires": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0" - } - }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -25934,9 +26035,9 @@ } }, "vfile": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.1.1.tgz", - "integrity": "sha512-lRjkpyDGjVlBA7cDQhQ+gNcvB1BGaTHYuSOcY3S7OhDmBtnzX95FhtZZDecSTDm6aajFymyve6S5DN4ZHGezdQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.0.tgz", + "integrity": "sha512-a/alcwCvtuc8OX92rqqo7PflxiCgXRFjdyoGVuYV+qbgCb0GgZJRvIgCD4+U/Kl1yhaRsaTwksF88xbPyGsgpw==", "dev": true, "requires": { "@types/unist": "^2.0.0", @@ -25946,12 +26047,6 @@ "vfile-message": "^2.0.0" } }, - "vfile-location": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-3.0.1.tgz", - "integrity": "sha512-yYBO06eeN/Ki6Kh1QAkgzYpWT1d3Qln+ZCtSbJqFExPl1S3y2qqotJQXoh6qEvl/jDlgpUJolBn3PItVnnZRqQ==", - "dev": true - }, "vfile-message": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", @@ -27283,6 +27378,12 @@ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true + }, + "zwitch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", + "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==", + "dev": true } } } diff --git a/package.json b/package.json index d99ee86b07..68222a0cf7 100644 --- a/package.json +++ b/package.json @@ -239,7 +239,7 @@ "regenerator": "^0.14.4", "rimraf": "^3.0.2", "sinon": "^9.2.1", - "stylelint": "^13.3.3", + "stylelint": "^13.8.0", "stylelint-config-prettier": "^8.0.2", "stylelint-config-standard": "^20.0.0", "stylelint-prettier": "^1.1.2", From 9d25c53800712687e4c5af7e584ca3a6ad622ee6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Nov 2020 18:13:54 +0800 Subject: [PATCH 04/34] chore(deps-dev): bump prettier from 2.1.2 to 2.2.0 (#709) * chore(deps-dev): bump prettier from 2.1.2 to 2.2.0 Bumps [prettier](https://github.com/prettier/prettier) from 2.1.2 to 2.2.0. - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/master/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/2.1.2...2.2.0) Signed-off-by: dependabot[bot] * fix: lint Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Kar Rui Lau --- package-lock.json | 6 ++--- package.json | 2 +- src/app/models/field/attachmentField.ts | 27 ++++++++++--------- src/app/models/user.server.model.ts | 21 +++++++-------- .../sms/__tests__/sms.factory.spec.ts | 6 ++--- .../validators/sectionValidator.ts | 4 +-- .../avatar-dropdown.client.component.js | 11 ++++---- .../ArrayAnswerResponse.class.js | 4 ++- .../SingleAnswerResponse.class.js | 4 ++- .../TableResponse.class.js | 4 ++- 10 files changed, 47 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1149753068..abe5129542 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20899,9 +20899,9 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" }, "prettier": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz", - "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.0.tgz", + "integrity": "sha512-yYerpkvseM4iKD/BXLYUkQV5aKt4tQPqaGW6EsZjzyu0r7sVZZNPJW4Y8MyKmicp6t42XUPcBVA+H6sB3gqndw==", "dev": true }, "prettier-linter-helpers": { diff --git a/package.json b/package.json index 68222a0cf7..9c067a1be7 100644 --- a/package.json +++ b/package.json @@ -234,7 +234,7 @@ "mongodb-memory-server-core": "^6.9.2", "ngrok": "^3.3.0", "optimize-css-assets-webpack-plugin": "^5.0.1", - "prettier": "^2.1.2", + "prettier": "^2.2.0", "proxyquire": "^2.1.3", "regenerator": "^0.14.4", "rimraf": "^3.0.2", diff --git a/src/app/models/field/attachmentField.ts b/src/app/models/field/attachmentField.ts index 3b9639d7e3..49ee6eadfe 100644 --- a/src/app/models/field/attachmentField.ts +++ b/src/app/models/field/attachmentField.ts @@ -25,19 +25,20 @@ const createAttachmentFieldSchema = () => { }) // Prevent attachments from being saved on a webhooked form. - AttachmentFieldSchema.pre('validate', function ( - next, - ) { - const { webhook, responseMode } = this.parent() - - if (responseMode === ResponseMode.Encrypt && webhook?.url) { - return next( - Error('Attachments are not allowed when a form has a webhook url'), - ) - } - - return next() - }) + AttachmentFieldSchema.pre( + 'validate', + function (next) { + const { webhook, responseMode } = this.parent() + + if (responseMode === ResponseMode.Encrypt && webhook?.url) { + return next( + Error('Attachments are not allowed when a form has a webhook url'), + ) + } + + return next() + }, + ) return AttachmentFieldSchema } diff --git a/src/app/models/user.server.model.ts b/src/app/models/user.server.model.ts index 4ca40f95c7..5b58163506 100644 --- a/src/app/models/user.server.model.ts +++ b/src/app/models/user.server.model.ts @@ -78,17 +78,16 @@ const compileUserModel = (db: Mongoose) => { * * See: https://masteringjs.io/tutorials/mongoose/e11000-duplicate-key. */ - UserSchema.post('save', function ( - err: any, - _doc: IUserSchema, - next: any, - ) { - if (err.name === 'MongoError' && err.code === 11000) { - next(new Error('Account already exists with this email')) - } else { - next() - } - }) + UserSchema.post( + 'save', + function (err: any, _doc: IUserSchema, next: any) { + if (err.name === 'MongoError' && err.code === 11000) { + next(new Error('Account already exists with this email')) + } else { + next() + } + }, + ) // Statics /** diff --git a/src/app/services/sms/__tests__/sms.factory.spec.ts b/src/app/services/sms/__tests__/sms.factory.spec.ts index 753753bdf8..5e7952f8bd 100644 --- a/src/app/services/sms/__tests__/sms.factory.spec.ts +++ b/src/app/services/sms/__tests__/sms.factory.spec.ts @@ -57,9 +57,9 @@ describe('sms.factory', () => { }) describe('sms feature enabled', () => { - const MOCK_ENABLED_SMS_FEATURE: Required> = { + const MOCK_ENABLED_SMS_FEATURE: Required< + RegisteredFeature + > = { isEnabled: true, props: { twilioAccountSid: 'ACrandomTwilioSid', diff --git a/src/app/utils/field-validation/validators/sectionValidator.ts b/src/app/utils/field-validation/validators/sectionValidator.ts index b46a0fdece..ded36560c1 100644 --- a/src/app/utils/field-validation/validators/sectionValidator.ts +++ b/src/app/utils/field-validation/validators/sectionValidator.ts @@ -3,9 +3,7 @@ import { left, right } from 'fp-ts/lib/Either' import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types' import { ResponseValidator } from 'src/types/field/utils/validation' -type SectionValidatorConstructor = () => ResponseValidator< - ProcessedSingleAnswerResponse -> +type SectionValidatorConstructor = () => ResponseValidator /** * A function that returns a validation function for a section field when called. diff --git a/src/public/modules/core/components/avatar-dropdown.client.component.js b/src/public/modules/core/components/avatar-dropdown.client.component.js index 633906acd5..f04e80fd55 100644 --- a/src/public/modules/core/components/avatar-dropdown.client.component.js +++ b/src/public/modules/core/components/avatar-dropdown.client.component.js @@ -74,11 +74,12 @@ function avatarDropdownController( vm.isDropdownOpen = false - $scope.$watchGroup(['vm.isDropdownHover', 'vm.isDropdownFocused'], function ( - newValues, - ) { - vm.isDropdownOpen = newValues[0] || newValues[1] - }) + $scope.$watchGroup( + ['vm.isDropdownHover', 'vm.isDropdownFocused'], + function (newValues) { + vm.isDropdownOpen = newValues[0] || newValues[1] + }, + ) vm.signOut = () => Auth.signOut() diff --git a/src/public/modules/forms/helpers/csv-response-classes/ArrayAnswerResponse.class.js b/src/public/modules/forms/helpers/csv-response-classes/ArrayAnswerResponse.class.js index 5b06db10ad..a5428523e9 100644 --- a/src/public/modules/forms/helpers/csv-response-classes/ArrayAnswerResponse.class.js +++ b/src/public/modules/forms/helpers/csv-response-classes/ArrayAnswerResponse.class.js @@ -1,6 +1,8 @@ const Response = require('./Response.class') -module.exports = class ArrayAnswerResponse extends Response { +module.exports = class ArrayAnswerResponse extends ( + Response +) { getAnswer() { return this._data.answerArray.join(';') } diff --git a/src/public/modules/forms/helpers/csv-response-classes/SingleAnswerResponse.class.js b/src/public/modules/forms/helpers/csv-response-classes/SingleAnswerResponse.class.js index fecb3d1d7e..1193420d59 100644 --- a/src/public/modules/forms/helpers/csv-response-classes/SingleAnswerResponse.class.js +++ b/src/public/modules/forms/helpers/csv-response-classes/SingleAnswerResponse.class.js @@ -1,6 +1,8 @@ const Response = require('./Response.class') -module.exports = class SingleAnswerResponse extends Response { +module.exports = class SingleAnswerResponse extends ( + Response +) { getAnswer() { return this._data.answer } diff --git a/src/public/modules/forms/helpers/csv-response-classes/TableResponse.class.js b/src/public/modules/forms/helpers/csv-response-classes/TableResponse.class.js index 4352169a40..9110b5ee3e 100644 --- a/src/public/modules/forms/helpers/csv-response-classes/TableResponse.class.js +++ b/src/public/modules/forms/helpers/csv-response-classes/TableResponse.class.js @@ -1,6 +1,8 @@ const Response = require('./Response.class') -module.exports = class TableResponse extends Response { +module.exports = class TableResponse extends ( + Response +) { getAnswer(colIndex) { // Leave cell empty if number of rows is fewer than the index if (colIndex >= this._data.answerArray.length) { From cb50ec03596572ddfca89c537fd8443521dd75c3 Mon Sep 17 00:00:00 2001 From: Kar Rui Lau Date: Wed, 25 Nov 2020 10:25:11 +0800 Subject: [PATCH 05/34] fix(AdminFormRoutes): add Joi validation on /submission endpoint (#712) --- src/app/routes/admin-forms.server.routes.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/app/routes/admin-forms.server.routes.js b/src/app/routes/admin-forms.server.routes.js index c8ae2c7183..167fc83023 100644 --- a/src/app/routes/admin-forms.server.routes.js +++ b/src/app/routes/admin-forms.server.routes.js @@ -400,12 +400,17 @@ module.exports = function (app) { * @returns {Object} 200 - Response document * @security OTP */ - app - .route('/:formId([a-fA-F0-9]{24})/adminform/submissions') - .get( - authEncryptedResponseAccess, - EncryptSubmissionController.handleGetEncryptedResponse, - ) + app.route('/:formId([a-fA-F0-9]{24})/adminform/submissions').get( + authEncryptedResponseAccess, + celebrate({ + [Segments.QUERY]: { + submissionId: Joi.string() + .regex(/^[0-9a-fA-F]{24}$/) + .required(), + }, + }), + EncryptSubmissionController.handleGetEncryptedResponse, + ) /** * Count the number of submissions for a public form From 29e1a94af4957c2a65f6923fc5d43bb797b01f68 Mon Sep 17 00:00:00 2001 From: Kar Rui Lau Date: Wed, 25 Nov 2020 10:26:49 +0800 Subject: [PATCH 06/34] feat: migrate get encrypt metadata endpoint controller to TypeScript (#711) * feat(EncryptSubSvc): add getSubmissionMetadata service fn (and tests) * feat(EncryptSubSvc): add getSubmissionMetadataList service fn (+tests) * feat(EncryptSubCtl): add handleGetMetadata fn * ref(AdminFormRoutes): use new handleGetMetadata handler fn * feat(AdminFormRoutes): allow optional page when submissionId exists * test(EncryptSubCtl): add tests for handleGetMetadata --- .../encrypt-submissions.server.controller.js | 59 ------ .../encrypt-submission.controller.spec.ts | 182 +++++++++++++++++- .../encrypt-submission.service.spec.ts | 170 +++++++++++++++- .../encrypt-submission.controller.ts | 65 +++++++ .../encrypt-submission.service.ts | 56 +++++- src/app/routes/admin-forms.server.routes.js | 7 +- 6 files changed, 474 insertions(+), 65 deletions(-) diff --git a/src/app/controllers/encrypt-submissions.server.controller.js b/src/app/controllers/encrypt-submissions.server.controller.js index d422275e50..919a764c37 100644 --- a/src/app/controllers/encrypt-submissions.server.controller.js +++ b/src/app/controllers/encrypt-submissions.server.controller.js @@ -3,7 +3,6 @@ const crypto = require('crypto') const { StatusCodes } = require('http-status-codes') const mongoose = require('mongoose') -const errorHandler = require('../utils/handle-mongo-error') const { getEncryptSubmissionModel, } = require('../models/submission.server.model') @@ -20,8 +19,6 @@ const { getProcessedResponses, } = require('../modules/submission/submission.service') -const HttpStatus = require('http-status-codes') - /** * Extracts relevant fields, injects questions, verifies visibility of field and validates answers * to produce req.body.parsedResponses @@ -210,59 +207,3 @@ exports.saveResponseToDb = function (req, res, next) { return onEncryptSubmissionFailure(err, req, res, submission) }) } - -/** - * Return metadata for encrypted form responses matching form._id - * @param {Object} req - Express request object - * @param {String} req.query.pageNo - page of table to return data for - * @param {Object} req.form - the form - * @param {Object} res - Express response object - */ -exports.getMetadata = function (req, res) { - let { page, submissionId } = req.query || {} - - if (submissionId) { - // Early return if submissionId is invalid. - if (!mongoose.Types.ObjectId.isValid(submissionId)) { - return res.status(HttpStatus.OK).json({ metadata: [], count: 0 }) - } - - return EncryptSubmission.findSingleMetadata(req.form._id, submissionId) - .then((result) => { - if (!result) { - return res.status(HttpStatus.OK).json({ metadata: [], count: 0 }) - } - - return res.status(HttpStatus.OK).json({ metadata: [result], count: 1 }) - }) - .catch((error) => { - logger.error({ - message: 'Failure retrieving metadata from database', - meta: { - action: 'getMetadata', - ...createReqMeta(req), - }, - error, - }) - return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ - message: errorHandler.getMongoErrorMessage(error), - }) - }) - } else { - return EncryptSubmission.findAllMetadataByFormId(req.form._id, { page }) - .then((result) => res.status(StatusCodes.OK).json(result)) - .catch((error) => { - logger.error({ - message: 'Failure retrieving metadata from database', - meta: { - action: 'getMetadata', - ...createReqMeta(req), - }, - error: error, - }) - return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - message: errorHandler.getMongoErrorMessage(error), - }) - }) - } -} diff --git a/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.controller.spec.ts b/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.controller.spec.ts index 4d8123762e..de8aa3707b 100644 --- a/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.controller.spec.ts +++ b/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.controller.spec.ts @@ -1,20 +1,26 @@ +import { ObjectId } from 'bson-ext' import { errAsync, okAsync } from 'neverthrow' import { mocked } from 'ts-jest/utils' import { DatabaseError } from 'src/app/modules/core/core.errors' import { CreatePresignedUrlError } from 'src/app/modules/form/admin-form/admin-form.errors' -import { SubmissionData } from 'src/types' +import { SubmissionData, SubmissionMetadata } from 'src/types' import expressHandler from 'tests/unit/backend/helpers/jest-express' import { SubmissionNotFoundError } from '../../submission.errors' -import { handleGetEncryptedResponse } from '../encrypt-submission.controller' +import { + handleGetEncryptedResponse, + handleGetMetadata, +} from '../encrypt-submission.controller' import * as EncryptSubmissionService from '../encrypt-submission.service' jest.mock('../encrypt-submission.service') const MockEncryptSubService = mocked(EncryptSubmissionService) describe('encrypt-submission.controller', () => { + beforeEach(() => jest.clearAllMocks()) + describe('handleGetEncryptedResponse', () => { const MOCK_REQ = expressHandler.mockRequest({ params: { formId: 'mockFormId' }, @@ -114,4 +120,176 @@ describe('encrypt-submission.controller', () => { expect(mockRes.json).toHaveBeenCalledWith({ message: mockErrorString }) }) }) + + describe('handleGetMetadata', () => { + const MOCK_FORM_ID = new ObjectId().toHexString() + + it('should return 200 with single submission metadata when query.submissionId is provided and can be found', async () => { + // Arrange + const mockSubmissionId = new ObjectId().toHexString() + const mockReq = expressHandler.mockRequest({ + params: { formId: MOCK_FORM_ID }, + query: { + submissionId: mockSubmissionId, + }, + }) + const mockRes = expressHandler.mockResponse() + // Mock service result. + const expectedMetadata: SubmissionMetadata = { + number: 2, + refNo: mockSubmissionId, + submissionTime: 'some submission time', + } + + MockEncryptSubService.getSubmissionMetadata.mockReturnValueOnce( + okAsync(expectedMetadata), + ) + + // Act + await handleGetMetadata(mockReq, mockRes, jest.fn()) + + // Assert + expect(mockRes.json).toHaveBeenCalledWith({ + metadata: [expectedMetadata], + count: 1, + }) + expect( + MockEncryptSubService.getSubmissionMetadataList, + ).not.toHaveBeenCalled() + expect(MockEncryptSubService.getSubmissionMetadata).toHaveBeenCalledWith( + MOCK_FORM_ID, + mockSubmissionId, + ) + }) + + it('should return 200 with empty submission metadata when query.submissionId is provided but cannot be found', async () => { + // Arrange + const mockSubmissionId = new ObjectId().toHexString() + const mockReq = expressHandler.mockRequest({ + params: { formId: MOCK_FORM_ID }, + query: { + submissionId: mockSubmissionId, + }, + }) + const mockRes = expressHandler.mockResponse() + // Mock service result. + MockEncryptSubService.getSubmissionMetadata.mockReturnValueOnce( + okAsync(null), + ) + + // Act + await handleGetMetadata(mockReq, mockRes, jest.fn()) + + // Assert + expect(mockRes.json).toHaveBeenCalledWith({ + metadata: [], + count: 0, + }) + expect( + MockEncryptSubService.getSubmissionMetadataList, + ).not.toHaveBeenCalled() + expect(MockEncryptSubService.getSubmissionMetadata).toHaveBeenCalledWith( + MOCK_FORM_ID, + mockSubmissionId, + ) + }) + + it('should return 200 with list of submission metadata', async () => { + // Arrange + const mockReq = expressHandler.mockRequest({ + params: { formId: MOCK_FORM_ID }, + query: { + page: 20, + }, + }) + const mockRes = expressHandler.mockResponse() + // Mock service result. + const expectedMetadataList = { + metadata: [ + { + number: 2, + refNo: new ObjectId().toHexString(), + submissionTime: 'some submission time', + }, + ] as SubmissionMetadata[], + count: 32, + } + + MockEncryptSubService.getSubmissionMetadataList.mockReturnValueOnce( + okAsync(expectedMetadataList), + ) + + // Act + await handleGetMetadata(mockReq, mockRes, jest.fn()) + + // Assert + expect(mockRes.json).toHaveBeenCalledWith(expectedMetadataList) + expect( + MockEncryptSubService.getSubmissionMetadataList, + ).toHaveBeenCalledWith(MOCK_FORM_ID, mockReq.query.page) + expect(MockEncryptSubService.getSubmissionMetadata).not.toHaveBeenCalled() + }) + + it('should return 500 when database errors occurs for single metadata retrieval', async () => { + // Arrange + const mockSubmissionId = new ObjectId().toHexString() + const mockReq = expressHandler.mockRequest({ + params: { formId: MOCK_FORM_ID }, + query: { + submissionId: mockSubmissionId, + }, + }) + const mockErrorString = 'error retrieving metadata' + const mockRes = expressHandler.mockResponse() + // Mock service result. + MockEncryptSubService.getSubmissionMetadata.mockReturnValueOnce( + errAsync(new DatabaseError(mockErrorString)), + ) + + // Act + await handleGetMetadata(mockReq, mockRes, jest.fn()) + + // Assert + expect(mockRes.json).toHaveBeenCalledWith({ + message: mockErrorString, + }) + expect(mockRes.status).toHaveBeenCalledWith(500) + expect( + MockEncryptSubService.getSubmissionMetadataList, + ).not.toHaveBeenCalled() + expect(MockEncryptSubService.getSubmissionMetadata).toHaveBeenCalledWith( + MOCK_FORM_ID, + mockSubmissionId, + ) + }) + + it('should return 500 when database errors occurs for metadata list retrieval', async () => { + // Arrange + const mockReq = expressHandler.mockRequest({ + params: { formId: MOCK_FORM_ID }, + query: { + page: 1, + }, + }) + const mockErrorString = 'error retrieving metadata list' + const mockRes = expressHandler.mockResponse() + // Mock service result. + MockEncryptSubService.getSubmissionMetadataList.mockReturnValueOnce( + errAsync(new DatabaseError(mockErrorString)), + ) + + // Act + await handleGetMetadata(mockReq, mockRes, jest.fn()) + + // Assert + expect(mockRes.json).toHaveBeenCalledWith({ + message: mockErrorString, + }) + expect(mockRes.status).toHaveBeenCalledWith(500) + expect(MockEncryptSubService.getSubmissionMetadata).not.toHaveBeenCalled() + expect( + MockEncryptSubService.getSubmissionMetadataList, + ).toHaveBeenCalledWith(MOCK_FORM_ID, mockReq.query.page) + }) + }) }) diff --git a/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.service.spec.ts b/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.service.spec.ts index 122cf01050..19fd666a51 100644 --- a/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.service.spec.ts +++ b/src/app/modules/submission/encrypt-submission/__tests__/encrypt-submission.service.spec.ts @@ -11,12 +11,18 @@ import { } from 'src/app/modules/core/core.errors' import { CreatePresignedUrlError } from 'src/app/modules/form/admin-form/admin-form.errors' import { aws } from 'src/config/config' -import { SubmissionCursorData, SubmissionData } from 'src/types' +import { + SubmissionCursorData, + SubmissionData, + SubmissionMetadata, +} from 'src/types' import { SubmissionNotFoundError } from '../../submission.errors' import { getEncryptedSubmissionData, getSubmissionCursor, + getSubmissionMetadata, + getSubmissionMetadataList, transformAttachmentMetasToSignedUrls, transformAttachmentMetaStream, } from '../encrypt-submission.service' @@ -524,6 +530,168 @@ describe('encrypt-submission.service', () => { ) }) }) + + describe('getSubmissionMetadata', () => { + const MOCK_FORM_ID = new ObjectId().toHexString() + + it('should return metadata successfully', async () => { + // Arrange + const mockSubmissionId = new ObjectId().toHexString() + const expectedMetadata: SubmissionMetadata = { + number: 200, + refNo: mockSubmissionId, + submissionTime: 'some submission time', + } + const getMetaSpy = jest + .spyOn(EncryptSubmission, 'findSingleMetadata') + .mockResolvedValueOnce(expectedMetadata) + + // Act + const actualResult = await getSubmissionMetadata( + MOCK_FORM_ID, + mockSubmissionId, + ) + + // Arrange + expect(actualResult.isOk()).toEqual(true) + expect(actualResult._unsafeUnwrap()).toEqual(expectedMetadata) + expect(getMetaSpy).toHaveBeenCalledWith(MOCK_FORM_ID, mockSubmissionId) + }) + + it('should return null when given submissionId is not valid', async () => { + // Arrange + const invalidSubmissionId = 'not an id at all' + + // Act + const actualResult = await getSubmissionMetadata( + MOCK_FORM_ID, + invalidSubmissionId, + ) + + // Arrange + expect(actualResult.isOk()).toEqual(true) + expect(actualResult._unsafeUnwrap()).toEqual(null) + }) + + it('should return null when query returns null', async () => { + // Arrange + const mockSubmissionId = new ObjectId().toHexString() + const getMetaSpy = jest + .spyOn(EncryptSubmission, 'findSingleMetadata') + .mockResolvedValueOnce(null) + + // Act + const actualResult = await getSubmissionMetadata( + MOCK_FORM_ID, + mockSubmissionId, + ) + + // Arrange + expect(actualResult.isOk()).toEqual(true) + expect(actualResult._unsafeUnwrap()).toEqual(null) + expect(getMetaSpy).toHaveBeenCalledWith(MOCK_FORM_ID, mockSubmissionId) + }) + + it('should return DatabaseError when database error occurs', async () => { + // Arrange + const mockSubmissionId = new ObjectId().toHexString() + const mockErrorString = 'some database error message' + const getMetaSpy = jest + .spyOn(EncryptSubmission, 'findSingleMetadata') + .mockRejectedValueOnce(new Error(mockErrorString)) + + // Act + const actualResult = await getSubmissionMetadata( + MOCK_FORM_ID, + mockSubmissionId, + ) + + // Arrange + expect(actualResult.isErr()).toEqual(true) + expect(actualResult._unsafeUnwrapErr()).toEqual( + new DatabaseError(mockErrorString), + ) + expect(getMetaSpy).toHaveBeenCalledWith(MOCK_FORM_ID, mockSubmissionId) + }) + }) + + describe('getSubmissionMetadataList', () => { + const MOCK_FORM_ID = new ObjectId().toHexString() + + it('should return metadata list successfully without page param', async () => { + // Arrange + const expectedResult = { + metadata: [ + { + number: 200, + refNo: new ObjectId().toHexString(), + submissionTime: 'some submission time', + }, + ], + count: 1, + } + const getMetaSpy = jest + .spyOn(EncryptSubmission, 'findAllMetadataByFormId') + .mockResolvedValueOnce(expectedResult) + + // Act + const actualResult = await getSubmissionMetadataList(MOCK_FORM_ID) + + // Arrange + expect(actualResult.isOk()).toEqual(true) + expect(actualResult._unsafeUnwrap()).toEqual(expectedResult) + expect(getMetaSpy).toHaveBeenCalledWith(MOCK_FORM_ID, { page: undefined }) + }) + + it('should return metadata list successfully with page param', async () => { + // Arrange + const mockPageNumber = 200 + const expectedResult = { + metadata: [ + { + number: 9, + refNo: new ObjectId().toHexString(), + submissionTime: 'another submission time', + }, + ], + count: 1, + } + const getMetaSpy = jest + .spyOn(EncryptSubmission, 'findAllMetadataByFormId') + .mockResolvedValueOnce(expectedResult) + + // Act + const actualResult = await getSubmissionMetadataList( + MOCK_FORM_ID, + mockPageNumber, + ) + + // Arrange + expect(actualResult.isOk()).toEqual(true) + expect(actualResult._unsafeUnwrap()).toEqual(expectedResult) + expect(getMetaSpy).toHaveBeenCalledWith(MOCK_FORM_ID, { + page: mockPageNumber, + }) + }) + + it('should return DatabaseError when database error occurs', async () => { + // Arrange + const mockErrorString = 'some database error message' + const getMetaSpy = jest + .spyOn(EncryptSubmission, 'findAllMetadataByFormId') + .mockRejectedValueOnce(new Error(mockErrorString)) + + // Act + const actualResult = await getSubmissionMetadataList(MOCK_FORM_ID) + + // Arrange + expect(actualResult.isErr()).toEqual(true) + expect(actualResult._unsafeUnwrapErr()).toEqual( + new DatabaseError(mockErrorString), + ) + expect(getMetaSpy).toHaveBeenCalledWith(MOCK_FORM_ID, { page: undefined }) + }) + }) }) /** diff --git a/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts b/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts index b219eb9273..397279ac1c 100644 --- a/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts +++ b/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts @@ -11,6 +11,8 @@ import { createReqMeta } from '../../../utils/request' import { getEncryptedSubmissionData, getSubmissionCursor, + getSubmissionMetadata, + getSubmissionMetadataList, transformAttachmentMetasToSignedUrls, transformAttachmentMetaStream, } from './encrypt-submission.service' @@ -202,3 +204,66 @@ export const handleGetEncryptedResponse: RequestHandler< return res.json(responseData) } + +/** + * Handler for GET /:formId([a-fA-F0-9]{24})/adminform/submissions/metadata + * + * @returns 200 with single submission metadata if query.submissionId is provided + * @returns 200 with list of submission metadata with total count (and optional offset if query.page is provided) if query.submissionId is not provided + * @returns 500 if any errors occurs whilst querying database + */ +export const handleGetMetadata: RequestHandler< + { formId: string }, + unknown, + unknown, + Query & { page?: number; submissionId?: string } +> = async (req, res) => { + const { formId } = req.params + const { page, submissionId } = req.query + + const logMeta = { + action: 'handleGetMetadata', + formId, + submissionId, + page, + ...createReqMeta(req), + } + + // Specific query. + if (submissionId) { + return getSubmissionMetadata(formId, submissionId) + .map((metadata) => { + return metadata + ? res.json({ metadata: [metadata], count: 1 }) + : res.json({ metadata: [], count: 0 }) + }) + .mapErr((error) => { + logger.error({ + message: 'Failure retrieving metadata from database', + meta: logMeta, + error, + }) + + const { statusCode, errorMessage } = mapRouteError(error) + return res.status(statusCode).json({ + message: errorMessage, + }) + }) + } + + // General query + return getSubmissionMetadataList(formId, page) + .map((result) => res.json(result)) + .mapErr((error) => { + logger.error({ + message: 'Failure retrieving metadata list from database', + meta: logMeta, + error, + }) + + const { statusCode, errorMessage } = mapRouteError(error) + return res.status(statusCode).json({ + message: errorMessage, + }) + }) +} diff --git a/src/app/modules/submission/encrypt-submission/encrypt-submission.service.ts b/src/app/modules/submission/encrypt-submission/encrypt-submission.service.ts index 9920ceb2d0..af0731f95d 100644 --- a/src/app/modules/submission/encrypt-submission/encrypt-submission.service.ts +++ b/src/app/modules/submission/encrypt-submission/encrypt-submission.service.ts @@ -5,7 +5,11 @@ import { Transform } from 'stream' import { aws as AwsConfig } from '../../../../config/config' import { createLoggerWithLabel } from '../../../../config/logger' -import { SubmissionCursorData, SubmissionData } from '../../../../types' +import { + SubmissionCursorData, + SubmissionData, + SubmissionMetadata, +} from '../../../../types' import { getEncryptSubmissionModel } from '../../../models/submission.server.model' import { isMalformedDate } from '../../../utils/date' import { getMongoErrorMessage } from '../../../utils/handle-mongo-error' @@ -205,3 +209,53 @@ export const transformAttachmentMetasToSignedUrls = ( }, ) } + +export const getSubmissionMetadata = ( + formId: string, + submissionId: string, +): ResultAsync => { + // Early return, do not even retrieve from database. + if (!mongoose.Types.ObjectId.isValid(submissionId)) { + return okAsync(null) + } + + return ResultAsync.fromPromise( + EncryptSubmissionModel.findSingleMetadata(formId, submissionId), + (error) => { + logger.error({ + message: 'Failure retrieving metadata from database', + meta: { + action: 'getSubmissionMetadata', + formId, + submissionId, + }, + error, + }) + return new DatabaseError(getMongoErrorMessage(error)) + }, + ) +} + +export const getSubmissionMetadataList = ( + formId: string, + page?: number, +): ResultAsync< + { metadata: SubmissionMetadata[]; count: number }, + DatabaseError +> => { + return ResultAsync.fromPromise( + EncryptSubmissionModel.findAllMetadataByFormId(formId, { page }), + (error) => { + logger.error({ + message: 'Failure retrieving metadata page from database', + meta: { + action: 'getSubmissionMetadataList', + formId, + page, + }, + error, + }) + return new DatabaseError(getMongoErrorMessage(error)) + }, + ) +} diff --git a/src/app/routes/admin-forms.server.routes.js b/src/app/routes/admin-forms.server.routes.js index 167fc83023..e97ba657b8 100644 --- a/src/app/routes/admin-forms.server.routes.js +++ b/src/app/routes/admin-forms.server.routes.js @@ -457,11 +457,14 @@ module.exports = function (app) { authEncryptedResponseAccess, celebrate({ [Segments.QUERY]: { - page: Joi.number().min(1).required(), submissionId: Joi.string().optional(), + page: Joi.number().min(1).when('submissionId', { + not: Joi.exist(), + then: Joi.required(), + }), }, }), - encryptSubmissions.getMetadata, + EncryptSubmissionController.handleGetMetadata, ) /** From 2aaf4dbaab8cd337f9f232066d557ac75166381c Mon Sep 17 00:00:00 2001 From: tshuli <63710093+tshuli@users.noreply.github.com> Date: Wed, 25 Nov 2020 11:04:43 +0800 Subject: [PATCH 07/34] refactor: migrate radio button validator to ts (#723) * refactor: migrate radioButtonValidator to ts * refactor: update references * chore: update isVisible for tests --- .../FieldValidatorFactory.class.js | 4 +-- src/app/utils/field-validation/config.js | 1 - src/app/utils/field-validation/index.ts | 3 +- .../singleAnswerValidator.factory.ts | 4 +++ .../validators/RadiobuttonValidator.class.js | 14 -------- .../field-validation/validators/index.js | 1 - .../validators/radioButtonValidator.ts | 36 +++++++++++++++++++ src/types/field/utils/guards.ts | 7 ++++ .../radio-button-validation.spec.js | 6 ++++ 9 files changed, 56 insertions(+), 20 deletions(-) delete mode 100644 src/app/utils/field-validation/validators/RadiobuttonValidator.class.js create mode 100644 src/app/utils/field-validation/validators/radioButtonValidator.ts diff --git a/src/app/utils/field-validation/FieldValidatorFactory.class.js b/src/app/utils/field-validation/FieldValidatorFactory.class.js index ed8fe42040..535e0e0d6f 100644 --- a/src/app/utils/field-validation/FieldValidatorFactory.class.js +++ b/src/app/utils/field-validation/FieldValidatorFactory.class.js @@ -1,6 +1,5 @@ const { DropdownValidator, - RadiobuttonValidator, CheckboxValidator, EmailValidator, TableValidator, @@ -42,9 +41,8 @@ class FieldValidatorFactory { case 'textarea': // long text case 'nric': case 'homeno': - throw new Error(`${fieldType} has been migrated to TypeScript`) case 'radiobutton': - return new RadiobuttonValidator(...arguments) + throw new Error(`${fieldType} has been migrated to TypeScript`) case 'dropdown': return new DropdownValidator(...arguments) case 'checkbox': diff --git a/src/app/utils/field-validation/config.js b/src/app/utils/field-validation/config.js index 9c271ed843..f7bdbe84b1 100644 --- a/src/app/utils/field-validation/config.js +++ b/src/app/utils/field-validation/config.js @@ -7,7 +7,6 @@ const ALLOWED_VALIDATORS = [ 'YesNoValidator', 'EmailValidator', 'DropdownValidator', - 'RadiobuttonValidator', 'DecimalValidator', 'NumberValidator', 'MobileValidator', diff --git a/src/app/utils/field-validation/index.ts b/src/app/utils/field-validation/index.ts index 82bcd79030..b38c11ae02 100644 --- a/src/app/utils/field-validation/index.ts +++ b/src/app/utils/field-validation/index.ts @@ -110,7 +110,8 @@ export const validateField = ( case BasicField.ShortText: case BasicField.LongText: case BasicField.Nric: - case BasicField.HomeNo: { + case BasicField.HomeNo: + case BasicField.Radio: { const validator = constructSingleAnswerValidator(formField) const validEither = validator(response) if (isLeft(validEither)) { diff --git a/src/app/utils/field-validation/singleAnswerValidator.factory.ts b/src/app/utils/field-validation/singleAnswerValidator.factory.ts index bbe3a36635..4d5b553054 100644 --- a/src/app/utils/field-validation/singleAnswerValidator.factory.ts +++ b/src/app/utils/field-validation/singleAnswerValidator.factory.ts @@ -5,6 +5,7 @@ import { isHomeNumberField, isLongTextField, isNricField, + isRadioButtonField, isSectionField, isShortTextField, } from '../../../types/field/utils/guards' @@ -13,6 +14,7 @@ import { ProcessedSingleAnswerResponse } from '../../modules/submission/submissi import { constructHomeNoValidator } from './validators/homeNoValidator' import { constructNricValidator } from './validators/nricValidator' +import { constructRadioButtonValidator } from './validators/radioButtonValidator' import { constructSectionValidator } from './validators/sectionValidator' import constructTextValidator from './validators/textValidator' @@ -31,6 +33,8 @@ export const constructSingleAnswerValidator = ( return constructNricValidator() } else if (isHomeNumberField(formField)) { return constructHomeNoValidator(formField) + } else if (isRadioButtonField(formField)) { + return constructRadioButtonValidator(formField) } return () => left('Unsupported field type') } diff --git a/src/app/utils/field-validation/validators/RadiobuttonValidator.class.js b/src/app/utils/field-validation/validators/RadiobuttonValidator.class.js deleted file mode 100644 index 6dda9adf34..0000000000 --- a/src/app/utils/field-validation/validators/RadiobuttonValidator.class.js +++ /dev/null @@ -1,14 +0,0 @@ -const BaseFieldValidator = require('./BaseFieldValidator.class') -const { isOneOfOptions, isOtherOption } = require('./options') -class RadiobuttonValidator extends BaseFieldValidator { - _isFilledAnswerValid() { - const { answer } = this.response - const { fieldOptions, othersRadioButton } = this.formField - const isValid = - isOneOfOptions(fieldOptions, answer) || - isOtherOption(othersRadioButton, answer) - this.logIfInvalid(isValid, `RadiobuttonValidator._isFilledAnswerValid`) - return isValid - } -} -module.exports = RadiobuttonValidator diff --git a/src/app/utils/field-validation/validators/index.js b/src/app/utils/field-validation/validators/index.js index 6ccf4e786d..d995ba465d 100644 --- a/src/app/utils/field-validation/validators/index.js +++ b/src/app/utils/field-validation/validators/index.js @@ -5,7 +5,6 @@ module.exports.DropdownValidator = require('./DropdownValidator.class') module.exports.EmailValidator = require('./EmailValidator.class') module.exports.MobileValidator = require('./MobileValidator.class') module.exports.NumberValidator = require('./NumberValidator.class') -module.exports.RadiobuttonValidator = require('./RadiobuttonValidator.class') module.exports.TableValidator = require('./TableValidator.class') module.exports.TextValidator = require('./TextValidator.class') module.exports.YesNoValidator = require('./YesNoValidator.class') diff --git a/src/app/utils/field-validation/validators/radioButtonValidator.ts b/src/app/utils/field-validation/validators/radioButtonValidator.ts new file mode 100644 index 0000000000..4474633be9 --- /dev/null +++ b/src/app/utils/field-validation/validators/radioButtonValidator.ts @@ -0,0 +1,36 @@ +import { chain, left, right } from 'fp-ts/lib/Either' +import { pipe } from 'fp-ts/lib/function' + +import { IRadioField } from 'src/types/field' +import { ResponseValidator } from 'src/types/field/utils/validation' +import { ISingleAnswerResponse } from 'src/types/response' + +import { notEmptySingleAnswerResponse } from './common' +import { isOneOfOptions, isOtherOption } from './options' + +type RadioButtonValidator = ResponseValidator +type RadioButtonValidatorConstructor = ( + radioButtonField: IRadioField, +) => RadioButtonValidator + +const radioButtonValidator: RadioButtonValidatorConstructor = ( + radioButtonField, +) => (response) => { + const { answer } = response + const { fieldOptions, othersRadioButton } = radioButtonField + const isValid = + isOneOfOptions(fieldOptions, answer) || + isOtherOption(othersRadioButton, answer) + + return isValid + ? right(response) + : left(`RadioButtonValidator:\tanswer is not a valid radio button option`) +} + +export const constructRadioButtonValidator: RadioButtonValidatorConstructor = ( + radioButtonField, +) => (response) => + pipe( + notEmptySingleAnswerResponse(response), + chain(radioButtonValidator(radioButtonField)), + ) diff --git a/src/types/field/utils/guards.ts b/src/types/field/utils/guards.ts index 3d1f7e4681..c648b5da61 100644 --- a/src/types/field/utils/guards.ts +++ b/src/types/field/utils/guards.ts @@ -3,6 +3,7 @@ import { IHomenoField, ILongTextField, INricField, + IRadioField, ISectionFieldSchema, IShortTextField, } from 'src/types/field' @@ -37,3 +38,9 @@ export const isHomeNumberField = ( ): formField is IHomenoField => { return formField.fieldType === BasicField.HomeNo } + +export const isRadioButtonField = ( + formField: IField, +): formField is IRadioField => { + return formField.fieldType === BasicField.Radio +} diff --git a/tests/unit/backend/utils/field-validation/radio-button-validation.spec.js b/tests/unit/backend/utils/field-validation/radio-button-validation.spec.js index d1e36da508..863e220c4a 100644 --- a/tests/unit/backend/utils/field-validation/radio-button-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/radio-button-validation.spec.js @@ -15,6 +15,7 @@ describe('Radio button validation', () => { _id: 'radioID', fieldType: 'radiobutton', answer: 'a', + isVisible: true, } const validateResult = validateField('formId', formField, response) expect(validateResult.isOk()).toBe(true) @@ -31,6 +32,7 @@ describe('Radio button validation', () => { _id: 'radioID', fieldType: 'radiobutton', answer: 'invalid', + isVisible: true, } const validateResult = validateField('formId', formField, response) expect(validateResult.isErr()).toBe(true) @@ -69,6 +71,7 @@ describe('Radio button validation', () => { _id: 'radioID', fieldType: 'radiobutton', answer: '', + isVisible: true, } const validateResult = validateField('formId', formField, response) expect(validateResult.isOk()).toBe(true) @@ -142,6 +145,7 @@ describe('Radio button validation', () => { _id: 'radioID', fieldType: 'radiobutton', answer: 'Others: hi i am others', + isVisible: true, } const validateResult = validateField('formId', formField, response) expect(validateResult.isOk()).toBe(true) @@ -159,6 +163,7 @@ describe('Radio button validation', () => { _id: 'radioID', fieldType: 'radiobutton', answer: 'Others: hi i am others', + isVisible: true, } const validateResult = validateField('formId', formField, response) expect(validateResult.isErr()).toBe(true) @@ -179,6 +184,7 @@ describe('Radio button validation', () => { _id: 'radioID', fieldType: 'radiobutton', answer: 'Others: ', + isVisible: true, } const validateResult = validateField('formId', formField, response) expect(validateResult.isErr()).toBe(true) From ea7c6e8e0633c3d4971071525227083396fa430b Mon Sep 17 00:00:00 2001 From: tshuli <63710093+tshuli@users.noreply.github.com> Date: Wed, 25 Nov 2020 12:08:09 +0800 Subject: [PATCH 08/34] refactor: migrate rating validator to ts (#724) * refactor: migrate ratingValidator to ts * refactor: update references --- .../FieldValidatorFactory.class.js | 5 +-- src/app/utils/field-validation/config.js | 1 - src/app/utils/field-validation/index.ts | 3 +- .../singleAnswerValidator.factory.ts | 4 ++ .../validators/RatingValidator.class.js | 27 -------------- .../field-validation/validators/index.js | 1 - .../validators/ratingValidator.ts | 37 +++++++++++++++++++ src/types/field/utils/guards.ts | 5 +++ 8 files changed, 50 insertions(+), 33 deletions(-) delete mode 100644 src/app/utils/field-validation/validators/RatingValidator.class.js create mode 100644 src/app/utils/field-validation/validators/ratingValidator.ts diff --git a/src/app/utils/field-validation/FieldValidatorFactory.class.js b/src/app/utils/field-validation/FieldValidatorFactory.class.js index 535e0e0d6f..75ab813e9e 100644 --- a/src/app/utils/field-validation/FieldValidatorFactory.class.js +++ b/src/app/utils/field-validation/FieldValidatorFactory.class.js @@ -10,7 +10,6 @@ const { DecimalValidator, AttachmentValidator, DateValidator, - RatingValidator, } = require('./validators') const myInfoTypes = require('../../../shared/resources/myinfo').types @@ -41,6 +40,8 @@ class FieldValidatorFactory { case 'textarea': // long text case 'nric': case 'homeno': + case 'rating': + throw new Error(`${fieldType} has been migrated to TypeScript`) case 'radiobutton': throw new Error(`${fieldType} has been migrated to TypeScript`) case 'dropdown': @@ -53,8 +54,6 @@ class FieldValidatorFactory { return new TableValidator(...arguments) case 'number': return new NumberValidator(...arguments) - case 'rating': - return new RatingValidator(...arguments) case 'yes_no': return new YesNoValidator(...arguments) case 'decimal': diff --git a/src/app/utils/field-validation/config.js b/src/app/utils/field-validation/config.js index f7bdbe84b1..c28147f316 100644 --- a/src/app/utils/field-validation/config.js +++ b/src/app/utils/field-validation/config.js @@ -10,7 +10,6 @@ const ALLOWED_VALIDATORS = [ 'DecimalValidator', 'NumberValidator', 'MobileValidator', - 'RatingValidator', 'TextValidator', 'DateValidator', 'TableValidator', diff --git a/src/app/utils/field-validation/index.ts b/src/app/utils/field-validation/index.ts index b38c11ae02..c6817fbb3f 100644 --- a/src/app/utils/field-validation/index.ts +++ b/src/app/utils/field-validation/index.ts @@ -111,7 +111,8 @@ export const validateField = ( case BasicField.LongText: case BasicField.Nric: case BasicField.HomeNo: - case BasicField.Radio: { + case BasicField.Radio: + case BasicField.Rating: { const validator = constructSingleAnswerValidator(formField) const validEither = validator(response) if (isLeft(validEither)) { diff --git a/src/app/utils/field-validation/singleAnswerValidator.factory.ts b/src/app/utils/field-validation/singleAnswerValidator.factory.ts index 4d5b553054..aadb4a046a 100644 --- a/src/app/utils/field-validation/singleAnswerValidator.factory.ts +++ b/src/app/utils/field-validation/singleAnswerValidator.factory.ts @@ -6,6 +6,7 @@ import { isLongTextField, isNricField, isRadioButtonField, + isRatingField, isSectionField, isShortTextField, } from '../../../types/field/utils/guards' @@ -15,6 +16,7 @@ import { ProcessedSingleAnswerResponse } from '../../modules/submission/submissi import { constructHomeNoValidator } from './validators/homeNoValidator' import { constructNricValidator } from './validators/nricValidator' import { constructRadioButtonValidator } from './validators/radioButtonValidator' +import { constructRatingValidator } from './validators/ratingValidator' import { constructSectionValidator } from './validators/sectionValidator' import constructTextValidator from './validators/textValidator' @@ -35,6 +37,8 @@ export const constructSingleAnswerValidator = ( return constructHomeNoValidator(formField) } else if (isRadioButtonField(formField)) { return constructRadioButtonValidator(formField) + } else if (isRatingField(formField)) { + return constructRatingValidator(formField) } return () => left('Unsupported field type') } diff --git a/src/app/utils/field-validation/validators/RatingValidator.class.js b/src/app/utils/field-validation/validators/RatingValidator.class.js deleted file mode 100644 index 8ede155b5c..0000000000 --- a/src/app/utils/field-validation/validators/RatingValidator.class.js +++ /dev/null @@ -1,27 +0,0 @@ -const { isInt } = require('validator') - -const BaseFieldValidator = require('./BaseFieldValidator.class') - -class RatingValidator extends BaseFieldValidator { - /** - * Validates a Rating field. If present, the answer must be - * between 1 and the maximum steps allowed. - * @returns {Boolean} - * @memberof BaseFieldValidator - */ - _isFilledAnswerValid() { - const { answer } = this.response - const { steps } = this.formField.ratingOptions - - const isValid = isInt(answer, { - min: 1, - max: steps, - allow_leading_zeroes: false, - }) - - this.logIfInvalid(isValid, `RatingValidator._isFilledAnswerValid`) - return isValid - } -} - -module.exports = RatingValidator diff --git a/src/app/utils/field-validation/validators/index.js b/src/app/utils/field-validation/validators/index.js index d995ba465d..0d3d3da308 100644 --- a/src/app/utils/field-validation/validators/index.js +++ b/src/app/utils/field-validation/validators/index.js @@ -9,5 +9,4 @@ module.exports.TableValidator = require('./TableValidator.class') module.exports.TextValidator = require('./TextValidator.class') module.exports.YesNoValidator = require('./YesNoValidator.class') module.exports.DateValidator = require('./DateValidator.class') -module.exports.RatingValidator = require('./RatingValidator.class') module.exports.AttachmentValidator = require('./AttachmentValidator.class') diff --git a/src/app/utils/field-validation/validators/ratingValidator.ts b/src/app/utils/field-validation/validators/ratingValidator.ts new file mode 100644 index 0000000000..71fcdc3e8d --- /dev/null +++ b/src/app/utils/field-validation/validators/ratingValidator.ts @@ -0,0 +1,37 @@ +import { chain, left, right } from 'fp-ts/lib/Either' +import { pipe } from 'fp-ts/lib/function' +import isInt from 'validator/lib/isInt' + +import { IRatingField } from 'src/types/field' +import { ResponseValidator } from 'src/types/field/utils/validation' +import { ISingleAnswerResponse } from 'src/types/response' + +import { notEmptySingleAnswerResponse } from './common' + +type RatingValidator = ResponseValidator +type RatingValidatorConstructor = (ratingField: IRatingField) => RatingValidator + +const ratingValidator: RatingValidatorConstructor = (ratingField) => ( + response, +) => { + const { answer } = response + const { steps } = ratingField.ratingOptions + + const isValid = isInt(answer, { + min: 1, + max: steps, + allow_leading_zeroes: false, + }) + + return isValid + ? right(response) + : left(`RatingValidator:\t answer is not a valid rating`) +} + +export const constructRatingValidator: RatingValidatorConstructor = ( + ratingField, +) => (response) => + pipe( + notEmptySingleAnswerResponse(response), + chain(ratingValidator(ratingField)), + ) diff --git a/src/types/field/utils/guards.ts b/src/types/field/utils/guards.ts index c648b5da61..4f6bd95798 100644 --- a/src/types/field/utils/guards.ts +++ b/src/types/field/utils/guards.ts @@ -4,6 +4,7 @@ import { ILongTextField, INricField, IRadioField, + IRatingField, ISectionFieldSchema, IShortTextField, } from 'src/types/field' @@ -44,3 +45,7 @@ export const isRadioButtonField = ( ): formField is IRadioField => { return formField.fieldType === BasicField.Radio } + +export const isRatingField = (formField: IField): formField is IRatingField => { + return formField.fieldType === BasicField.Rating +} From eb93d5d23a65eefc05dd2fe01d8bbd0639e0c29b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Nov 2020 12:51:34 +0800 Subject: [PATCH 09/34] chore(deps-dev): bump ts-jest from 26.4.1 to 26.4.4 (#720) Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 26.4.1 to 26.4.4. - [Release notes](https://github.com/kulshekhar/ts-jest/releases) - [Changelog](https://github.com/kulshekhar/ts-jest/blob/master/CHANGELOG.md) - [Commits](https://github.com/kulshekhar/ts-jest/compare/v26.4.1...v26.4.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 35 +++++++++++++---------------------- package.json | 2 +- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index abe5129542..c9a1830658 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16302,12 +16302,12 @@ } }, "jest-util": { - "version": "26.6.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.0.tgz", - "integrity": "sha512-/cUGqcnKeZMjvTQLfJo65nBOEZ/k0RB/8usv2JpfYya05u0XvBmKkIH5o5c4nCh9DD61B1YQjMGGqh1Ha0aXdg==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", "dev": true, "requires": { - "@jest/types": "^26.6.0", + "@jest/types": "^26.6.2", "@types/node": "*", "chalk": "^4.0.0", "graceful-fs": "^4.2.4", @@ -16316,9 +16316,9 @@ }, "dependencies": { "@jest/types": { - "version": "26.6.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.0.tgz", - "integrity": "sha512-8pDeq/JVyAYw7jBGU83v8RMYAkdrRxLG3BGnAJuqaQAUd6GWBmND2uyl+awI88+hit48suLoLjNFtR+ZXxWaYg==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", @@ -16328,15 +16328,6 @@ "chalk": "^4.0.0" } }, - "@types/istanbul-reports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", - "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", - "dev": true, - "requires": { - "@types/istanbul-lib-report": "*" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -25306,9 +25297,9 @@ } }, "ts-jest": { - "version": "26.4.1", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.4.1.tgz", - "integrity": "sha512-F4aFq01aS6mnAAa0DljNmKr/Kk9y4HVZ1m6/rtJ0ED56cuxINGq3Q9eVAh+z5vcYKe5qnTMvv90vE8vUMFxomg==", + "version": "26.4.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.4.4.tgz", + "integrity": "sha512-3lFWKbLxJm34QxyVNNCgXX1u4o/RV0myvA2y2Bxm46iGIjKlaY0own9gIckbjZJPn+WaJEnfPPJ20HHGpoq4yg==", "dev": true, "requires": { "@types/jest": "26.x", @@ -25346,9 +25337,9 @@ "dev": true }, "yargs-parser": { - "version": "20.2.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.3.tgz", - "integrity": "sha512-emOFRT9WVHw03QSvN5qor9QQT9+sw5vwxfYweivSMHTcAXPefwVae2FjO7JJjj8hCE4CzPOPeFM83VwT29HCww==", + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", "dev": true } } diff --git a/package.json b/package.json index 9c067a1be7..5b98fb7486 100644 --- a/package.json +++ b/package.json @@ -247,7 +247,7 @@ "supertest-session": "^4.1.0", "terser-webpack-plugin": "^1.2.3", "testcafe": "^1.9.4", - "ts-jest": "^26.4.1", + "ts-jest": "^26.4.4", "ts-loader": "^7.0.5", "ts-mock-imports": "^1.3.0", "ts-node": "^9.0.0", From 9b84a2f9b6749f219dd98febb51b0ae1c549f706 Mon Sep 17 00:00:00 2001 From: tshuli <63710093+tshuli@users.noreply.github.com> Date: Wed, 25 Nov 2020 20:19:58 +0800 Subject: [PATCH 10/34] refactor: migrate mobile number validator to ts (#713) * refactor: migrate mobileValidator to ts * refactor: update references * chore: correct param name * refactor: improve clarity of prefixValidator --- .../FieldValidatorFactory.class.js | 4 +- src/app/utils/field-validation/config.js | 1 + src/app/utils/field-validation/index.ts | 3 +- .../singleAnswerValidator.factory.ts | 4 ++ .../validators/MobileValidator.class.js | 28 ----------- .../validators/homeNoValidator.ts | 13 ++---- .../field-validation/validators/index.js | 1 - .../validators/mobileNoValidator.ts | 46 +++++++++++++++++++ src/types/field/utils/guards.ts | 7 +++ 9 files changed, 66 insertions(+), 41 deletions(-) delete mode 100644 src/app/utils/field-validation/validators/MobileValidator.class.js create mode 100644 src/app/utils/field-validation/validators/mobileNoValidator.ts diff --git a/src/app/utils/field-validation/FieldValidatorFactory.class.js b/src/app/utils/field-validation/FieldValidatorFactory.class.js index 75ab813e9e..b30f939c21 100644 --- a/src/app/utils/field-validation/FieldValidatorFactory.class.js +++ b/src/app/utils/field-validation/FieldValidatorFactory.class.js @@ -5,7 +5,6 @@ const { TableValidator, NumberValidator, YesNoValidator, - MobileValidator, BaseFieldValidator, DecimalValidator, AttachmentValidator, @@ -41,6 +40,7 @@ class FieldValidatorFactory { case 'nric': case 'homeno': case 'rating': + case 'mobileno': throw new Error(`${fieldType} has been migrated to TypeScript`) case 'radiobutton': throw new Error(`${fieldType} has been migrated to TypeScript`) @@ -62,8 +62,6 @@ class FieldValidatorFactory { return new AttachmentValidator(...arguments) case 'date': return new DateValidator(...arguments) - case 'mobile': - return new MobileValidator(...arguments) default: // Checks if answer is optional or required, but will throw an error when there is an answer // since _isFilledAnswerValid is not implemented diff --git a/src/app/utils/field-validation/config.js b/src/app/utils/field-validation/config.js index c28147f316..f7bdbe84b1 100644 --- a/src/app/utils/field-validation/config.js +++ b/src/app/utils/field-validation/config.js @@ -10,6 +10,7 @@ const ALLOWED_VALIDATORS = [ 'DecimalValidator', 'NumberValidator', 'MobileValidator', + 'RatingValidator', 'TextValidator', 'DateValidator', 'TableValidator', diff --git a/src/app/utils/field-validation/index.ts b/src/app/utils/field-validation/index.ts index c6817fbb3f..708b47b21f 100644 --- a/src/app/utils/field-validation/index.ts +++ b/src/app/utils/field-validation/index.ts @@ -112,7 +112,8 @@ export const validateField = ( case BasicField.Nric: case BasicField.HomeNo: case BasicField.Radio: - case BasicField.Rating: { + case BasicField.Rating: + case BasicField.Mobile: { const validator = constructSingleAnswerValidator(formField) const validEither = validator(response) if (isLeft(validEither)) { diff --git a/src/app/utils/field-validation/singleAnswerValidator.factory.ts b/src/app/utils/field-validation/singleAnswerValidator.factory.ts index aadb4a046a..3b3579cb65 100644 --- a/src/app/utils/field-validation/singleAnswerValidator.factory.ts +++ b/src/app/utils/field-validation/singleAnswerValidator.factory.ts @@ -4,6 +4,7 @@ import { IField } from '../../../types/field/baseField' import { isHomeNumberField, isLongTextField, + isMobileNumberField, isNricField, isRadioButtonField, isRatingField, @@ -14,6 +15,7 @@ import { ResponseValidator } from '../../../types/field/utils/validation' import { ProcessedSingleAnswerResponse } from '../../modules/submission/submission.types' import { constructHomeNoValidator } from './validators/homeNoValidator' +import { constructMobileNoValidator } from './validators/mobileNoValidator' import { constructNricValidator } from './validators/nricValidator' import { constructRadioButtonValidator } from './validators/radioButtonValidator' import { constructRatingValidator } from './validators/ratingValidator' @@ -39,6 +41,8 @@ export const constructSingleAnswerValidator = ( return constructRadioButtonValidator(formField) } else if (isRatingField(formField)) { return constructRatingValidator(formField) + } else if (isMobileNumberField(formField)) { + return constructMobileNoValidator(formField) } return () => left('Unsupported field type') } diff --git a/src/app/utils/field-validation/validators/MobileValidator.class.js b/src/app/utils/field-validation/validators/MobileValidator.class.js deleted file mode 100644 index 41a14eacfc..0000000000 --- a/src/app/utils/field-validation/validators/MobileValidator.class.js +++ /dev/null @@ -1,28 +0,0 @@ -const BaseFieldValidator = require('./BaseFieldValidator.class') -const { - isMobilePhoneNumber, - startsWithSgPrefix, -} = require('../../../../shared/util/phone-num-validation') -class MobileValidator extends BaseFieldValidator { - _isFilledAnswerValid() { - const { answer } = this.response - - let isValid = isMobilePhoneNumber(answer) - - this.logIfInvalid( - isValid, - 'MobileValidator._isFilledAnswerValid.isValidMobileNumber', - ) - - if (!this.formField.allowIntlNumbers) { - isValid = isValid && startsWithSgPrefix(answer) - this.logIfInvalid( - isValid, - 'MobileValidator._isFilledAnswerValid.isValidSgNumber', - ) - } - return isValid - } -} - -module.exports = MobileValidator diff --git a/src/app/utils/field-validation/validators/homeNoValidator.ts b/src/app/utils/field-validation/validators/homeNoValidator.ts index f79c93ea4c..6f17bceb2b 100644 --- a/src/app/utils/field-validation/validators/homeNoValidator.ts +++ b/src/app/utils/field-validation/validators/homeNoValidator.ts @@ -28,14 +28,11 @@ const homePhoneNumberValidator: HomeNoValidatorConstructor = () => ( const prefixValidator: HomeNoValidatorConstructor = (homeNumberField) => ( response, ) => { - if ( - !homeNumberField.allowIntlNumbers && - !startsWithSgPrefix(response.answer) - ) { - return left(`HomeNoValidator:\t answer is not an SG number - but intl numbers are not allowed`) - } - return right(response) + return homeNumberField.allowIntlNumbers || startsWithSgPrefix(response.answer) + ? right(response) + : left( + `HomeNoValidator:\t answer is not an SG number but intl numbers are not allowed`, + ) } export const constructHomeNoValidator: HomeNoValidatorConstructor = ( diff --git a/src/app/utils/field-validation/validators/index.js b/src/app/utils/field-validation/validators/index.js index 0d3d3da308..bfdbb34b31 100644 --- a/src/app/utils/field-validation/validators/index.js +++ b/src/app/utils/field-validation/validators/index.js @@ -3,7 +3,6 @@ module.exports.CheckboxValidator = require('./CheckboxValidator.class') module.exports.DecimalValidator = require('./DecimalValidator.class') module.exports.DropdownValidator = require('./DropdownValidator.class') module.exports.EmailValidator = require('./EmailValidator.class') -module.exports.MobileValidator = require('./MobileValidator.class') module.exports.NumberValidator = require('./NumberValidator.class') module.exports.TableValidator = require('./TableValidator.class') module.exports.TextValidator = require('./TextValidator.class') diff --git a/src/app/utils/field-validation/validators/mobileNoValidator.ts b/src/app/utils/field-validation/validators/mobileNoValidator.ts new file mode 100644 index 0000000000..c53b34881a --- /dev/null +++ b/src/app/utils/field-validation/validators/mobileNoValidator.ts @@ -0,0 +1,46 @@ +import { chain, left, right } from 'fp-ts/lib/Either' +import { pipe } from 'fp-ts/lib/function' + +import { IMobileField } from 'src/types/field' +import { ResponseValidator } from 'src/types/field/utils/validation' +import { ISingleAnswerResponse } from 'src/types/response' + +import { + isMobilePhoneNumber, + startsWithSgPrefix, +} from '../../../../shared/util/phone-num-validation' + +import { notEmptySingleAnswerResponse } from './common' + +type MobileNoValidator = ResponseValidator +type MobileNoValidatorConstructor = ( + mobileNumberField: IMobileField, +) => MobileNoValidator + +const mobilePhoneNumberValidator: MobileNoValidatorConstructor = () => ( + response, +) => { + return isMobilePhoneNumber(response.answer) + ? right(response) + : left(`MobileNoValidator:\t answer is not a valid mobile phone number`) +} + +const prefixValidator: MobileNoValidatorConstructor = (mobileNumberField) => ( + response, +) => { + return mobileNumberField.allowIntlNumbers || + startsWithSgPrefix(response.answer) + ? right(response) + : left( + `MobileNoValidator:\t answer is not an SG number but intl numbers are not allowed`, + ) +} + +export const constructMobileNoValidator: MobileNoValidatorConstructor = ( + mobileNumberField, +) => (response) => + pipe( + notEmptySingleAnswerResponse(response), + chain(mobilePhoneNumberValidator(mobileNumberField)), + chain(prefixValidator(mobileNumberField)), + ) diff --git a/src/types/field/utils/guards.ts b/src/types/field/utils/guards.ts index 4f6bd95798..5b5821a2f2 100644 --- a/src/types/field/utils/guards.ts +++ b/src/types/field/utils/guards.ts @@ -2,6 +2,7 @@ import { IHomenoField, ILongTextField, + IMobileField, INricField, IRadioField, IRatingField, @@ -49,3 +50,9 @@ export const isRadioButtonField = ( export const isRatingField = (formField: IField): formField is IRatingField => { return formField.fieldType === BasicField.Rating } + +export const isMobileNumberField = ( + formField: IField, +): formField is IMobileField => { + return formField.fieldType === BasicField.Mobile +} From 0af9198c1948f117c48c59c919c41b9be156008b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Nov 2020 21:59:12 +0800 Subject: [PATCH 11/34] fix(deps): bump winston-cloudwatch from 2.3.2 to 2.4.0 (#728) Bumps [winston-cloudwatch](https://github.com/lazywithclass/winston-cloudwatch) from 2.3.2 to 2.4.0. - [Release notes](https://github.com/lazywithclass/winston-cloudwatch/releases) - [Changelog](https://github.com/lazywithclass/winston-cloudwatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/lazywithclass/winston-cloudwatch/commits) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 50 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index c9a1830658..8609a3fbac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4341,7 +4341,8 @@ "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true }, "@types/compression": { "version": "1.7.0", @@ -19888,11 +19889,11 @@ } }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "https-proxy-agent": { @@ -19905,9 +19906,9 @@ }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "requires": { "ms": "^2.1.1" } @@ -21042,11 +21043,11 @@ } }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "https-proxy-agent": { @@ -21059,9 +21060,9 @@ }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "requires": { "ms": "^2.1.1" } @@ -26894,9 +26895,9 @@ } }, "winston-cloudwatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/winston-cloudwatch/-/winston-cloudwatch-2.3.2.tgz", - "integrity": "sha512-dUQF0ENHcNVyiEJNTowvfggDbjF7mUw7s3gWVto6pW9pxdKwr682UOXK0wlr4jwNHChD6+n288jwgShn8Spwqw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/winston-cloudwatch/-/winston-cloudwatch-2.4.0.tgz", + "integrity": "sha512-mODEWN2lK83RGx2BFUuHoxo6Aam2I36ZEUz7lz2K4V8dvYACRqy1OXyql2wL9Qs8IBSwU0XnC7XPERxiD4XWEg==", "requires": { "async": "^3.1.0", "aws-sdk": "^2.553.0", @@ -26910,11 +26911,10 @@ }, "dependencies": { "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==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -26951,9 +26951,9 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "requires": { "has-flag": "^4.0.0" } diff --git a/package.json b/package.json index 5b98fb7486..a154a08e0c 100644 --- a/package.json +++ b/package.json @@ -160,7 +160,7 @@ "web-streams-polyfill": "^2.1.1", "whatwg-fetch": "^3.4.1", "winston": "^3.3.3", - "winston-cloudwatch": "^2.3.2" + "winston-cloudwatch": "^2.4.0" }, "devDependencies": { "@babel/core": "^7.12.3", From 819221e5babfb3a021492b3324424a05d9848c82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Nov 2020 22:00:12 +0800 Subject: [PATCH 12/34] fix(deps): bump angular-messages from 1.8.1 to 1.8.2 (#725) Bumps [angular-messages](https://github.com/angular/angular.js) from 1.8.1 to 1.8.2. - [Release notes](https://github.com/angular/angular.js/releases) - [Changelog](https://github.com/angular/angular.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/angular/angular.js/compare/v1.8.1...v1.8.2) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8609a3fbac..023abf0d7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5507,9 +5507,9 @@ } }, "angular-messages": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/angular-messages/-/angular-messages-1.8.1.tgz", - "integrity": "sha512-09+sV4b51/y6oE9DRVcc6+zs2jN5a1Hf0LWO18tv9dgjit2qVR99eYlzLM5HQAaCBUN5gfzJEk2cgVrXWzkijA==" + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/angular-messages/-/angular-messages-1.8.2.tgz", + "integrity": "sha512-M1qNh/30cLJi4yJJ+3YB8saPonRcavz5Dquqz0T/aUySKJhIkUoeCkmF+BcLH4SJ5PBp04yy4CZUUeNRVi7jZA==" }, "angular-moment": { "version": "1.2.0", diff --git a/package.json b/package.json index a154a08e0c..54e5556b0c 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "angular-aria": "^1.8.2", "angular-cookies": "~1.8.2", "angular-drag-scroll": "^0.2.1", - "angular-messages": "^1.8.1", + "angular-messages": "^1.8.2", "angular-moment": "~1.2.0", "angular-permission": "~1.1.1", "angular-resource": "^1.8.2", From ea4d7d5febba1fc6207743e9a0295c431cefd771 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Nov 2020 22:01:52 +0800 Subject: [PATCH 13/34] fix(deps): bump express-rate-limit from 5.1.3 to 5.2.3 (#726) Bumps [express-rate-limit](https://github.com/nfriedly/express-rate-limit) from 5.1.3 to 5.2.3. - [Release notes](https://github.com/nfriedly/express-rate-limit/releases) - [Commits](https://github.com/nfriedly/express-rate-limit/compare/v5.1.3...v5.2.3) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 023abf0d7b..9c9e0cec9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11692,9 +11692,9 @@ "integrity": "sha1-iLnEAWSv2aVSeGKbKUjmrOe/9F8=" }, "express-rate-limit": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.1.3.tgz", - "integrity": "sha512-TINcxve5510pXj4n9/1AMupkj3iWxl3JuZaWhCdYDlZeoCPqweGZrxbrlqTCFb1CT5wli7s8e2SH/Qz2c9GorA==" + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.2.3.tgz", + "integrity": "sha512-cjQH+oDrEPXxc569XvxhHC6QXqJiuBT6BhZ70X3bdAImcnHnTNMVuMAJaT0TXPoRiEErUrVPRcOTpZpM36VbOQ==" }, "express-request-id": { "version": "1.4.1", diff --git a/package.json b/package.json index 54e5556b0c..99a9b5c82f 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "ejs": "^3.1.5", "express": "^4.16.4", "express-device": "~0.4.2", - "express-rate-limit": "^5.1.3", + "express-rate-limit": "^5.2.3", "express-request-id": "^1.4.1", "express-session": "^1.15.6", "express-winston": "^4.0.5", From 954055fd07a776f9611b8f8f62ef6b04f8f9ab29 Mon Sep 17 00:00:00 2001 From: Kar Rui Lau Date: Thu, 26 Nov 2020 11:19:55 +0800 Subject: [PATCH 14/34] feat: remove .oa file from allowed file types (#731) --- src/shared/util/file-validation.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/shared/util/file-validation.ts b/src/shared/util/file-validation.ts index a5ae0e0418..1b17a33e30 100644 --- a/src/shared/util/file-validation.ts +++ b/src/shared/util/file-validation.ts @@ -34,7 +34,6 @@ const validExtensions = [ '.mpp', '.msg', '.mso', - '.oa', '.odb', '.odf', '.odg', From 4602ebb3ee0a5879afdfde5d590493c13bc70a73 Mon Sep 17 00:00:00 2001 From: Kar Rui Lau Date: Thu, 26 Nov 2020 13:59:44 +0800 Subject: [PATCH 15/34] feat(utils): add exhaustive switch case typeguard (#729) --- src/app/modules/form/form.service.ts | 15 ++++++--------- .../__tests__/submission/submission.utils.spec.ts | 6 ++++-- src/app/modules/submission/submission.utils.ts | 7 +++++-- src/app/utils/assert-unreachable.ts | 10 ++++++++++ 4 files changed, 25 insertions(+), 13 deletions(-) create mode 100644 src/app/utils/assert-unreachable.ts diff --git a/src/app/modules/form/form.service.ts b/src/app/modules/form/form.service.ts index 4e390a53fe..136e2bf4a3 100644 --- a/src/app/modules/form/form.service.ts +++ b/src/app/modules/form/form.service.ts @@ -4,6 +4,7 @@ import { err, errAsync, ok, okAsync, Result, ResultAsync } from 'neverthrow' import { createLoggerWithLabel } from '../../../config/logger' import { IFormSchema, IPopulatedForm, Status } from '../../../types' import getFormModel from '../../models/form.server.model' +import { assertUnreachable } from '../../utils/assert-unreachable' import { ApplicationError, DatabaseError } from '../core/core.errors' import { @@ -67,6 +68,10 @@ export const retrieveFullFormById = ( export const isFormPublic = ( form: IPopulatedForm, ): Result => { + if (!form.status) { + return err(new ApplicationError()) + } + switch (form.status) { case Status.Public: return ok(true) @@ -75,14 +80,6 @@ export const isFormPublic = ( case Status.Private: return err(new PrivateFormError(form.inactiveMessage)) default: - logger.error({ - message: 'Encountered invalid form status', - meta: { - action: 'isFormPublic', - formStatus: form.status, - form, - }, - }) - return err(new ApplicationError()) + return assertUnreachable(form.status) } } diff --git a/src/app/modules/submission/__tests__/submission/submission.utils.spec.ts b/src/app/modules/submission/__tests__/submission/submission.utils.spec.ts index 0e7e9ed8b0..fe448f4340 100644 --- a/src/app/modules/submission/__tests__/submission/submission.utils.spec.ts +++ b/src/app/modules/submission/__tests__/submission/submission.utils.spec.ts @@ -46,9 +46,11 @@ describe('submission.utils', () => { }) it('should throw error if called with invalid responseMode', async () => { + // Arrange + const invalidResponseMode = 'something' as ResponseMode // Act + Assert - expect(() => getModeFilter(undefined!)).toThrowError( - 'getResponsesForEachField: Invalid response mode parameter', + expect(() => getModeFilter(invalidResponseMode)).toThrowError( + `This should never be reached in TypeScript: "${invalidResponseMode}"`, ) }) }) diff --git a/src/app/modules/submission/submission.utils.ts b/src/app/modules/submission/submission.utils.ts index 2dce7da9ac..39aceea9cf 100644 --- a/src/app/modules/submission/submission.utils.ts +++ b/src/app/modules/submission/submission.utils.ts @@ -1,18 +1,21 @@ import { BasicField, ResponseMode } from '../../../types' +import { assertUnreachable } from '../../utils/assert-unreachable' import { FIELDS_TO_REJECT } from '../../utils/field-validation/config' type ModeFilterParam = { fieldType: BasicField } -export const getModeFilter = (responseMode: ResponseMode) => { +export const getModeFilter = ( + responseMode: ResponseMode, +): ((responses: T[]) => T[]) => { switch (responseMode) { case ResponseMode.Email: return emailModeFilter case ResponseMode.Encrypt: return encryptModeFilter default: - throw Error('getResponsesForEachField: Invalid response mode parameter') + return assertUnreachable(responseMode) } } diff --git a/src/app/utils/assert-unreachable.ts b/src/app/utils/assert-unreachable.ts new file mode 100644 index 0000000000..bb4fca757f --- /dev/null +++ b/src/app/utils/assert-unreachable.ts @@ -0,0 +1,10 @@ +/** + * Typescript type guard that asserts that all switch cases are exhaustive. + * Use to get compile-time safety for making sure all the cases are handled. + * + * See: + * https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript + */ +export const assertUnreachable = (switchCase: never): never => { + throw new Error(`This should never be reached in TypeScript: "${switchCase}"`) +} From b08a588cee8a93c5d1c73b35e27d23dfda079115 Mon Sep 17 00:00:00 2001 From: Antariksh Mahajan Date: Thu, 26 Nov 2020 15:04:01 +0800 Subject: [PATCH 16/34] refactor(field-validation): simplify (#734) * ref: pass length validator function to chain * ref: use flow instead of pipe * ref: cut indirection in homeno * ref: cut indirection in mobileno * ref: cut indirection in nric * ref: cut indirection in radiobutton * ref: cut indirection in rating * ref: improve variable name for textfield --- .../validators/homeNoValidator.ts | 26 ++++++++--------- .../validators/mobileNoValidator.ts | 29 ++++++++++--------- .../validators/nricValidator.ts | 7 ++--- .../validators/radioButtonValidator.ts | 12 ++++---- .../validators/ratingValidator.ts | 12 ++++---- .../validators/textValidator.ts | 24 +++++++-------- 6 files changed, 53 insertions(+), 57 deletions(-) diff --git a/src/app/utils/field-validation/validators/homeNoValidator.ts b/src/app/utils/field-validation/validators/homeNoValidator.ts index 6f17bceb2b..dd5696d242 100644 --- a/src/app/utils/field-validation/validators/homeNoValidator.ts +++ b/src/app/utils/field-validation/validators/homeNoValidator.ts @@ -1,5 +1,5 @@ import { chain, left, right } from 'fp-ts/lib/Either' -import { pipe } from 'fp-ts/lib/function' +import { flow } from 'fp-ts/lib/function' import { IHomenoField } from 'src/types/field' import { ResponseValidator } from 'src/types/field/utils/validation' @@ -17,29 +17,29 @@ type HomeNoValidatorConstructor = ( homeNumberField: IHomenoField, ) => HomeNoValidator -const homePhoneNumberValidator: HomeNoValidatorConstructor = () => ( - response, -) => { +const homePhoneNumberValidator: HomeNoValidator = (response) => { return isHomePhoneNumber(response.answer) ? right(response) : left(`HomeNoValidator:\t answer is not a valid home phone number`) } -const prefixValidator: HomeNoValidatorConstructor = (homeNumberField) => ( - response, -) => { - return homeNumberField.allowIntlNumbers || startsWithSgPrefix(response.answer) +const sgPrefixValidator: HomeNoValidator = (response) => { + return startsWithSgPrefix(response.answer) ? right(response) : left( `HomeNoValidator:\t answer is not an SG number but intl numbers are not allowed`, ) } +const makePrefixValidator: HomeNoValidatorConstructor = (homeNumberField) => { + return homeNumberField.allowIntlNumbers ? right : sgPrefixValidator +} + export const constructHomeNoValidator: HomeNoValidatorConstructor = ( homeNumberField, -) => (response) => - pipe( - notEmptySingleAnswerResponse(response), - chain(homePhoneNumberValidator(homeNumberField)), - chain(prefixValidator(homeNumberField)), +) => + flow( + notEmptySingleAnswerResponse, + chain(homePhoneNumberValidator), + chain(makePrefixValidator(homeNumberField)), ) diff --git a/src/app/utils/field-validation/validators/mobileNoValidator.ts b/src/app/utils/field-validation/validators/mobileNoValidator.ts index c53b34881a..41ade8dc62 100644 --- a/src/app/utils/field-validation/validators/mobileNoValidator.ts +++ b/src/app/utils/field-validation/validators/mobileNoValidator.ts @@ -1,5 +1,5 @@ import { chain, left, right } from 'fp-ts/lib/Either' -import { pipe } from 'fp-ts/lib/function' +import { flow } from 'fp-ts/lib/function' import { IMobileField } from 'src/types/field' import { ResponseValidator } from 'src/types/field/utils/validation' @@ -17,30 +17,31 @@ type MobileNoValidatorConstructor = ( mobileNumberField: IMobileField, ) => MobileNoValidator -const mobilePhoneNumberValidator: MobileNoValidatorConstructor = () => ( - response, -) => { +const mobilePhoneNumberValidator: MobileNoValidator = (response) => { return isMobilePhoneNumber(response.answer) ? right(response) : left(`MobileNoValidator:\t answer is not a valid mobile phone number`) } -const prefixValidator: MobileNoValidatorConstructor = (mobileNumberField) => ( - response, -) => { - return mobileNumberField.allowIntlNumbers || - startsWithSgPrefix(response.answer) +const sgPrefixValidator: MobileNoValidator = (response) => { + return startsWithSgPrefix(response.answer) ? right(response) : left( `MobileNoValidator:\t answer is not an SG number but intl numbers are not allowed`, ) } +const makePrefixValidator: MobileNoValidatorConstructor = ( + mobileNumberField, +) => { + return mobileNumberField.allowIntlNumbers ? right : sgPrefixValidator +} + export const constructMobileNoValidator: MobileNoValidatorConstructor = ( mobileNumberField, -) => (response) => - pipe( - notEmptySingleAnswerResponse(response), - chain(mobilePhoneNumberValidator(mobileNumberField)), - chain(prefixValidator(mobileNumberField)), +) => + flow( + notEmptySingleAnswerResponse, + chain(mobilePhoneNumberValidator), + chain(makePrefixValidator(mobileNumberField)), ) diff --git a/src/app/utils/field-validation/validators/nricValidator.ts b/src/app/utils/field-validation/validators/nricValidator.ts index e0289a9d3b..75e5a8f8ba 100644 --- a/src/app/utils/field-validation/validators/nricValidator.ts +++ b/src/app/utils/field-validation/validators/nricValidator.ts @@ -1,5 +1,5 @@ import { chain, left, right } from 'fp-ts/lib/Either' -import { pipe } from 'fp-ts/lib/function' +import { flow } from 'fp-ts/lib/function' import { ResponseValidator } from 'src/types/field/utils/validation' import { ISingleAnswerResponse } from 'src/types/response' @@ -17,6 +17,5 @@ const nricValidator: NricValidator = (response) => { : left(`NricValidator:\tanswer is not a valid NRIC`) } -export const constructNricValidator: NricValidatorConstructor = () => ( - response, -) => pipe(notEmptySingleAnswerResponse(response), chain(nricValidator)) +export const constructNricValidator: NricValidatorConstructor = () => + flow(notEmptySingleAnswerResponse, chain(nricValidator)) diff --git a/src/app/utils/field-validation/validators/radioButtonValidator.ts b/src/app/utils/field-validation/validators/radioButtonValidator.ts index 4474633be9..0a6ec426e7 100644 --- a/src/app/utils/field-validation/validators/radioButtonValidator.ts +++ b/src/app/utils/field-validation/validators/radioButtonValidator.ts @@ -1,5 +1,5 @@ import { chain, left, right } from 'fp-ts/lib/Either' -import { pipe } from 'fp-ts/lib/function' +import { flow } from 'fp-ts/lib/function' import { IRadioField } from 'src/types/field' import { ResponseValidator } from 'src/types/field/utils/validation' @@ -13,7 +13,7 @@ type RadioButtonValidatorConstructor = ( radioButtonField: IRadioField, ) => RadioButtonValidator -const radioButtonValidator: RadioButtonValidatorConstructor = ( +const makeRadioOptionsValidator: RadioButtonValidatorConstructor = ( radioButtonField, ) => (response) => { const { answer } = response @@ -29,8 +29,8 @@ const radioButtonValidator: RadioButtonValidatorConstructor = ( export const constructRadioButtonValidator: RadioButtonValidatorConstructor = ( radioButtonField, -) => (response) => - pipe( - notEmptySingleAnswerResponse(response), - chain(radioButtonValidator(radioButtonField)), +) => + flow( + notEmptySingleAnswerResponse, + chain(makeRadioOptionsValidator(radioButtonField)), ) diff --git a/src/app/utils/field-validation/validators/ratingValidator.ts b/src/app/utils/field-validation/validators/ratingValidator.ts index 71fcdc3e8d..2aa40d4cad 100644 --- a/src/app/utils/field-validation/validators/ratingValidator.ts +++ b/src/app/utils/field-validation/validators/ratingValidator.ts @@ -1,5 +1,5 @@ import { chain, left, right } from 'fp-ts/lib/Either' -import { pipe } from 'fp-ts/lib/function' +import { flow } from 'fp-ts/lib/function' import isInt from 'validator/lib/isInt' import { IRatingField } from 'src/types/field' @@ -11,7 +11,7 @@ import { notEmptySingleAnswerResponse } from './common' type RatingValidator = ResponseValidator type RatingValidatorConstructor = (ratingField: IRatingField) => RatingValidator -const ratingValidator: RatingValidatorConstructor = (ratingField) => ( +const makeRatingLimitsValidator: RatingValidatorConstructor = (ratingField) => ( response, ) => { const { answer } = response @@ -30,8 +30,8 @@ const ratingValidator: RatingValidatorConstructor = (ratingField) => ( export const constructRatingValidator: RatingValidatorConstructor = ( ratingField, -) => (response) => - pipe( - notEmptySingleAnswerResponse(response), - chain(ratingValidator(ratingField)), +) => + flow( + notEmptySingleAnswerResponse, + chain(makeRatingLimitsValidator(ratingField)), ) diff --git a/src/app/utils/field-validation/validators/textValidator.ts b/src/app/utils/field-validation/validators/textValidator.ts index 503324bb63..e723a2aeb8 100644 --- a/src/app/utils/field-validation/validators/textValidator.ts +++ b/src/app/utils/field-validation/validators/textValidator.ts @@ -1,5 +1,5 @@ import { chain, left, right } from 'fp-ts/lib/Either' -import { pipe } from 'fp-ts/lib/function' +import { flow } from 'fp-ts/lib/function' import { ILongTextField, IShortTextField } from 'src/types/field' import { ResponseValidator } from 'src/types/field/utils/validation' @@ -53,27 +53,23 @@ const exactLengthValidator: TextFieldValidatorConstructor = (textField) => ( ) } -const lengthValidator: TextFieldValidatorConstructor = (textField) => ( - response, -) => { +const getLengthValidator: TextFieldValidatorConstructor = (textField) => { switch (textField.ValidationOptions.selectedValidation) { case TextSelectedValidation.Exact: - return exactLengthValidator(textField)(response) + return exactLengthValidator(textField) case TextSelectedValidation.Minimum: - return minLengthValidator(textField)(response) + return minLengthValidator(textField) case TextSelectedValidation.Maximum: - return maxLengthValidator(textField)(response) + return maxLengthValidator(textField) default: - return right(response) + return right } } -const constructTextValidator: TextFieldValidatorConstructor = (textField) => ( - response, -) => { - return pipe( - notEmptySingleAnswerResponse(response), - chain(lengthValidator(textField)), +const constructTextValidator: TextFieldValidatorConstructor = (textField) => { + return flow( + notEmptySingleAnswerResponse, + chain(getLengthValidator(textField)), ) } From 5b8b38261a28e9633fa1d9df3347532bfc0a3bc0 Mon Sep 17 00:00:00 2001 From: Kar Rui Lau Date: Thu, 26 Nov 2020 19:38:13 +0800 Subject: [PATCH 17/34] ref: migrate stream feedback flow to TypeScript (#733) * feat(FeedbackModel): add static method getFeedbackCursorByFormId * feat(FeedbackSvc): add getFormFeedbackStream fn * feat(AdminFormCtl): add handleStreamFormFeedback fn * feat(utils): add exhaustive switch case typeguard * ref(AdminFormsRoutes): use new handleStreamFormFeedback handler * test(FormFeedbackModel): add tests for model schema and statics * feat(AuthService): add getFormAfterPermissionChecks helper function * test(AuthService): add test cases for getFormAfterPermissionChecks * ref(AdminFormCtl): use getFormAfterPermissionChecks fn in controllers * test(AdminFormCtl): add tests for handleStreamFormFeedback Note that I have no clue how to test express streams, and my Google-fu failed me this time. Thus, the success case test is just testing that the services return correctly and not whether the stream passes :( * feat: remove unused code and add jsdoc for handler * test(AuthSvc): replace assertSpy with inlined mock --- .../admin-forms.server.controller.js | 56 -- src/app/models/form_feedback.server.model.ts | 14 + .../auth/__tests__/auth.service.spec.ts | 152 ++++- src/app/modules/auth/auth.service.ts | 53 +- .../__tests__/feedback.service.spec.ts | 18 + src/app/modules/feedback/feedback.service.ts | 12 + .../__tests__/admin-form.controller.spec.ts | 539 +++++++++++++----- .../form/admin-form/admin-form.controller.ts | 221 ++++--- .../form/admin-form/admin-form.types.ts | 6 +- .../form/admin-form/admin-form.utils.ts | 33 +- .../form/public-form/public-form.service.ts | 2 +- src/app/routes/admin-forms.server.routes.js | 2 +- src/types/form_feedback.ts | 13 +- tests/unit/backend/helpers/jest-express.ts | 4 + .../models/form_feedback.server.model.spec.ts | 106 ++++ 15 files changed, 912 insertions(+), 319 deletions(-) create mode 100644 tests/unit/backend/models/form_feedback.server.model.spec.ts diff --git a/src/app/controllers/admin-forms.server.controller.js b/src/app/controllers/admin-forms.server.controller.js index 14c33e280e..30380c315c 100644 --- a/src/app/controllers/admin-forms.server.controller.js +++ b/src/app/controllers/admin-forms.server.controller.js @@ -6,7 +6,6 @@ const mongoose = require('mongoose') const moment = require('moment-timezone') const _ = require('lodash') -const JSONStream = require('JSONStream') const { StatusCodes } = require('http-status-codes') const logger = require('../../config/logger').createLoggerWithLabel(module) @@ -463,61 +462,6 @@ function makeModule(connection) { } }) }, - /** - * Stream download feedback for a form - * @param {Object} req - Express request object - * @param {Object} req.form - the form to download - * @param {Object} res - Express response object - */ - streamFeedback: function (req, res) { - let FormFeedback = getFormFeedbackModel(connection) - FormFeedback.find({ formId: req.form._id }) - .cursor() - .on('error', function (err) { - logger.error({ - message: 'Error streaming feedback from MongoDB', - meta: { - action: 'makeModule.streamFeedback', - ...createReqMeta(req), - }, - error: err, - }) - res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - message: 'Error retrieving from database.', - }) - }) - .pipe(JSONStream.stringify()) - .on('error', function (err) { - logger.error({ - message: 'Error converting feedback to JSON', - meta: { - action: 'makeModule.streamFeedback', - ...createReqMeta(req), - }, - error: err, - }) - res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - message: 'Error converting feedback to JSON', - }) - }) - .pipe(res.type('json')) - .on('error', function (err) { - logger.error({ - message: 'Error writing feedback to HTTP stream', - meta: { - action: 'makeModule.streamFeedback', - ...createReqMeta(req), - }, - error: err, - }) - res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ - message: 'Error writing feedback to HTTP stream', - }) - }) - .on('end', function () { - res.end() - }) - }, /** * Submit feedback when previewing forms * Preview feedback is not stored diff --git a/src/app/models/form_feedback.server.model.ts b/src/app/models/form_feedback.server.model.ts index 97055d281d..3f42609a83 100644 --- a/src/app/models/form_feedback.server.model.ts +++ b/src/app/models/form_feedback.server.model.ts @@ -34,6 +34,20 @@ const FormFeedbackSchema = new Schema( }, ) +/** + * Returns a cursor for all feedback for the form with formId. + * @param formId the form id to return the submissions cursor for + * @returns a cursor to the feedback retrieved + */ +const getFeedbackCursorByFormId: IFormFeedbackModel['getFeedbackCursorByFormId'] = function ( + this: IFormFeedbackModel, + formId, +) { + return this.find({ formId }).batchSize(2000).read('secondary').lean().cursor() +} + +FormFeedbackSchema.statics.getFeedbackCursorByFormId = getFeedbackCursorByFormId + /** * Form Feedback Schema * @param db Active DB Connection diff --git a/src/app/modules/auth/__tests__/auth.service.spec.ts b/src/app/modules/auth/__tests__/auth.service.spec.ts index 32488245de..6c7710c9c3 100644 --- a/src/app/modules/auth/__tests__/auth.service.spec.ts +++ b/src/app/modules/auth/__tests__/auth.service.spec.ts @@ -1,15 +1,32 @@ +import { ObjectId } from 'bson-ext' import mongoose from 'mongoose' +import { err, errAsync, ok, okAsync } from 'neverthrow' +import { mocked } from 'ts-jest/utils' import { ImportMock } from 'ts-mock-imports' import getTokenModel from 'src/app/models/token.server.model' import * as OtpUtils from 'src/app/utils/otp' -import { IAgencySchema } from 'src/types' +import { IAgencySchema, IPopulatedForm, IPopulatedUser } from 'src/types' import dbHandler from 'tests/unit/backend/helpers/jest-db' +import { DatabaseError } from '../../core/core.errors' +import { PermissionLevel } from '../../form/admin-form/admin-form.types' +import * as AdminFormUtils from '../../form/admin-form/admin-form.utils' +import { + ForbiddenFormError, + FormDeletedError, + FormNotFoundError, +} from '../../form/form.errors' +import * as FormService from '../../form/form.service' import { InvalidDomainError, InvalidOtpError } from '../auth.errors' import * as AuthService from '../auth.service' +jest.mock('../../form/form.service') +const MockFormService = mocked(FormService) +jest.mock('../../form/admin-form/admin-form.utils') +const MockAdminFormUtils = mocked(AdminFormUtils) + const TokenModel = getTokenModel(mongoose) const VALID_EMAIL_DOMAIN = 'test.gov.sg' @@ -30,10 +47,10 @@ describe('auth.service', () => { }) // Only need to clear Token collection, and ignore other collections. - beforeEach( - async () => - await dbHandler.clearCollection(TokenModel.collection.collectionName), - ) + beforeEach(async () => { + await dbHandler.clearCollection(TokenModel.collection.collectionName) + jest.resetAllMocks() + }) afterAll(async () => await dbHandler.closeDatabase()) @@ -187,4 +204,129 @@ describe('auth.service', () => { expect(actualResult._unsafeUnwrapErr()).toEqual(expectedError) }) }) + + describe('getFormAfterPermissionChecks', () => { + const MOCK_USER = { + _id: new ObjectId(), + } as IPopulatedUser + it('should return form when user has permissions', async () => { + // Arrange + const mockFormId = new ObjectId().toHexString() + const expectedForm = { + title: 'mock form', + _id: mockFormId, + } as IPopulatedForm + MockFormService.retrieveFullFormById.mockReturnValueOnce( + okAsync(expectedForm), + ) + MockAdminFormUtils.assertFormAvailable.mockReturnValueOnce(ok(true)) + MockAdminFormUtils.getAssertPermissionFn.mockReturnValueOnce(() => + ok(true), + ) + + // Act + const actualResult = await AuthService.getFormAfterPermissionChecks({ + user: MOCK_USER, + formId: mockFormId, + level: PermissionLevel.Write, + }) + + // Assert + expect(actualResult.isOk()).toEqual(true) + expect(actualResult._unsafeUnwrap()).toEqual(expectedForm) + expect(MockAdminFormUtils.getAssertPermissionFn).toHaveBeenCalledWith( + PermissionLevel.Write, + ) + }) + + it('should return FormNotFoundError when form does not exist in the database', async () => { + // Arrange + const expectedError = new FormNotFoundError('not found') + MockFormService.retrieveFullFormById.mockReturnValueOnce( + errAsync(expectedError), + ) + + // Act + const actualResult = await AuthService.getFormAfterPermissionChecks({ + user: MOCK_USER, + formId: new ObjectId().toHexString(), + level: PermissionLevel.Read, + }) + + // Assert + expect(actualResult.isErr()).toEqual(true) + expect(actualResult._unsafeUnwrapErr()).toEqual(expectedError) + }) + + it('should return FormDeletedError when form is already archived', async () => { + // Arrange + const mockFormId = new ObjectId().toHexString() + const expectedError = new FormDeletedError('form deleted') + MockFormService.retrieveFullFormById.mockReturnValueOnce( + okAsync({} as IPopulatedForm), + ) + MockAdminFormUtils.assertFormAvailable.mockReturnValueOnce( + err(expectedError), + ) + // Act + const actualResult = await AuthService.getFormAfterPermissionChecks({ + user: MOCK_USER, + formId: mockFormId, + level: PermissionLevel.Delete, + }) + + // Assert + expect(actualResult.isErr()).toEqual(true) + expect(actualResult._unsafeUnwrapErr()).toEqual(expectedError) + }) + + it('should return ForbiddenFormError when user does not have permission', async () => { + // Arrange + const mockFormId = new ObjectId().toHexString() + MockFormService.retrieveFullFormById.mockReturnValueOnce( + okAsync({} as IPopulatedForm), + ) + const expectedError = new ForbiddenFormError('user not allowed') + MockAdminFormUtils.assertFormAvailable.mockReturnValueOnce(ok(true)) + MockAdminFormUtils.getAssertPermissionFn.mockReturnValueOnce(() => + err(expectedError), + ) + + // Act + const actualResult = await AuthService.getFormAfterPermissionChecks({ + user: MOCK_USER, + formId: mockFormId, + level: PermissionLevel.Write, + }) + + // Assert + expect(actualResult.isErr()).toEqual(true) + expect(actualResult._unsafeUnwrapErr()).toEqual(expectedError) + expect(MockAdminFormUtils.getAssertPermissionFn).toHaveBeenCalledWith( + PermissionLevel.Write, + ) + }) + + it('should return DatabaseError when error occurs whilst retrieving form', async () => { + // Arrange + const mockFormId = new ObjectId().toHexString() + const expectedError = new DatabaseError('db boom') + MockFormService.retrieveFullFormById.mockReturnValueOnce( + okAsync({} as IPopulatedForm), + ) + MockAdminFormUtils.assertFormAvailable.mockReturnValueOnce( + err(expectedError), + ) + // Act + const actualResult = await AuthService.getFormAfterPermissionChecks({ + user: MOCK_USER, + formId: mockFormId, + level: PermissionLevel.Delete, + }) + + // Assert + expect(actualResult.isErr()).toEqual(true) + expect(actualResult._unsafeUnwrapErr()).toEqual(expectedError) + }) + }) }) diff --git a/src/app/modules/auth/auth.service.ts b/src/app/modules/auth/auth.service.ts index 2fc6001e6e..ba4ffac6fb 100644 --- a/src/app/modules/auth/auth.service.ts +++ b/src/app/modules/auth/auth.service.ts @@ -2,16 +2,31 @@ import mongoose from 'mongoose' import { errAsync, okAsync, ResultAsync } from 'neverthrow' import validator from 'validator' -import { IAgencySchema, ITokenSchema } from 'src/types' - import config from '../../../config/config' import { createLoggerWithLabel } from '../../../config/logger' import { LINKS } from '../../../shared/constants' +import { + IAgencySchema, + IPopulatedForm, + ITokenSchema, + IUserSchema, +} from '../../../types' import getAgencyModel from '../../models/agency.server.model' import getTokenModel from '../../models/token.server.model' import { compareHash, hashData } from '../../utils/hash' import { generateOtp } from '../../utils/otp' import { ApplicationError, DatabaseError } from '../core/core.errors' +import { PermissionLevel } from '../form/admin-form/admin-form.types' +import { + assertFormAvailable, + getAssertPermissionFn, +} from '../form/admin-form/admin-form.utils' +import { + ForbiddenFormError, + FormDeletedError, + FormNotFoundError, +} from '../form/form.errors' +import * as FormService from '../form/form.service' import { InvalidDomainError, InvalidOtpError } from './auth.errors' @@ -249,3 +264,37 @@ const removeTokenOnSuccess = (email: string) => { }, ) } + +/** + * Retrieves the form of given formId provided that the given user has the + * required permissions. + * + * @returns ok(form) if the user has the required permissions + * @returns err(FormNotFoundError) if form does not exist in the database + * @returns err(FormDeleteError) if form is already archived + * @returns err(ForbiddenFormError if user does not have permission + * @returns err(DatabaseError) if any database error occurs + */ +export const getFormAfterPermissionChecks = ({ + user, + formId, + level, +}: { + user: IUserSchema + formId: string + level: PermissionLevel +}): ResultAsync< + IPopulatedForm, + FormNotFoundError | FormDeletedError | DatabaseError | ForbiddenFormError +> => { + // Step 1: Retrieve full form. + return FormService.retrieveFullFormById(formId).andThen((fullForm) => + // Step 2: Check whether form is available to be retrieved. + assertFormAvailable(fullForm).andThen(() => + // Step 3: Check required permission levels. + getAssertPermissionFn(level)(user, fullForm) + // Step 4: If success, return retrieved form. + .map(() => fullForm), + ), + ) +} diff --git a/src/app/modules/feedback/__tests__/feedback.service.spec.ts b/src/app/modules/feedback/__tests__/feedback.service.spec.ts index 3aa0b8f9fe..4dd4730873 100644 --- a/src/app/modules/feedback/__tests__/feedback.service.spec.ts +++ b/src/app/modules/feedback/__tests__/feedback.service.spec.ts @@ -85,4 +85,22 @@ describe('feedback.service', () => { expect(actualResult._unsafeUnwrapErr()).toBeInstanceOf(DatabaseError) }) }) + + describe('getFormFeedbackStream', () => { + it('should return stream successfully', async () => { + // Arrange + const mockFormId = 'some form id' + const mockCursor = ('some cursor' as unknown) as mongoose.QueryCursor + const streamSpy = jest + .spyOn(FormFeedback, 'getFeedbackCursorByFormId') + .mockReturnValue(mockCursor) + + // Act + const actual = FeedbackService.getFormFeedbackStream(mockFormId) + + // Assert + expect(actual).toEqual(mockCursor) + expect(streamSpy).toHaveBeenCalledWith(mockFormId) + }) + }) }) diff --git a/src/app/modules/feedback/feedback.service.ts b/src/app/modules/feedback/feedback.service.ts index 56ba093edc..6bada6b8aa 100644 --- a/src/app/modules/feedback/feedback.service.ts +++ b/src/app/modules/feedback/feedback.service.ts @@ -2,6 +2,7 @@ import mongoose from 'mongoose' import { ResultAsync } from 'neverthrow' import { createLoggerWithLabel } from '../../../config/logger' +import { IFormFeedbackSchema } from '../../../types' import getFormFeedbackModel from '../../models/form_feedback.server.model' import { getMongoErrorMessage } from '../../utils/handle-mongo-error' import { DatabaseError } from '../core/core.errors' @@ -35,3 +36,14 @@ export const getFormFeedbackCount = ( }, ) } + +/** + * Retrieves form feedback stream. + * @param formId the formId of form to retrieve feedback for + * @returns form feedback stream + */ +export const getFormFeedbackStream = ( + formId: string, +): mongoose.QueryCursor => { + return FormFeedbackModel.getFeedbackCursorByFormId(formId) +} diff --git a/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts b/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts index 0e66017a18..2dc3eacf40 100644 --- a/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts +++ b/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts @@ -1,9 +1,11 @@ import { PresignedPost } from 'aws-sdk/clients/s3' import { ObjectId } from 'bson-ext' import { merge } from 'lodash' -import { err, errAsync, ok, okAsync } from 'neverthrow' +import { errAsync, okAsync } from 'neverthrow' +import { PassThrough } from 'stream' import { mocked } from 'ts-jest/utils' +import * as AuthService from 'src/app/modules/auth/auth.service' import { DatabaseError } from 'src/app/modules/core/core.errors' import * as FeedbackService from 'src/app/modules/feedback/feedback.service' import * as SubmissionService from 'src/app/modules/submission/submission.service' @@ -18,15 +20,16 @@ import { FormDeletedError, FormNotFoundError, } from '../../form.errors' -import * as FormService from '../../form.service' import * as AdminFormController from '../admin-form.controller' import { CreatePresignedUrlError, InvalidFileTypeError, } from '../admin-form.errors' import * as AdminFormService from '../admin-form.service' -import * as AdminFormUtils from '../admin-form.utils' +import { PermissionLevel } from '../admin-form.types' +jest.mock('src/app/modules/auth/auth.service') +const MockAuthService = mocked(AuthService) jest.mock('src/app/modules/feedback/feedback.service') const MockFeedbackService = mocked(FeedbackService) jest.mock('src/app/modules/submission/submission.service') @@ -35,8 +38,6 @@ jest.mock('../admin-form.service') const MockAdminFormService = mocked(AdminFormService) jest.mock('../../../user/user.service') const MockUserService = mocked(UserService) -jest.mock('../../form.service') -const MockFormService = mocked(FormService) describe('admin-form.controller', () => { beforeEach(() => jest.clearAllMocks()) @@ -301,12 +302,9 @@ describe('admin-form.controller', () => { MockUserService.getPopulatedUserById.mockReturnValueOnce( okAsync(MOCK_USER as IPopulatedUser), ) - MockFormService.retrieveFullFormById.mockReturnValueOnce( + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( okAsync(MOCK_FORM as IPopulatedForm), ) - const readPermsSpy = jest - .spyOn(AdminFormUtils, 'assertHasReadPermissions') - .mockReturnValueOnce(ok(true)) MockSubmissionService.getFormSubmissionsCount.mockReturnValueOnce( okAsync(expectedSubmissionCount), ) @@ -323,10 +321,13 @@ describe('admin-form.controller', () => { expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( MOCK_USER_ID, ) - expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( - MOCK_FORM_ID.toHexString(), + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID.toHexString(), + level: PermissionLevel.Read, + }, ) - expect(readPermsSpy).toHaveBeenCalledWith(MOCK_USER, MOCK_FORM) expect( MockSubmissionService.getFormSubmissionsCount, ).toHaveBeenCalledWith(String(MOCK_FORM._id), { @@ -352,12 +353,9 @@ describe('admin-form.controller', () => { MockUserService.getPopulatedUserById.mockReturnValueOnce( okAsync(MOCK_USER as IPopulatedUser), ) - MockFormService.retrieveFullFormById.mockReturnValueOnce( + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( okAsync(MOCK_FORM as IPopulatedForm), ) - const readPermsSpy = jest - .spyOn(AdminFormUtils, 'assertHasReadPermissions') - .mockReturnValueOnce(ok(true)) MockSubmissionService.getFormSubmissionsCount.mockReturnValueOnce( okAsync(expectedSubmissionCount), ) @@ -374,10 +372,13 @@ describe('admin-form.controller', () => { expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( MOCK_USER_ID, ) - expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( - MOCK_FORM_ID.toHexString(), + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID.toHexString(), + level: PermissionLevel.Read, + }, ) - expect(readPermsSpy).toHaveBeenCalledWith(MOCK_USER, MOCK_FORM) expect( MockSubmissionService.getFormSubmissionsCount, ).toHaveBeenCalledWith(String(MOCK_FORM._id), expectedDateRange) @@ -387,19 +388,16 @@ describe('admin-form.controller', () => { it('should return 403 when ForbiddenFormError is returned when verifying user permissions', async () => { // Arrange + const expectedErrorString = 'no read access' + const mockRes = expressHandler.mockResponse() // Mock various services to return expected results. MockUserService.getPopulatedUserById.mockReturnValueOnce( okAsync(MOCK_USER as IPopulatedUser), ) - MockFormService.retrieveFullFormById.mockReturnValueOnce( - okAsync(MOCK_FORM as IPopulatedForm), + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new ForbiddenFormError(expectedErrorString)), ) - // Mock error here. - const expectedErrorString = 'no read access' - const readPermsSpy = jest - .spyOn(AdminFormUtils, 'assertHasReadPermissions') - .mockReturnValueOnce(err(new ForbiddenFormError(expectedErrorString))) // Act await AdminFormController.handleCountFormSubmissions( @@ -413,10 +411,13 @@ describe('admin-form.controller', () => { expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( MOCK_USER_ID, ) - expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( - MOCK_FORM_ID.toHexString(), + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID.toHexString(), + level: PermissionLevel.Read, + }, ) - expect(readPermsSpy).toHaveBeenCalledWith(MOCK_USER, MOCK_FORM) expect( MockSubmissionService.getFormSubmissionsCount, ).not.toHaveBeenCalled() @@ -435,7 +436,7 @@ describe('admin-form.controller', () => { ) // Mock error when retrieving form. const expectedErrorString = 'form is not found' - MockFormService.retrieveFullFormById.mockReturnValueOnce( + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( errAsync(new FormNotFoundError(expectedErrorString)), ) @@ -451,8 +452,12 @@ describe('admin-form.controller', () => { expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( MOCK_USER_ID, ) - expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( - MOCK_FORM_ID.toHexString(), + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID.toHexString(), + level: PermissionLevel.Read, + }, ) expect( MockSubmissionService.getFormSubmissionsCount, @@ -472,7 +477,7 @@ describe('admin-form.controller', () => { ) // Mock error when retrieving form. const expectedErrorString = 'form is deleted' - MockFormService.retrieveFullFormById.mockReturnValueOnce( + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( errAsync(new FormDeletedError(expectedErrorString)), ) @@ -488,50 +493,13 @@ describe('admin-form.controller', () => { expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( MOCK_USER_ID, ) - expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( - MOCK_FORM_ID.toHexString(), - ) - expect( - MockSubmissionService.getFormSubmissionsCount, - ).not.toHaveBeenCalled() - expect(mockRes.status).toHaveBeenCalledWith(410) - expect(mockRes.json).toHaveBeenCalledWith({ - message: expectedErrorString, - }) - }) - - it('should return 410 when FormDeletedError is returned when checking form availability', async () => { - // Arrange - const mockRes = expressHandler.mockResponse() - // Mock various services to return expected results. - MockUserService.getPopulatedUserById.mockReturnValueOnce( - okAsync(MOCK_USER as IPopulatedUser), - ) - MockFormService.retrieveFullFormById.mockReturnValueOnce( - okAsync(MOCK_FORM as IPopulatedForm), - ) - // Mock error when checking form availability. - const expectedErrorString = 'form is archived' - const utilSpy = jest - .spyOn(AdminFormUtils, 'assertFormAvailable') - .mockReturnValueOnce(err(new FormDeletedError(expectedErrorString))) - - // Act - await AdminFormController.handleCountFormSubmissions( - MOCK_REQ, - mockRes, - jest.fn(), - ) - - // Assert - // Check all arguments of called services. - expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( - MOCK_USER_ID, - ) - expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( - MOCK_FORM_ID.toHexString(), + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID.toHexString(), + level: PermissionLevel.Read, + }, ) - expect(utilSpy).toHaveBeenCalledWith(MOCK_FORM) expect( MockSubmissionService.getFormSubmissionsCount, ).not.toHaveBeenCalled() @@ -562,7 +530,9 @@ describe('admin-form.controller', () => { expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( MOCK_USER_ID, ) - expect(MockFormService.retrieveFullFormById).not.toHaveBeenCalled() + expect( + MockAuthService.getFormAfterPermissionChecks, + ).not.toHaveBeenCalled() expect( MockSubmissionService.getFormSubmissionsCount, ).not.toHaveBeenCalled() @@ -593,7 +563,9 @@ describe('admin-form.controller', () => { expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( MOCK_USER_ID, ) - expect(MockFormService.retrieveFullFormById).not.toHaveBeenCalled() + expect( + MockAuthService.getFormAfterPermissionChecks, + ).not.toHaveBeenCalled() expect( MockSubmissionService.getFormSubmissionsCount, ).not.toHaveBeenCalled() @@ -612,7 +584,7 @@ describe('admin-form.controller', () => { ) // Mock error when retrieving form. const expectedErrorString = 'database goes boom' - MockFormService.retrieveFullFormById.mockReturnValueOnce( + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( errAsync(new DatabaseError(expectedErrorString)), ) @@ -628,8 +600,12 @@ describe('admin-form.controller', () => { expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( MOCK_USER_ID, ) - expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( - MOCK_FORM_ID.toHexString(), + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID.toHexString(), + level: PermissionLevel.Read, + }, ) expect( MockSubmissionService.getFormSubmissionsCount, @@ -647,12 +623,9 @@ describe('admin-form.controller', () => { MockUserService.getPopulatedUserById.mockReturnValueOnce( okAsync(MOCK_USER as IPopulatedUser), ) - MockFormService.retrieveFullFormById.mockReturnValueOnce( + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( okAsync(MOCK_FORM as IPopulatedForm), ) - const readPermsSpy = jest - .spyOn(AdminFormUtils, 'assertHasReadPermissions') - .mockReturnValueOnce(ok(true)) const expectedErrorString = 'database goes boom' MockSubmissionService.getFormSubmissionsCount.mockReturnValueOnce( errAsync(new DatabaseError(expectedErrorString)), @@ -670,10 +643,13 @@ describe('admin-form.controller', () => { expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( MOCK_USER_ID, ) - expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( - MOCK_FORM_ID.toHexString(), + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID.toHexString(), + level: PermissionLevel.Read, + }, ) - expect(readPermsSpy).toHaveBeenCalledWith(MOCK_USER, MOCK_FORM) expect( MockSubmissionService.getFormSubmissionsCount, ).toHaveBeenCalledWith(String(MOCK_FORM._id), { @@ -720,12 +696,9 @@ describe('admin-form.controller', () => { MockUserService.getPopulatedUserById.mockReturnValueOnce( okAsync(MOCK_USER as IPopulatedUser), ) - MockFormService.retrieveFullFormById.mockReturnValueOnce( + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( okAsync(MOCK_FORM as IPopulatedForm), ) - const readPermsSpy = jest - .spyOn(AdminFormUtils, 'assertHasReadPermissions') - .mockReturnValueOnce(ok(true)) MockFeedbackService.getFormFeedbackCount.mockReturnValueOnce( okAsync(expectedFeedbackCount), ) @@ -742,10 +715,13 @@ describe('admin-form.controller', () => { expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( MOCK_USER_ID, ) - expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( - MOCK_FORM_ID.toHexString(), + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID.toHexString(), + level: PermissionLevel.Read, + }, ) - expect(readPermsSpy).toHaveBeenCalledWith(MOCK_USER, MOCK_FORM) expect(MockFeedbackService.getFormFeedbackCount).toHaveBeenCalledWith( String(MOCK_FORM._id), ) @@ -760,14 +736,10 @@ describe('admin-form.controller', () => { MockUserService.getPopulatedUserById.mockReturnValueOnce( okAsync(MOCK_USER as IPopulatedUser), ) - MockFormService.retrieveFullFormById.mockReturnValueOnce( - okAsync(MOCK_FORM as IPopulatedForm), - ) - // Mock error here. const expectedErrorString = 'no read access' - const readPermsSpy = jest - .spyOn(AdminFormUtils, 'assertHasReadPermissions') - .mockReturnValueOnce(err(new ForbiddenFormError(expectedErrorString))) + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new ForbiddenFormError(expectedErrorString)), + ) // Act await AdminFormController.handleCountFormFeedback( @@ -781,10 +753,13 @@ describe('admin-form.controller', () => { expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( MOCK_USER_ID, ) - expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( - MOCK_FORM_ID.toHexString(), + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID.toHexString(), + level: PermissionLevel.Read, + }, ) - expect(readPermsSpy).toHaveBeenCalledWith(MOCK_USER, MOCK_FORM) expect(MockFeedbackService.getFormFeedbackCount).not.toHaveBeenCalled() expect(mockRes.status).toHaveBeenCalledWith(403) expect(mockRes.json).toHaveBeenCalledWith({ @@ -801,7 +776,7 @@ describe('admin-form.controller', () => { ) // Mock error when retrieving form. const expectedErrorString = 'form is not found' - MockFormService.retrieveFullFormById.mockReturnValueOnce( + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( errAsync(new FormNotFoundError(expectedErrorString)), ) @@ -817,8 +792,12 @@ describe('admin-form.controller', () => { expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( MOCK_USER_ID, ) - expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( - MOCK_FORM_ID.toHexString(), + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID.toHexString(), + level: PermissionLevel.Read, + }, ) expect(MockFeedbackService.getFormFeedbackCount).not.toHaveBeenCalled() expect(mockRes.status).toHaveBeenCalledWith(404) @@ -836,7 +815,7 @@ describe('admin-form.controller', () => { ) // Mock error when retrieving form. const expectedErrorString = 'form is deleted' - MockFormService.retrieveFullFormById.mockReturnValueOnce( + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( errAsync(new FormDeletedError(expectedErrorString)), ) @@ -852,8 +831,12 @@ describe('admin-form.controller', () => { expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( MOCK_USER_ID, ) - expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( - MOCK_FORM_ID.toHexString(), + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID.toHexString(), + level: PermissionLevel.Read, + }, ) expect(MockFeedbackService.getFormFeedbackCount).not.toHaveBeenCalled() expect(mockRes.status).toHaveBeenCalledWith(410) @@ -862,21 +845,14 @@ describe('admin-form.controller', () => { }) }) - it('should return 410 when FormDeletedError is returned when checking form availability', async () => { + it('should return 422 when MissingUserError is returned when retrieving logged in user', async () => { // Arrange const mockRes = expressHandler.mockResponse() // Mock various services to return expected results. + const expectedErrorString = 'user is not found' MockUserService.getPopulatedUserById.mockReturnValueOnce( - okAsync(MOCK_USER as IPopulatedUser), - ) - MockFormService.retrieveFullFormById.mockReturnValueOnce( - okAsync(MOCK_FORM as IPopulatedForm), + errAsync(new MissingUserError(expectedErrorString)), ) - // Mock error when checking form availability. - const expectedErrorString = 'form is archived' - const utilSpy = jest - .spyOn(AdminFormUtils, 'assertFormAvailable') - .mockReturnValueOnce(err(new FormDeletedError(expectedErrorString))) // Act await AdminFormController.handleCountFormFeedback( @@ -890,24 +866,23 @@ describe('admin-form.controller', () => { expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( MOCK_USER_ID, ) - expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( - MOCK_FORM_ID.toHexString(), - ) - expect(utilSpy).toHaveBeenCalledWith(MOCK_FORM) + expect( + MockAuthService.getFormAfterPermissionChecks, + ).not.toHaveBeenCalled() expect(MockFeedbackService.getFormFeedbackCount).not.toHaveBeenCalled() - expect(mockRes.status).toHaveBeenCalledWith(410) + expect(mockRes.status).toHaveBeenCalledWith(422) expect(mockRes.json).toHaveBeenCalledWith({ message: expectedErrorString, }) }) - it('should return 422 when MissingUserError is returned when retrieving logged in user', async () => { + it('should return 500 when database error occurs whilst retrieving user in session', async () => { // Arrange const mockRes = expressHandler.mockResponse() // Mock various services to return expected results. - const expectedErrorString = 'user is not found' + const expectedErrorString = 'database goes boom' MockUserService.getPopulatedUserById.mockReturnValueOnce( - errAsync(new MissingUserError(expectedErrorString)), + errAsync(new DatabaseError(expectedErrorString)), ) // Act @@ -922,20 +897,26 @@ describe('admin-form.controller', () => { expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( MOCK_USER_ID, ) - expect(MockFormService.retrieveFullFormById).not.toHaveBeenCalled() + expect( + MockAuthService.getFormAfterPermissionChecks, + ).not.toHaveBeenCalled() expect(MockFeedbackService.getFormFeedbackCount).not.toHaveBeenCalled() - expect(mockRes.status).toHaveBeenCalledWith(422) + expect(mockRes.status).toHaveBeenCalledWith(500) expect(mockRes.json).toHaveBeenCalledWith({ message: expectedErrorString, }) }) - it('should return 500 when database error occurs whilst retrieving user in session', async () => { + it('should return 500 when database error occurs whilst retrieving populated form', async () => { // Arrange const mockRes = expressHandler.mockResponse() // Mock various services to return expected results. - const expectedErrorString = 'database goes boom' MockUserService.getPopulatedUserById.mockReturnValueOnce( + okAsync(MOCK_USER as IPopulatedUser), + ) + // Mock error when retrieving form. + const expectedErrorString = 'database goes boom' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( errAsync(new DatabaseError(expectedErrorString)), ) @@ -951,7 +932,13 @@ describe('admin-form.controller', () => { expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( MOCK_USER_ID, ) - expect(MockFormService.retrieveFullFormById).not.toHaveBeenCalled() + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID.toHexString(), + level: PermissionLevel.Read, + }, + ) expect(MockFeedbackService.getFormFeedbackCount).not.toHaveBeenCalled() expect(mockRes.status).toHaveBeenCalledWith(500) expect(mockRes.json).toHaveBeenCalledWith({ @@ -959,16 +946,18 @@ describe('admin-form.controller', () => { }) }) - it('should return 500 when database error occurs whilst retrieving populated form', async () => { + it('should return 500 when database error occurs whilst retrieving form feedback count', async () => { // Arrange const mockRes = expressHandler.mockResponse() // Mock various services to return expected results. MockUserService.getPopulatedUserById.mockReturnValueOnce( okAsync(MOCK_USER as IPopulatedUser), ) - // Mock error when retrieving form. + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + okAsync(MOCK_FORM as IPopulatedForm), + ) const expectedErrorString = 'database goes boom' - MockFormService.retrieveFullFormById.mockReturnValueOnce( + MockFeedbackService.getFormFeedbackCount.mockReturnValueOnce( errAsync(new DatabaseError(expectedErrorString)), ) @@ -984,36 +973,246 @@ describe('admin-form.controller', () => { expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( MOCK_USER_ID, ) - expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( - MOCK_FORM_ID.toHexString(), + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID.toHexString(), + level: PermissionLevel.Read, + }, + ) + expect(MockFeedbackService.getFormFeedbackCount).toHaveBeenCalledWith( + String(MOCK_FORM._id), ) - expect(MockFeedbackService.getFormFeedbackCount).not.toHaveBeenCalled() expect(mockRes.status).toHaveBeenCalledWith(500) expect(mockRes.json).toHaveBeenCalledWith({ message: expectedErrorString, }) }) + }) - it('should return 500 when database error occurs whilst retrieving form feedback count', async () => { + describe('handleStreamFormFeedback', () => { + const MOCK_USER_ID = new ObjectId() + const MOCK_FORM_ID = new ObjectId() + const MOCK_USER: Partial = { + _id: MOCK_USER_ID, + email: 'somerandom@example.com', + } + const MOCK_FORM: Partial = { + admin: MOCK_USER as IPopulatedUser, + _id: MOCK_FORM_ID, + title: 'mock title', + } + + const MOCK_REQ = expressHandler.mockRequest({ + params: { + formId: MOCK_FORM_ID.toHexString(), + }, + session: { + user: { + _id: MOCK_USER_ID, + }, + }, + }) + + it('should return 200 with feedback stream of given form', async () => { + // Not sure how to really test the stream in Jest, testing to assert that + // the correct services are being called instead. // Arrange const mockRes = expressHandler.mockResponse() // Mock various services to return expected results. MockUserService.getPopulatedUserById.mockReturnValueOnce( okAsync(MOCK_USER as IPopulatedUser), ) - MockFormService.retrieveFullFormById.mockReturnValueOnce( + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( okAsync(MOCK_FORM as IPopulatedForm), ) - const readPermsSpy = jest - .spyOn(AdminFormUtils, 'assertHasReadPermissions') - .mockReturnValueOnce(ok(true)) + // Mock cursor return. + const mockCursor = new PassThrough() + MockFeedbackService.getFormFeedbackStream.mockReturnValueOnce( + mockCursor as any, + ) + // Act + await AdminFormController.handleStreamFormFeedback( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + // Check all arguments of called services. + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID.toHexString(), + level: PermissionLevel.Read, + }, + ) + expect(MockFeedbackService.getFormFeedbackStream).toHaveBeenCalledWith( + String(MOCK_FORM._id), + ) + }) + + it('should return 403 when ForbiddenFormError is returned when verifying user permissions', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + // Mock various services to return expected results. + MockUserService.getPopulatedUserById.mockReturnValueOnce( + okAsync(MOCK_USER as IPopulatedUser), + ) + const expectedErrorString = 'no read access' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new ForbiddenFormError(expectedErrorString)), + ) + + // Act + await AdminFormController.handleStreamFormFeedback( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + // Check all arguments of called services. + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID.toHexString(), + level: PermissionLevel.Read, + }, + ) + expect(MockFeedbackService.getFormFeedbackStream).not.toHaveBeenCalled() + expect(mockRes.status).toHaveBeenCalledWith(403) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + }) + + it('should return 404 when FormNotFoundError is returned when retrieving form', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + // Mock various services to return expected results. + MockUserService.getPopulatedUserById.mockReturnValueOnce( + okAsync(MOCK_USER as IPopulatedUser), + ) + // Mock error when retrieving form. + const expectedErrorString = 'form is not found' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new FormNotFoundError(expectedErrorString)), + ) + + // Act + await AdminFormController.handleStreamFormFeedback( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + // Check all arguments of called services. + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID.toHexString(), + level: PermissionLevel.Read, + }, + ) + expect(MockFeedbackService.getFormFeedbackStream).not.toHaveBeenCalled() + expect(mockRes.status).toHaveBeenCalledWith(404) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + }) + + it('should return 410 when FormDeletedError is returned when retrieving form', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + // Mock various services to return expected results. + MockUserService.getPopulatedUserById.mockReturnValueOnce( + okAsync(MOCK_USER as IPopulatedUser), + ) + // Mock error when retrieving form. + const expectedErrorString = 'form is deleted' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new FormDeletedError(expectedErrorString)), + ) + + // Act + await AdminFormController.handleStreamFormFeedback( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + // Check all arguments of called services. + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID.toHexString(), + level: PermissionLevel.Read, + }, + ) + expect(MockFeedbackService.getFormFeedbackStream).not.toHaveBeenCalled() + expect(mockRes.status).toHaveBeenCalledWith(410) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + }) + + it('should return 422 when MissingUserError is returned when retrieving logged in user', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + // Mock various services to return expected results. + const expectedErrorString = 'user is not found' + MockUserService.getPopulatedUserById.mockReturnValueOnce( + errAsync(new MissingUserError(expectedErrorString)), + ) + + // Act + await AdminFormController.handleStreamFormFeedback( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + // Check all arguments of called services. + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect( + MockAuthService.getFormAfterPermissionChecks, + ).not.toHaveBeenCalled() + expect(MockFeedbackService.getFormFeedbackStream).not.toHaveBeenCalled() + expect(mockRes.status).toHaveBeenCalledWith(422) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + }) + + it('should return 500 when database error occurs whilst retrieving user in session', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + // Mock various services to return expected results. const expectedErrorString = 'database goes boom' - MockFeedbackService.getFormFeedbackCount.mockReturnValueOnce( + MockUserService.getPopulatedUserById.mockReturnValueOnce( errAsync(new DatabaseError(expectedErrorString)), ) // Act - await AdminFormController.handleCountFormFeedback( + await AdminFormController.handleStreamFormFeedback( MOCK_REQ, mockRes, jest.fn(), @@ -1024,13 +1223,49 @@ describe('admin-form.controller', () => { expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( MOCK_USER_ID, ) - expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( - MOCK_FORM_ID.toHexString(), + expect( + MockAuthService.getFormAfterPermissionChecks, + ).not.toHaveBeenCalled() + expect(MockFeedbackService.getFormFeedbackStream).not.toHaveBeenCalled() + expect(mockRes.status).toHaveBeenCalledWith(500) + expect(mockRes.json).toHaveBeenCalledWith({ + message: expectedErrorString, + }) + }) + + it('should return 500 when database error occurs whilst retrieving populated form', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + // Mock various services to return expected results. + MockUserService.getPopulatedUserById.mockReturnValueOnce( + okAsync(MOCK_USER as IPopulatedUser), ) - expect(readPermsSpy).toHaveBeenCalledWith(MOCK_USER, MOCK_FORM) - expect(MockFeedbackService.getFormFeedbackCount).toHaveBeenCalledWith( - String(MOCK_FORM._id), + // Mock error when retrieving form. + const expectedErrorString = 'database goes boom' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new DatabaseError(expectedErrorString)), + ) + + // Act + await AdminFormController.handleStreamFormFeedback( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + // Check all arguments of called services. + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID.toHexString(), + level: PermissionLevel.Read, + }, ) + expect(MockFeedbackService.getFormFeedbackStream).not.toHaveBeenCalled() expect(mockRes.status).toHaveBeenCalledWith(500) expect(mockRes.json).toHaveBeenCalledWith({ message: expectedErrorString, diff --git a/src/app/modules/form/admin-form/admin-form.controller.ts b/src/app/modules/form/admin-form/admin-form.controller.ts index 0603b728e8..45fce0e6e6 100644 --- a/src/app/modules/form/admin-form/admin-form.controller.ts +++ b/src/app/modules/form/admin-form/admin-form.controller.ts @@ -1,13 +1,15 @@ import { RequestHandler } from 'express' import { ParamsDictionary } from 'express-serve-static-core' +import { StatusCodes } from 'http-status-codes' +import JSONStream from 'JSONStream' import { createLoggerWithLabel } from '../../../../config/logger' import { AuthType, WithForm } from '../../../../types' import { createReqMeta } from '../../../utils/request' +import * as AuthService from '../../auth/auth.service' import * as FeedbackService from '../../feedback/feedback.service' import * as SubmissionService from '../../submission/submission.service' import * as UserService from '../../user/user.service' -import * as FormService from '../form.service' import { createPresignedPostForImages, @@ -15,11 +17,8 @@ import { getDashboardForms, getMockSpcpLocals, } from './admin-form.service' -import { - assertFormAvailable, - assertHasReadPermissions, - mapRouteError, -} from './admin-form.utils' +import { PermissionLevel } from './admin-form.types' +import { mapRouteError } from './admin-form.utils' const logger = createLoggerWithLabel(module) @@ -153,85 +152,47 @@ export const handleCountFormSubmissions: RequestHandler< } // Step 1: Retrieve currently logged in user. - const adminResult = await UserService.getPopulatedUserById(sessionUserId) - // Step 1a: Error retrieving logged in user. - if (adminResult.isErr()) { - logger.error({ - message: 'Error occurred whilst retrieving user', - meta: logMeta, - error: adminResult.error, - }) - - const { errorMessage, statusCode } = mapRouteError(adminResult.error) - return res.status(statusCode).json({ message: errorMessage }) - } - // Step 1b: Successfully retrieved logged in user. - const admin = adminResult.value + const formResult = await UserService.getPopulatedUserById( + sessionUserId, + ).andThen((user) => + // Step 2: Check whether user has read permissions to form + AuthService.getFormAfterPermissionChecks({ + user, + formId, + level: PermissionLevel.Read, + }), + ) - // Step 2: Retrieve full form. - const formResult = await FormService.retrieveFullFormById(formId) - // Step 2a: Error retrieving form. if (formResult.isErr()) { - logger.error({ - message: 'Failed to retrieve form', + logger.warn({ + message: 'Error occurred when checking user permissions for form', meta: logMeta, error: formResult.error, }) const { errorMessage, statusCode } = mapRouteError(formResult.error) return res.status(statusCode).json({ message: errorMessage }) } - // Step 2b: Successfully retrieved form. - const form = formResult.value - - // Step 3: Check whether form is already archived. - const availableResult = assertFormAvailable(form) - // Step 3a: Form is already archived. - if (availableResult.isErr()) { - logger.warn({ - message: 'User attempted to retrieve archived form', - meta: logMeta, - error: availableResult.error, - }) - const { errorMessage, statusCode } = mapRouteError(availableResult.error) - return res.status(statusCode).json({ message: errorMessage }) - } - - // Step 4: Form is still available for retrieval, check form permissions. - const permissionResult = assertHasReadPermissions(admin, form) - // Step 4a: Read permission error. - if (permissionResult.isErr()) { - logger.warn({ - message: 'User does not have read permissions', - meta: logMeta, - error: permissionResult.error, - }) - const { errorMessage, statusCode } = mapRouteError(permissionResult.error) - return res.status(statusCode).json({ message: errorMessage }) - } - // Step 5: Has permissions, continue to retrieve submission counts. - const countResult = await SubmissionService.getFormSubmissionsCount( - String(form._id), - { startDate, endDate }, - ) - // Step 5a: Error retrieving form submissions counts. - if (countResult.isErr()) { - logger.error({ - message: 'Error retrieving form submission count', - meta: { - action: 'handleCountFormSubmissions', - ...createReqMeta(req), - userId: sessionUserId, - formId, - }, - error: countResult.error, + // Step 3: Has permissions, continue to retrieve submission counts. + return SubmissionService.getFormSubmissionsCount(formId, { + startDate, + endDate, + }) + .map((count) => res.json(count)) + .mapErr((error) => { + logger.error({ + message: 'Error retrieving form submission count', + meta: { + action: 'handleCountFormSubmissions', + ...createReqMeta(req), + userId: sessionUserId, + formId, + }, + error, + }) + const { errorMessage, statusCode } = mapRouteError(error) + return res.status(statusCode).json({ message: errorMessage }) }) - const { errorMessage, statusCode } = mapRouteError(countResult.error) - return res.status(statusCode).json({ message: errorMessage }) - } - - // Successfully retrieved count. - return res.json(countResult.value) } /** @@ -275,16 +236,14 @@ export const handleCountFormFeedback: RequestHandler<{ // Step 1: Retrieve currently logged in user. UserService.getPopulatedUserById(sessionUserId) .andThen((user) => - // Step 2: Retrieve full form. - FormService.retrieveFullFormById(formId).andThen((fullForm) => - // Step 3: Check whether form is active. - assertFormAvailable(fullForm).andThen(() => - // Step 4: Check whether current user has read permissions to form. - assertHasReadPermissions(user, fullForm), - ), - ), + // Step 2: Check whether user has read permissions to form + AuthService.getFormAfterPermissionChecks({ + user, + formId, + level: PermissionLevel.Read, + }), ) - // Step 5: Retrieve form feedback counts. + // Step 3: Retrieve form feedback counts. .andThen(() => FeedbackService.getFormFeedbackCount(formId)) .map((feedbackCount) => res.json(feedbackCount)) // Some error occurred earlier in the chain. @@ -304,3 +263,97 @@ export const handleCountFormFeedback: RequestHandler<{ }) ) } + +/** + * Handler for GET /{formId}/adminform/feedback/download. + * @security session + * + * @returns 200 with feedback stream + * @returns 403 when user does not have permissions to access form + * @returns 404 when form cannot be found + * @returns 410 when form is archived + * @returns 422 when user in session cannot be retrieved from the database + * @returns 500 when database or stream error occurs + */ +export const handleStreamFormFeedback: RequestHandler<{ + formId: string +}> = async (req, res) => { + const { formId } = req.params + const sessionUserId = (req.session as Express.AuthedSession).user._id + + // Step 1: Retrieve currently logged in user. + const hasReadPermissionResult = await UserService.getPopulatedUserById( + sessionUserId, + ).andThen((user) => + // Step 2: Check whether user has read permissions to form + AuthService.getFormAfterPermissionChecks({ + user, + formId, + level: PermissionLevel.Read, + }), + ) + + const logMeta = { + action: 'handleStreamFormFeedback', + ...createReqMeta(req), + userId: sessionUserId, + formId, + } + + if (hasReadPermissionResult.isErr()) { + logger.error({ + message: 'Error occurred whilst verifying user permissions', + meta: logMeta, + error: hasReadPermissionResult.error, + }) + const { errorMessage, statusCode } = mapRouteError( + hasReadPermissionResult.error, + ) + return res.status(statusCode).json({ message: errorMessage }) + } + + // No errors, start stream. + const cursor = FeedbackService.getFormFeedbackStream(formId) + + cursor + .on('error', (error) => { + logger.error({ + message: 'Error streaming feedback from MongoDB', + meta: logMeta, + error, + }) + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + message: 'Error retrieving from database.', + }) + }) + .pipe(JSONStream.stringify()) + .on('error', (error) => { + logger.error({ + message: 'Error converting feedback to JSON', + meta: logMeta, + error, + }) + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + message: 'Error converting feedback to JSON', + }) + }) + .pipe(res.type('json')) + .on('error', (error) => { + logger.error({ + message: 'Error writing feedback to HTTP stream', + meta: logMeta, + error, + }) + return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + message: 'Error writing feedback to HTTP stream', + }) + }) + .on('close', () => { + logger.info({ + message: 'Stream feedback closed', + meta: logMeta, + }) + + return res.end() + }) +} diff --git a/src/app/modules/form/admin-form/admin-form.types.ts b/src/app/modules/form/admin-form/admin-form.types.ts index aa5f924b91..9431fbd372 100644 --- a/src/app/modules/form/admin-form/admin-form.types.ts +++ b/src/app/modules/form/admin-form/admin-form.types.ts @@ -1,5 +1,6 @@ import { Result } from 'neverthrow' +import { IPopulatedForm, IUserSchema } from '../../../../types' import { ForbiddenFormError } from '../form.errors' export enum PermissionLevel { @@ -8,4 +9,7 @@ export enum PermissionLevel { Delete = 'delete', } -export type FormPermissionResult = Result +export type AssertFormFn = ( + user: IUserSchema, + form: IPopulatedForm, +) => Result diff --git a/src/app/modules/form/admin-form/admin-form.utils.ts b/src/app/modules/form/admin-form/admin-form.utils.ts index 19f3196d69..0824b375d1 100644 --- a/src/app/modules/form/admin-form/admin-form.utils.ts +++ b/src/app/modules/form/admin-form/admin-form.utils.ts @@ -2,7 +2,8 @@ import { StatusCodes } from 'http-status-codes' import { err, ok, Result } from 'neverthrow' import { createLoggerWithLabel } from '../../../../config/logger' -import { IPopulatedForm, IUserSchema, Status } from '../../../../types' +import { IPopulatedForm, Status } from '../../../../types' +import { assertUnreachable } from '../../../utils/assert-unreachable' import { ApplicationError, DatabaseError, @@ -20,7 +21,7 @@ import { CreatePresignedUrlError, InvalidFileTypeError, } from './admin-form.errors' -import { FormPermissionResult } from './admin-form.types' +import { AssertFormFn, PermissionLevel } from './admin-form.types' const logger = createLoggerWithLabel(module) @@ -106,10 +107,7 @@ export const assertFormAvailable = ( * @returns ok(true) if given user has read permissions * @returns err(ForbiddenFormError) if user does not have read permissions */ -export const assertHasReadPermissions = ( - user: IUserSchema, - form: IPopulatedForm, -): FormPermissionResult => { +export const assertHasReadPermissions: AssertFormFn = (user, form) => { // Is form admin. Automatically has permissions. if (String(user._id) === String(form.admin._id)) { return ok(true) @@ -134,10 +132,7 @@ export const assertHasReadPermissions = ( * @returns ok(true) if given user has delete permissions * @returns err(ForbiddenFormError) if user does not have delete permissions */ -export const assertHasDeletePermissions = ( - user: IUserSchema, - form: IPopulatedForm, -): FormPermissionResult => { +export const assertHasDeletePermissions: AssertFormFn = (user, form) => { const isFormAdmin = String(user._id) === String(form.admin._id) // If form admin return isFormAdmin @@ -154,10 +149,7 @@ export const assertHasDeletePermissions = ( * @returns ok(true) if given user has write permissions * @returns err(ForbiddenFormError) if user does not have write permissions */ -export const assertHasWritePermissions = ( - user: IUserSchema, - form: IPopulatedForm, -): FormPermissionResult => { +export const assertHasWritePermissions: AssertFormFn = (user, form) => { // Is form admin. Automatically has permissions. if (String(user._id) === String(form.admin._id)) { return ok(true) @@ -177,3 +169,16 @@ export const assertHasWritePermissions = ( ), ) } + +export const getAssertPermissionFn = (level: PermissionLevel): AssertFormFn => { + switch (level) { + case PermissionLevel.Read: + return assertHasReadPermissions + case PermissionLevel.Write: + return assertHasWritePermissions + case PermissionLevel.Delete: + return assertHasDeletePermissions + default: + return assertUnreachable(level) + } +} diff --git a/src/app/modules/form/public-form/public-form.service.ts b/src/app/modules/form/public-form/public-form.service.ts index 0aa0929d5a..6d924cea1d 100644 --- a/src/app/modules/form/public-form/public-form.service.ts +++ b/src/app/modules/form/public-form/public-form.service.ts @@ -29,7 +29,7 @@ export const insertFormFeedback = ({ }: { formId: string rating: number - comment: string + comment?: string }): ResultAsync => { if (!mongoose.Types.ObjectId.isValid(formId)) { return errAsync(new FormNotFoundError()) diff --git a/src/app/routes/admin-forms.server.routes.js b/src/app/routes/admin-forms.server.routes.js index e97ba657b8..211382112d 100644 --- a/src/app/routes/admin-forms.server.routes.js +++ b/src/app/routes/admin-forms.server.routes.js @@ -298,7 +298,7 @@ module.exports = function (app) { */ app .route('/:formId([a-fA-F0-9]{24})/adminform/feedback/download') - .get(authActiveForm(PermissionLevel.Read), adminForms.streamFeedback) + .get(withUserAuthentication, AdminFormController.handleStreamFormFeedback) /** * Transfer form ownership to another user diff --git a/src/types/form_feedback.ts b/src/types/form_feedback.ts index f9cae5133f..8ab08cac7d 100644 --- a/src/types/form_feedback.ts +++ b/src/types/form_feedback.ts @@ -1,11 +1,11 @@ -import { Document, Model } from 'mongoose' +import { Document, Model, QueryCursor } from 'mongoose' import { IFormSchema } from './form' export interface IFormFeedback { formId: IFormSchema['_id'] rating: number - comment: string + comment?: string _id?: Document['_id'] } @@ -24,4 +24,11 @@ export interface IFormFeedbackDoc extends IFormFeedbackSchema { created: Date } -export type IFormFeedbackModel = Model +export interface IFormFeedbackModel extends Model { + /** + * Returns a cursor for all feedback for the form with formId. + * @param formId the form id to return the submissions cursor for + * @returns a cursor to the feedback retrieved + */ + getFeedbackCursorByFormId(formId: string): QueryCursor +} diff --git a/tests/unit/backend/helpers/jest-express.ts b/tests/unit/backend/helpers/jest-express.ts index 8f6b1d7867..46e571fafb 100644 --- a/tests/unit/backend/helpers/jest-express.ts +++ b/tests/unit/backend/helpers/jest-express.ts @@ -39,6 +39,10 @@ const mockResponse = ( send: jest.fn().mockReturnThis(), sendStatus: jest.fn().mockReturnThis(), type: jest.fn().mockReturnThis(), + pipe: jest.fn().mockReturnThis(), + emit: jest.fn().mockReturnThis(), + on: jest.fn().mockReturnThis(), + end: jest.fn(), json: jest.fn(), render: jest.fn(), redirect: jest.fn(), diff --git a/tests/unit/backend/models/form_feedback.server.model.spec.ts b/tests/unit/backend/models/form_feedback.server.model.spec.ts new file mode 100644 index 0000000000..b794c0e2a7 --- /dev/null +++ b/tests/unit/backend/models/form_feedback.server.model.spec.ts @@ -0,0 +1,106 @@ +import { ObjectId } from 'bson-ext' +import { omit } from 'lodash' +import mongoose from 'mongoose' + +import getFormFeedbackModel from 'src/app/models/form_feedback.server.model' +import { IFormFeedback } from 'src/types' + +import dbHandler from '../helpers/jest-db' + +const FeedbackModel = getFormFeedbackModel(mongoose) + +describe('form_feedback.server.model', () => { + beforeAll(async () => await dbHandler.connect()) + beforeEach(async () => await dbHandler.clearDatabase()) + afterAll(async () => await dbHandler.closeDatabase()) + + describe('Schema', () => { + const DEFAULT_PARAMS: IFormFeedback = { + formId: new ObjectId(), + rating: 5, + comment: 'feedback comment', + } + + it('should create and save successfully', async () => { + // Act + const actual = await FeedbackModel.create(DEFAULT_PARAMS) + + // Assert + expect(actual).toEqual( + expect.objectContaining({ + ...DEFAULT_PARAMS, + created: expect.any(Date), + lastModified: expect.any(Date), + }), + ) + }) + + it('should save successfully even when comment param is missing', async () => { + // Arrange + const paramsWithoutComment = omit(DEFAULT_PARAMS, 'comment') + // Act + const actual = await FeedbackModel.create(paramsWithoutComment) + + // Assert + expect(actual).toEqual( + expect.objectContaining({ + ...paramsWithoutComment, + created: expect.any(Date), + lastModified: expect.any(Date), + }), + ) + }) + + it('should throw validation error when formId param is missing', async () => { + // Act + const actualPromise = new FeedbackModel( + omit(DEFAULT_PARAMS, 'formId'), + ).save() + + // Assert + await expect(actualPromise).rejects.toThrowError( + mongoose.Error.ValidationError, + ) + }) + + it('should throw validation error when rating param is missing', async () => { + // Act + const actualPromise = new FeedbackModel( + omit(DEFAULT_PARAMS, 'rating'), + ).save() + + // Assert + await expect(actualPromise).rejects.toThrowError( + mongoose.Error.ValidationError, + ) + }) + }) + + describe('Statics', () => { + describe('getFeedbackCursorByFormId', () => { + it('should return cursor to feedback', async () => { + // Arrange + // Create document + const mockFormId = new ObjectId() + const mockFeedbackDoc = await FeedbackModel.create({ + formId: mockFormId, + rating: 5, + comment: 'feedback comment', + }) + + // Act + const cursor = FeedbackModel.getFeedbackCursorByFormId( + mockFormId.toHexString(), + ) + const actualFeedback = [] + for await (const fb of cursor) { + actualFeedback.push(fb) + } + + // Assert + // Cursor should return a lean object instead of a document. + expect(actualFeedback).toEqual([mockFeedbackDoc.toObject()]) + }) + }) + }) +}) From db1960a30f7efe0580e4fc2dd5748565c5de58b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Nov 2020 19:46:01 +0800 Subject: [PATCH 18/34] chore(deps-dev): bump eslint from 7.13.0 to 7.14.0 (#741) Bumps [eslint](https://github.com/eslint/eslint) from 7.13.0 to 7.14.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v7.13.0...v7.14.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 30 +++++++++--------------------- package.json | 2 +- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9c9e0cec9a..c63b9ccc16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2884,9 +2884,9 @@ }, "dependencies": { "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -10852,9 +10852,9 @@ } }, "eslint": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.13.0.tgz", - "integrity": "sha512-uCORMuOO8tUzJmsdRtrvcGq5qposf7Rw0LwkTJkoDbOycVQtQjmnhZSuLQnozLE4TmAzlMVV45eCHmQ1OpDKUQ==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.14.0.tgz", + "integrity": "sha512-5YubdnPXrlrYAFCKybPuHIAH++PINe1pmKNc5wQRB9HSbqIK1ywAnntE3Wwua4giKu0bjligf1gLF6qxMGOYRA==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -10948,9 +10948,9 @@ } }, "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -23790,18 +23790,6 @@ "string-width": "^3.0.0" }, "dependencies": { - "ajv": { - "version": "6.12.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", - "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", diff --git a/package.json b/package.json index 99a9b5c82f..9593f83d0e 100644 --- a/package.json +++ b/package.json @@ -210,7 +210,7 @@ "css-loader": "^2.1.1", "csv-parse": "^4.12.0", "env-cmd": "^10.1.0", - "eslint": "^7.13.0", + "eslint": "^7.14.0", "eslint-config-prettier": "^6.15.0", "eslint-plugin-angular": "^4.0.1", "eslint-plugin-import": "^2.22.1", From 7784c48187cf0ef3aed8eae389ac71f26bbeba53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Nov 2020 19:46:20 +0800 Subject: [PATCH 19/34] chore(deps-dev): bump @babel/preset-env from 7.12.1 to 7.12.7 (#740) Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.12.1 to 7.12.7. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.12.7/packages/babel-preset-env) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 447 +++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 220 insertions(+), 229 deletions(-) diff --git a/package-lock.json b/package-lock.json index c63b9ccc16..37f72f6657 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,9 @@ } }, "@babel/compat-data": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.5.tgz", - "integrity": "sha512-DTsS7cxrsH3by8nqQSpFSyjSfSYl57D6Cf4q8dW3LK83tBKBDCkfcay1nYkXq1nIHXnpX8WMMb/O25HOy3h1zg==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.7.tgz", + "integrity": "sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw==", "dev": true }, "@babel/core": { @@ -277,9 +277,9 @@ "dev": true }, "@babel/types": { - "version": "7.12.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", - "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", + "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", @@ -302,33 +302,34 @@ }, "dependencies": { "browserslist": { - "version": "4.14.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.6.tgz", - "integrity": "sha512-zeFYcUo85ENhc/zxHbiIp0LGzzTrE2Pv2JhxvS7kpUb9Q9D38kUX6Bie7pGutJ/5iF5rOxE7CepAuWD56xJ33A==", + "version": "4.14.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.7.tgz", + "integrity": "sha512-BSVRLCeG3Xt/j/1cCGj1019Wbty0H+Yvu2AOuZSuoaUWn3RatbL33Cxk+Q4jRMRAbOm0p7SLravLjpnT6s0vzQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001154", - "electron-to-chromium": "^1.3.585", + "caniuse-lite": "^1.0.30001157", + "colorette": "^1.2.1", + "electron-to-chromium": "^1.3.591", "escalade": "^3.1.1", - "node-releases": "^1.1.65" + "node-releases": "^1.1.66" } }, "caniuse-lite": { - "version": "1.0.30001157", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001157.tgz", - "integrity": "sha512-gOerH9Wz2IRZ2ZPdMfBvyOi3cjaz4O4dgNwPGzx8EhqAs4+2IL/O+fJsbt+znSigujoZG8bVcIAUM/I/E5K3MA==", + "version": "1.0.30001161", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001161.tgz", + "integrity": "sha512-JharrCDxOqPLBULF9/SPa6yMcBRTjZARJ6sc3cuKrPfyIk64JN6kuMINWqA99Xc8uElMFcROliwtz0n9pYej+g==", "dev": true }, "electron-to-chromium": { - "version": "1.3.591", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.591.tgz", - "integrity": "sha512-ol/0WzjL4NS4Kqy9VD6xXQON91xIihDT36sYCew/G/bnd1v0/4D+kahp26JauQhgFUjrdva3kRSo7URcUmQ+qw==", + "version": "1.3.607", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.607.tgz", + "integrity": "sha512-h2SYNaBnlplGS0YyXl8oJWokfcNxVjJANQfMCsQefG6OSuAuNIeW+A8yGT/ci+xRoBb3k2zq1FrOvkgoKBol8g==", "dev": true }, "node-releases": { - "version": "1.1.66", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.66.tgz", - "integrity": "sha512-JHEQ1iWPGK+38VLB2H9ef2otU4l8s3yAMt9Xf934r6+ojCYDMHPMqvCc9TnzfeFSP1QEOeU6YZEd3+De0LTCgg==", + "version": "1.1.67", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz", + "integrity": "sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==", "dev": true } } @@ -387,21 +388,21 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz", - "integrity": "sha512-k0CIe3tXUKTRSoEx1LQEPFU9vRQfqHtl+kf8eNnDqb4AUJEy5pz6aIiog+YWtVm2jpggjS1laH68bPsR+KWWPQ==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", + "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", "dev": true, "requires": { - "@babel/types": "^7.12.1" + "@babel/types": "^7.12.7" } }, "@babel/helper-optimise-call-expression": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", - "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.7.tgz", + "integrity": "sha512-I5xc9oSJ2h59OwyUqjv95HRyzxj53DAubUERgQMrpcCEYQyToeHA+NEcUEsVWB4j53RDeskeBJ0SgRAYHDBckw==", "dev": true, "requires": { - "@babel/types": "^7.10.4" + "@babel/types": "^7.12.7" } }, "@babel/helper-replace-supers": { @@ -443,43 +444,43 @@ } }, "@babel/parser": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", - "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz", + "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==", "dev": true }, "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" } }, "@babel/traverse": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.5.tgz", - "integrity": "sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA==", + "version": "7.12.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz", + "integrity": "sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.12.5", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.5", - "@babel/types": "^7.12.5", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.12.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", - "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", + "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", @@ -488,9 +489,9 @@ } }, "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -499,13 +500,12 @@ } }, "@babel/helper-create-regexp-features-plugin": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.1.tgz", - "integrity": "sha512-rsZ4LGvFTZnzdNZR5HZdmJVuXK8834R5QkF3WvcnBhrlVtF0HSIUC6zbreL9MgjTywhKokn8RIYRiq99+DLAxA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz", + "integrity": "sha512-idnutvQPdpbduutvi3JVfEgcVIHooQnhvhx0Nk9isOINOIGYkZea1Pk2JlJRiUnMefrlvr0vkByATBY/mB4vjQ==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.10.4", - "@babel/helper-regex": "^7.10.4", "regexpu-core": "^4.7.1" }, "dependencies": { @@ -525,9 +525,9 @@ "dev": true }, "@babel/types": { - "version": "7.12.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", - "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", + "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", @@ -564,9 +564,9 @@ "dev": true }, "@babel/types": { - "version": "7.12.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", - "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", + "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", @@ -612,9 +612,9 @@ "dev": true }, "@babel/types": { - "version": "7.12.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", - "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", + "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", @@ -719,21 +719,21 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz", - "integrity": "sha512-k0CIe3tXUKTRSoEx1LQEPFU9vRQfqHtl+kf8eNnDqb4AUJEy5pz6aIiog+YWtVm2jpggjS1laH68bPsR+KWWPQ==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", + "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", "dev": true, "requires": { - "@babel/types": "^7.12.1" + "@babel/types": "^7.12.7" } }, "@babel/helper-optimise-call-expression": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", - "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.7.tgz", + "integrity": "sha512-I5xc9oSJ2h59OwyUqjv95HRyzxj53DAubUERgQMrpcCEYQyToeHA+NEcUEsVWB4j53RDeskeBJ0SgRAYHDBckw==", "dev": true, "requires": { - "@babel/types": "^7.10.4" + "@babel/types": "^7.12.7" } }, "@babel/helper-replace-supers": { @@ -775,43 +775,43 @@ } }, "@babel/parser": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", - "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz", + "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==", "dev": true }, "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" } }, "@babel/traverse": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.5.tgz", - "integrity": "sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA==", + "version": "7.12.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz", + "integrity": "sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.12.5", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.5", - "@babel/types": "^7.12.5", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.12.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", - "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", + "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", @@ -820,9 +820,9 @@ } }, "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -845,15 +845,6 @@ "integrity": "sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA==", "dev": true }, - "@babel/helper-regex": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.10.5.tgz", - "integrity": "sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg==", - "dev": true, - "requires": { - "lodash": "^4.17.19" - } - }, "@babel/helper-remap-async-to-generator": { "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz", @@ -953,43 +944,43 @@ } }, "@babel/parser": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", - "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz", + "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==", "dev": true }, "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" } }, "@babel/traverse": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.5.tgz", - "integrity": "sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA==", + "version": "7.12.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz", + "integrity": "sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.12.5", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.5", - "@babel/types": "^7.12.5", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.12.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", - "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", + "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", @@ -998,9 +989,9 @@ } }, "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -1036,9 +1027,9 @@ "dev": true }, "@babel/types": { - "version": "7.12.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", - "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", + "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", @@ -1064,9 +1055,9 @@ "dev": true }, "@babel/types": { - "version": "7.12.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", - "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", + "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", @@ -1398,9 +1389,9 @@ } }, "@babel/plugin-proposal-numeric-separator": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.5.tgz", - "integrity": "sha512-UiAnkKuOrCyjZ3sYNHlRlfuZJbBHknMQ9VMwVeX97Ofwx7RpD6gS2HfqTCh8KNUQgcOm8IKt103oR4KIjh7Q8g==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz", + "integrity": "sha512-8c+uy0qmnRTeukiGsjLGy6uVs/TFjJchGXUeBqlG4VWYOdJWkhhVPdQ3uHwbmalfJwv2JsV0qffXP4asRfL2SQ==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", @@ -1453,9 +1444,9 @@ } }, "@babel/plugin-proposal-optional-chaining": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.1.tgz", - "integrity": "sha512-c2uRpY6WzaVDzynVY9liyykS+kVU+WRZPMPYpkelXH8KBt1oXoI89kPbZKKG/jDT5UK92FTW2fZkZaJhdiBabw==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz", + "integrity": "sha512-4ovylXZ0PWmwoOvhU2vhnzVNnm88/Sm9nx7V8BPgMvAzn5zDou3/Awy0EjglyubVHasJj+XCEkr/r1X3P5elCA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4", @@ -1912,26 +1903,26 @@ } }, "@babel/parser": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", - "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz", + "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==", "dev": true }, "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" } }, "@babel/types": { - "version": "7.12.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", - "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", + "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", @@ -2136,21 +2127,21 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz", - "integrity": "sha512-k0CIe3tXUKTRSoEx1LQEPFU9vRQfqHtl+kf8eNnDqb4AUJEy5pz6aIiog+YWtVm2jpggjS1laH68bPsR+KWWPQ==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", + "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", "dev": true, "requires": { - "@babel/types": "^7.12.1" + "@babel/types": "^7.12.7" } }, "@babel/helper-optimise-call-expression": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", - "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.7.tgz", + "integrity": "sha512-I5xc9oSJ2h59OwyUqjv95HRyzxj53DAubUERgQMrpcCEYQyToeHA+NEcUEsVWB4j53RDeskeBJ0SgRAYHDBckw==", "dev": true, "requires": { - "@babel/types": "^7.10.4" + "@babel/types": "^7.12.7" } }, "@babel/helper-plugin-utils": { @@ -2198,43 +2189,43 @@ } }, "@babel/parser": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", - "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz", + "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==", "dev": true }, "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" } }, "@babel/traverse": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.5.tgz", - "integrity": "sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA==", + "version": "7.12.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz", + "integrity": "sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.12.5", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.5", - "@babel/types": "^7.12.5", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.12.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", - "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", + "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", @@ -2243,9 +2234,9 @@ } }, "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -2369,13 +2360,12 @@ } }, "@babel/plugin-transform-sticky-regex": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.1.tgz", - "integrity": "sha512-CiUgKQ3AGVk7kveIaPEET1jNDhZZEl1RPMWdTBE1799bdz++SwqDHStmxfCtDfBhQgCl38YRiSnrMuUMZIWSUQ==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz", + "integrity": "sha512-VEiqZL5N/QvDbdjfYQBhruN0HYjSPjC4XkeqW4ny/jNtH9gcbgaqBIXYEZCNnESMAGs0/K/R7oFGMhOyu/eIxg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-regex": "^7.10.4" + "@babel/helper-plugin-utils": "^7.10.4" }, "dependencies": { "@babel/helper-plugin-utils": { @@ -2456,14 +2446,14 @@ } }, "@babel/preset-env": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.1.tgz", - "integrity": "sha512-H8kxXmtPaAGT7TyBvSSkoSTUK6RHh61So05SyEbpmr0MCZrsNYn7mGMzzeYoOUCdHzww61k8XBft2TaES+xPLg==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.7.tgz", + "integrity": "sha512-OnNdfAr1FUQg7ksb7bmbKoby4qFOHw6DKWWUNB9KqnnCldxhxJlP+21dpyaWFmf2h0rTbOkXJtAGevY3XW1eew==", "dev": true, "requires": { - "@babel/compat-data": "^7.12.1", - "@babel/helper-compilation-targets": "^7.12.1", - "@babel/helper-module-imports": "^7.12.1", + "@babel/compat-data": "^7.12.7", + "@babel/helper-compilation-targets": "^7.12.5", + "@babel/helper-module-imports": "^7.12.5", "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-validator-option": "^7.12.1", "@babel/plugin-proposal-async-generator-functions": "^7.12.1", @@ -2473,10 +2463,10 @@ "@babel/plugin-proposal-json-strings": "^7.12.1", "@babel/plugin-proposal-logical-assignment-operators": "^7.12.1", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", - "@babel/plugin-proposal-numeric-separator": "^7.12.1", + "@babel/plugin-proposal-numeric-separator": "^7.12.7", "@babel/plugin-proposal-object-rest-spread": "^7.12.1", "@babel/plugin-proposal-optional-catch-binding": "^7.12.1", - "@babel/plugin-proposal-optional-chaining": "^7.12.1", + "@babel/plugin-proposal-optional-chaining": "^7.12.7", "@babel/plugin-proposal-private-methods": "^7.12.1", "@babel/plugin-proposal-unicode-property-regex": "^7.12.1", "@babel/plugin-syntax-async-generators": "^7.8.0", @@ -2518,14 +2508,14 @@ "@babel/plugin-transform-reserved-words": "^7.12.1", "@babel/plugin-transform-shorthand-properties": "^7.12.1", "@babel/plugin-transform-spread": "^7.12.1", - "@babel/plugin-transform-sticky-regex": "^7.12.1", + "@babel/plugin-transform-sticky-regex": "^7.12.7", "@babel/plugin-transform-template-literals": "^7.12.1", "@babel/plugin-transform-typeof-symbol": "^7.12.1", "@babel/plugin-transform-unicode-escapes": "^7.12.1", "@babel/plugin-transform-unicode-regex": "^7.12.1", "@babel/preset-modules": "^0.1.3", - "@babel/types": "^7.12.1", - "core-js-compat": "^3.6.2", + "@babel/types": "^7.12.7", + "core-js-compat": "^3.7.0", "semver": "^5.5.0" }, "dependencies": { @@ -2590,21 +2580,21 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz", - "integrity": "sha512-k0CIe3tXUKTRSoEx1LQEPFU9vRQfqHtl+kf8eNnDqb4AUJEy5pz6aIiog+YWtVm2jpggjS1laH68bPsR+KWWPQ==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", + "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", "dev": true, "requires": { - "@babel/types": "^7.12.1" + "@babel/types": "^7.12.7" } }, "@babel/helper-optimise-call-expression": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", - "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.7.tgz", + "integrity": "sha512-I5xc9oSJ2h59OwyUqjv95HRyzxj53DAubUERgQMrpcCEYQyToeHA+NEcUEsVWB4j53RDeskeBJ0SgRAYHDBckw==", "dev": true, "requires": { - "@babel/types": "^7.10.4" + "@babel/types": "^7.12.7" } }, "@babel/helper-plugin-utils": { @@ -2652,9 +2642,9 @@ } }, "@babel/parser": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.5.tgz", - "integrity": "sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz", + "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==", "dev": true }, "@babel/plugin-syntax-class-properties": { @@ -2719,37 +2709,37 @@ } }, "@babel/template": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", - "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.10.4", - "@babel/types": "^7.10.4" + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" } }, "@babel/traverse": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.5.tgz", - "integrity": "sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA==", + "version": "7.12.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz", + "integrity": "sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/generator": "^7.12.5", "@babel/helper-function-name": "^7.10.4", "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.5", - "@babel/types": "^7.12.5", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" } }, "@babel/types": { - "version": "7.12.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.6.tgz", - "integrity": "sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==", + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", + "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.10.4", @@ -2758,9 +2748,9 @@ } }, "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -9091,43 +9081,44 @@ "dev": true }, "core-js-compat": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.7.0.tgz", - "integrity": "sha512-V8yBI3+ZLDVomoWICO6kq/CD28Y4r1M7CWeO4AGpMdMfseu8bkSubBmUPySMGKRTS+su4XQ07zUkAsiu9FCWTg==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.0.tgz", + "integrity": "sha512-o9QKelQSxQMYWHXc/Gc4L8bx/4F7TTraE5rhuN8I7mKBt5dBIUpXpIR3omv70ebr8ST5R3PqbDQr+ZI3+Tt1FQ==", "dev": true, "requires": { - "browserslist": "^4.14.6", + "browserslist": "^4.14.7", "semver": "7.0.0" }, "dependencies": { "browserslist": { - "version": "4.14.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.6.tgz", - "integrity": "sha512-zeFYcUo85ENhc/zxHbiIp0LGzzTrE2Pv2JhxvS7kpUb9Q9D38kUX6Bie7pGutJ/5iF5rOxE7CepAuWD56xJ33A==", + "version": "4.14.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.7.tgz", + "integrity": "sha512-BSVRLCeG3Xt/j/1cCGj1019Wbty0H+Yvu2AOuZSuoaUWn3RatbL33Cxk+Q4jRMRAbOm0p7SLravLjpnT6s0vzQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001154", - "electron-to-chromium": "^1.3.585", + "caniuse-lite": "^1.0.30001157", + "colorette": "^1.2.1", + "electron-to-chromium": "^1.3.591", "escalade": "^3.1.1", - "node-releases": "^1.1.65" + "node-releases": "^1.1.66" } }, "caniuse-lite": { - "version": "1.0.30001157", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001157.tgz", - "integrity": "sha512-gOerH9Wz2IRZ2ZPdMfBvyOi3cjaz4O4dgNwPGzx8EhqAs4+2IL/O+fJsbt+znSigujoZG8bVcIAUM/I/E5K3MA==", + "version": "1.0.30001161", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001161.tgz", + "integrity": "sha512-JharrCDxOqPLBULF9/SPa6yMcBRTjZARJ6sc3cuKrPfyIk64JN6kuMINWqA99Xc8uElMFcROliwtz0n9pYej+g==", "dev": true }, "electron-to-chromium": { - "version": "1.3.591", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.591.tgz", - "integrity": "sha512-ol/0WzjL4NS4Kqy9VD6xXQON91xIihDT36sYCew/G/bnd1v0/4D+kahp26JauQhgFUjrdva3kRSo7URcUmQ+qw==", + "version": "1.3.607", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.607.tgz", + "integrity": "sha512-h2SYNaBnlplGS0YyXl8oJWokfcNxVjJANQfMCsQefG6OSuAuNIeW+A8yGT/ci+xRoBb3k2zq1FrOvkgoKBol8g==", "dev": true }, "node-releases": { - "version": "1.1.66", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.66.tgz", - "integrity": "sha512-JHEQ1iWPGK+38VLB2H9ef2otU4l8s3yAMt9Xf934r6+ojCYDMHPMqvCc9TnzfeFSP1QEOeU6YZEd3+De0LTCgg==", + "version": "1.1.67", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz", + "integrity": "sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==", "dev": true }, "semver": { diff --git a/package.json b/package.json index 9593f83d0e..52a5cd92f2 100644 --- a/package.json +++ b/package.json @@ -165,7 +165,7 @@ "devDependencies": { "@babel/core": "^7.12.3", "@babel/plugin-transform-runtime": "^7.12.1", - "@babel/preset-env": "^7.12.1", + "@babel/preset-env": "^7.12.7", "@opengovsg/mockpass": "^2.5.6", "@shelf/jest-mongodb": "^1.2.3", "@types/bcrypt": "^3.0.0", From 84c7d7c3568191849f96f7f0390a767fa8f4b08e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Nov 2020 19:47:11 +0800 Subject: [PATCH 20/34] fix(deps): bump opossum from 5.0.1 to 5.0.2 (#738) Bumps [opossum](https://github.com/nodeshift/opossum) from 5.0.1 to 5.0.2. - [Release notes](https://github.com/nodeshift/opossum/releases) - [Changelog](https://github.com/nodeshift/opossum/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeshift/opossum/compare/v5.0.1...v5.0.2) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 37f72f6657..228769c3d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19731,9 +19731,9 @@ } }, "opossum": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/opossum/-/opossum-5.0.1.tgz", - "integrity": "sha512-iUDUQmFl3RanaBVLMDTZ6WtXj/Hk84pwJ5JWoJaQd1lXGifdApHhszI3biZvdBDdpTERCmB6x+7+uNvzhzVZIg==" + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/opossum/-/opossum-5.0.2.tgz", + "integrity": "sha512-WLroHTijCRgt539CF563QnkEUuoR8j1O1fzFmnXCbUM0Pc6qfb2LOjW7ekVJKoZ4djPaqpXCzAvhJb8T+pQEUg==" }, "optimist": { "version": "0.6.1", diff --git a/package.json b/package.json index 52a5cd92f2..19bf29dc6a 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ "node-cache": "^5.1.2", "nodemailer": "^6.4.16", "nodemailer-direct-transport": "~3.3.2", - "opossum": "^5.0.1", + "opossum": "^5.0.2", "promise-retry": "^2.0.1", "puppeteer-core": "^5.3.1", "selectize": "0.12.6", From 0fb552c1add83784639b7f43c208a39e6d167024 Mon Sep 17 00:00:00 2001 From: Kar Rui Lau Date: Mon, 30 Nov 2020 12:06:05 +0800 Subject: [PATCH 21/34] ref: migrate get feedback flow to TypeScript (#735) --- .../admin-forms.server.controller.js | 48 --- .../__tests__/feedback.service.spec.ts | 123 +++++++ src/app/modules/feedback/feedback.service.ts | 62 ++++ src/app/modules/feedback/feedback.types.ts | 14 + .../__tests__/admin-form.controller.spec.ts | 308 ++++++++++++++++++ .../form/admin-form/admin-form.controller.ts | 43 +++ src/app/routes/admin-forms.server.routes.js | 2 +- src/types/form_feedback.ts | 2 + .../admin-forms.server.controller.spec.js | 57 ---- 9 files changed, 553 insertions(+), 106 deletions(-) create mode 100644 src/app/modules/feedback/feedback.types.ts diff --git a/src/app/controllers/admin-forms.server.controller.js b/src/app/controllers/admin-forms.server.controller.js index 30380c315c..9c5a2560eb 100644 --- a/src/app/controllers/admin-forms.server.controller.js +++ b/src/app/controllers/admin-forms.server.controller.js @@ -4,7 +4,6 @@ * Module dependencies. */ const mongoose = require('mongoose') -const moment = require('moment-timezone') const _ = require('lodash') const { StatusCodes } = require('http-status-codes') @@ -22,8 +21,6 @@ const { getEmailFormModel, } = require('../models/form.server.model') const getFormModel = require('../models/form.server.model').default -const getFormFeedbackModel = require('../models/form_feedback.server.model') - .default const getSubmissionModel = require('../models/submission.server.model').default const { ResponseMode } = require('../../types') @@ -417,51 +414,6 @@ function makeModule(connection) { } }) }, - /** - * Return form feedback matching query - * @param {Object} req - Express request object - * @param {Object} res - Express response object - */ - getFeedback: function (req, res) { - let FormFeedback = getFormFeedbackModel(connection) - let query = FormFeedback.find({ formId: req.form._id }).sort({ - created: 1, - }) - query.exec(function (err, feedback) { - if (err) { - return respondOnMongoError(req, res, err) - } else if (!feedback) { - return res - .status(StatusCodes.NOT_FOUND) - .json({ message: 'No feedback found' }) - } else { - let sum = 0 - let count = 0 - feedback = feedback.map(function (element) { - sum += element.rating - count += 1 - return { - index: count, - timestamp: moment(element.created).valueOf(), - rating: element.rating, - comment: element.comment, - date: moment(element.created) - .tz('Asia/Singapore') - .format('D MMM YYYY'), - dateShort: moment(element.created) - .tz('Asia/Singapore') - .format('D MMM'), - } - }) - let average = count > 0 ? (sum / count).toFixed(2) : undefined - return res.json({ - average: average, - count: count, - feedback: feedback, - }) - } - }) - }, /** * Submit feedback when previewing forms * Preview feedback is not stored diff --git a/src/app/modules/feedback/__tests__/feedback.service.spec.ts b/src/app/modules/feedback/__tests__/feedback.service.spec.ts index 4dd4730873..242d133c21 100644 --- a/src/app/modules/feedback/__tests__/feedback.service.spec.ts +++ b/src/app/modules/feedback/__tests__/feedback.service.spec.ts @@ -1,5 +1,6 @@ import { ObjectId } from 'bson-ext' import { times } from 'lodash' +import moment from 'moment-timezone' import mongoose from 'mongoose' import getFormFeedbackModel from 'src/app/models/form_feedback.server.model' @@ -8,6 +9,7 @@ import dbHandler from 'tests/unit/backend/helpers/jest-db' import { DatabaseError } from '../../core/core.errors' import * as FeedbackService from '../feedback.service' +import { FeedbackResponse } from '../feedback.types' const FormFeedback = getFormFeedbackModel(mongoose) @@ -103,4 +105,125 @@ describe('feedback.service', () => { expect(streamSpy).toHaveBeenCalledWith(mockFormId) }) }) + + describe('getFormFeedbacks', () => { + it('should return correct feedback responses', async () => { + // Arrange + const expectedCount = 3 + const mockFormId = new ObjectId().toHexString() + const expectedFbPromises = times(expectedCount, (count) => + FormFeedback.create({ + formId: mockFormId, + comment: `cool form ${count}`, + rating: 5 - count, + }), + ) + // Add another feedback with a different form id. + await FormFeedback.create({ + formId: new ObjectId(), + comment: 'boo this form sux', + rating: 1, + }) + const expectedCreatedFbs = await Promise.all(expectedFbPromises) + const expectedFeedbackList = expectedCreatedFbs.map((fb, idx) => ({ + index: idx + 1, + timestamp: moment(fb.created).valueOf(), + rating: fb.rating, + comment: fb.comment, + date: moment(fb.created).tz('Asia/Singapore').format('D MMM YYYY'), + dateShort: moment(fb.created).tz('Asia/Singapore').format('D MMM'), + })) + + // Act + const actualResult = await FeedbackService.getFormFeedbacks(mockFormId) + + // Assert + // Should only average from the feedbacks for given formId. + const expectedAverage = ( + expectedCreatedFbs.reduce((acc, curr) => acc + curr.rating, 0) / + expectedCount + ).toFixed(2) + expect(actualResult.isOk()).toEqual(true) + expect(actualResult._unsafeUnwrap()).toEqual({ + average: expectedAverage, + count: expectedCount, + feedback: expectedFeedbackList, + }) + }) + + it('should return feedback response with zero count and empty array when no feedback is available', async () => { + // Arrange + const mockFormId = new ObjectId().toHexString() + + // Act + const actualResult = await FeedbackService.getFormFeedbacks(mockFormId) + + // Assert + expect(actualResult.isOk()).toEqual(true) + expect(actualResult._unsafeUnwrap()).toEqual({ + count: 0, + feedback: [], + }) + }) + + it('should return feedback response with empty string comment if feedback comment is undefined', async () => { + // Arrange + const mockFormId = new ObjectId().toHexString() + const createdFb = await FormFeedback.create({ + formId: mockFormId, + // Missing comment key value. + rating: 3, + }) + + // Act + const actualResult = await FeedbackService.getFormFeedbacks(mockFormId) + + // Assert + const expectedResult: FeedbackResponse = { + count: 1, + average: '3.00', + feedback: [ + { + index: 1, + timestamp: moment(createdFb.created).valueOf(), + rating: createdFb.rating, + // Empty comment string + comment: '', + date: moment(createdFb.created) + .tz('Asia/Singapore') + .format('D MMM YYYY'), + dateShort: moment(createdFb.created) + .tz('Asia/Singapore') + .format('D MMM'), + }, + ], + } + expect(actualResult.isOk()).toEqual(true) + expect(actualResult._unsafeUnwrap()).toEqual(expectedResult) + }) + + it('should return DatabaseError when error occurs whilst querying database', async () => { + // Arrange + const mockFormId = new ObjectId().toHexString() + const sortSpy = jest.fn().mockReturnThis() + const findSpy = jest.spyOn(FormFeedback, 'find').mockImplementationOnce( + () => + (({ + sort: sortSpy, + exec: () => Promise.reject(new Error('boom')), + } as unknown) as mongoose.Query), + ) + + // Act + const actualResult = await FeedbackService.getFormFeedbacks(mockFormId) + + // Assert + expect(findSpy).toHaveBeenCalledWith({ + formId: mockFormId, + }) + expect(sortSpy).toHaveBeenCalledWith({ created: 1 }) + expect(actualResult.isErr()).toEqual(true) + expect(actualResult._unsafeUnwrapErr()).toBeInstanceOf(DatabaseError) + }) + }) }) diff --git a/src/app/modules/feedback/feedback.service.ts b/src/app/modules/feedback/feedback.service.ts index 6bada6b8aa..8156a8624d 100644 --- a/src/app/modules/feedback/feedback.service.ts +++ b/src/app/modules/feedback/feedback.service.ts @@ -1,3 +1,5 @@ +import { isEmpty } from 'lodash' +import moment from 'moment-timezone' import mongoose from 'mongoose' import { ResultAsync } from 'neverthrow' @@ -7,6 +9,8 @@ import getFormFeedbackModel from '../../models/form_feedback.server.model' import { getMongoErrorMessage } from '../../utils/handle-mongo-error' import { DatabaseError } from '../core/core.errors' +import { FeedbackResponse, ProcessedFeedback } from './feedback.types' + const FormFeedbackModel = getFormFeedbackModel(mongoose) const logger = createLoggerWithLabel(module) @@ -47,3 +51,61 @@ export const getFormFeedbackStream = ( ): mongoose.QueryCursor => { return FormFeedbackModel.getFeedbackCursorByFormId(formId) } + +/** + * Returned processed object containing count of feedback, average rating, and + * list of feedback. + * @param formId the form to retrieve feedback for + * @returns ok(feedback response object) on success + * @returns err(DatabaseError) if database error occurs during query + */ +export const getFormFeedbacks = ( + formId: string, +): ResultAsync => { + return ResultAsync.fromPromise( + FormFeedbackModel.find({ formId }).sort({ created: 1 }).exec(), + (error) => { + logger.error({ + message: 'Error retrieving feedback documents from database', + meta: { + action: 'getFormFeedbacks', + formId, + }, + error, + }) + + return new DatabaseError(getMongoErrorMessage(error)) + }, + ).map((feedbacks) => { + if (isEmpty(feedbacks)) { + return { + count: 0, + feedback: [], + } + } + + // Process retrieved feedback. + const totalFeedbackCount = feedbacks.length + let totalRating = 0 + const processedFeedback = feedbacks.map((fb, idx) => { + totalRating += fb.rating + const response: ProcessedFeedback = { + // 1-based indexing. + index: idx + 1, + timestamp: moment(fb.created).valueOf(), + rating: fb.rating, + comment: fb.comment ?? '', + date: moment(fb.created).tz('Asia/Singapore').format('D MMM YYYY'), + dateShort: moment(fb.created).tz('Asia/Singapore').format('D MMM'), + } + + return response + }) + const averageRating = (totalRating / totalFeedbackCount).toFixed(2) + return { + average: averageRating, + count: totalFeedbackCount, + feedback: processedFeedback, + } + }) +} diff --git a/src/app/modules/feedback/feedback.types.ts b/src/app/modules/feedback/feedback.types.ts new file mode 100644 index 0000000000..36db83d845 --- /dev/null +++ b/src/app/modules/feedback/feedback.types.ts @@ -0,0 +1,14 @@ +export type ProcessedFeedback = { + index: number + timestamp: number + rating: number + comment: string + date: string + dateShort: string +} + +export type FeedbackResponse = { + average?: string + count: number + feedback: ProcessedFeedback[] +} diff --git a/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts b/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts index 2dc3eacf40..89bfd00249 100644 --- a/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts +++ b/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts @@ -8,6 +8,7 @@ import { mocked } from 'ts-jest/utils' import * as AuthService from 'src/app/modules/auth/auth.service' import { DatabaseError } from 'src/app/modules/core/core.errors' import * as FeedbackService from 'src/app/modules/feedback/feedback.service' +import { FeedbackResponse } from 'src/app/modules/feedback/feedback.types' import * as SubmissionService from 'src/app/modules/submission/submission.service' import { MissingUserError } from 'src/app/modules/user/user.errors' import { IPopulatedForm, IPopulatedUser } from 'src/types' @@ -1272,4 +1273,311 @@ describe('admin-form.controller', () => { }) }) }) + + describe('handleGetFormFeedbacks', () => { + const MOCK_USER_ID = new ObjectId().toHexString() + const MOCK_FORM_ID = new ObjectId().toHexString() + const MOCK_USER = { + _id: MOCK_USER_ID, + email: 'yetanothertest@example.com', + } as IPopulatedUser + const MOCK_FORM = { + admin: MOCK_USER as IPopulatedUser, + _id: MOCK_FORM_ID, + title: 'mock title again', + } as IPopulatedForm + + const MOCK_REQ = expressHandler.mockRequest({ + params: { + formId: MOCK_FORM_ID, + }, + session: { + user: { + _id: MOCK_USER_ID, + }, + }, + }) + + it('should return 200 with feedback response successfully', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + const expectedFormFeedback: FeedbackResponse = { + count: 212, + feedback: [ + { + comment: 'test feedback', + rating: 5, + date: 'some date', + dateShort: 'some short date', + index: 1, + timestamp: Date.now(), + }, + ], + average: '5.00', + } + // Mock success on all service invocations. + MockUserService.getPopulatedUserById.mockReturnValueOnce( + okAsync(MOCK_USER), + ) + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + okAsync(MOCK_FORM), + ) + MockFeedbackService.getFormFeedbacks.mockReturnValueOnce( + okAsync(expectedFormFeedback), + ) + + // Act + await AdminFormController.handleGetFormFeedbacks( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.json).toHaveBeenCalledWith(expectedFormFeedback) + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID, + level: PermissionLevel.Read, + }, + ) + expect(MockFeedbackService.getFormFeedbacks).toHaveBeenCalledWith( + MOCK_FORM_ID, + ) + }) + + it('should return 403 when user does not have permissions to access form', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + MockUserService.getPopulatedUserById.mockReturnValueOnce( + okAsync(MOCK_USER), + ) + const mockErrorString = 'not allowed' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new ForbiddenFormError(mockErrorString)), + ) + + // Act + await AdminFormController.handleGetFormFeedbacks( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(403) + expect(mockRes.json).toHaveBeenCalledWith({ message: mockErrorString }) + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID, + level: PermissionLevel.Read, + }, + ) + expect(MockFeedbackService.getFormFeedbacks).not.toHaveBeenCalled() + }) + + it('should return 404 when form cannot be found', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + MockUserService.getPopulatedUserById.mockReturnValueOnce( + okAsync(MOCK_USER), + ) + const mockErrorString = 'not found' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new FormNotFoundError(mockErrorString)), + ) + + // Act + await AdminFormController.handleGetFormFeedbacks( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(404) + expect(mockRes.json).toHaveBeenCalledWith({ message: mockErrorString }) + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID, + level: PermissionLevel.Read, + }, + ) + expect(MockFeedbackService.getFormFeedbacks).not.toHaveBeenCalled() + }) + + it('should return 410 when form is archived', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + MockUserService.getPopulatedUserById.mockReturnValueOnce( + okAsync(MOCK_USER), + ) + const mockErrorString = 'form gone' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new FormDeletedError(mockErrorString)), + ) + + // Act + await AdminFormController.handleGetFormFeedbacks( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(410) + expect(mockRes.json).toHaveBeenCalledWith({ message: mockErrorString }) + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID, + level: PermissionLevel.Read, + }, + ) + expect(MockFeedbackService.getFormFeedbacks).not.toHaveBeenCalled() + }) + + it('should return 422 when user in session does not exist in database', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + const mockErrorString = 'user gone' + MockUserService.getPopulatedUserById.mockReturnValueOnce( + errAsync(new MissingUserError(mockErrorString)), + ) + + // Act + await AdminFormController.handleGetFormFeedbacks( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(422) + expect(mockRes.json).toHaveBeenCalledWith({ message: mockErrorString }) + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect( + MockAuthService.getFormAfterPermissionChecks, + ).not.toHaveBeenCalled() + expect(MockFeedbackService.getFormFeedbacks).not.toHaveBeenCalled() + }) + + it('should return 500 when database error occurs whilst retrieving user', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + const mockErrorString = 'db gone' + MockUserService.getPopulatedUserById.mockReturnValueOnce( + errAsync(new DatabaseError(mockErrorString)), + ) + + // Act + await AdminFormController.handleGetFormFeedbacks( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(500) + expect(mockRes.json).toHaveBeenCalledWith({ message: mockErrorString }) + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect( + MockAuthService.getFormAfterPermissionChecks, + ).not.toHaveBeenCalled() + expect(MockFeedbackService.getFormFeedbacks).not.toHaveBeenCalled() + }) + + it('should return 500 when database error occurs whilst retrieving form', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + MockUserService.getPopulatedUserById.mockReturnValueOnce( + okAsync(MOCK_USER), + ) + const mockErrorString = 'db error' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new DatabaseError(mockErrorString)), + ) + + // Act + await AdminFormController.handleGetFormFeedbacks( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(500) + expect(mockRes.json).toHaveBeenCalledWith({ message: mockErrorString }) + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID, + level: PermissionLevel.Read, + }, + ) + expect(MockFeedbackService.getFormFeedbacks).not.toHaveBeenCalled() + }) + + it('should return 500 when database error occurs whilst retrieving form feedback', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + // Mock success on all service invocations. + MockUserService.getPopulatedUserById.mockReturnValueOnce( + okAsync(MOCK_USER), + ) + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + okAsync(MOCK_FORM), + ) + const mockErrorString = 'db boom' + MockFeedbackService.getFormFeedbacks.mockReturnValueOnce( + errAsync(new DatabaseError(mockErrorString)), + ) + + // Act + await AdminFormController.handleGetFormFeedbacks( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toHaveBeenCalledWith(500) + expect(mockRes.json).toHaveBeenCalledWith({ message: mockErrorString }) + expect(MockUserService.getPopulatedUserById).toHaveBeenCalledWith( + MOCK_USER_ID, + ) + expect(MockAuthService.getFormAfterPermissionChecks).toHaveBeenCalledWith( + { + user: MOCK_USER, + formId: MOCK_FORM_ID, + level: PermissionLevel.Read, + }, + ) + expect(MockFeedbackService.getFormFeedbacks).toHaveBeenCalledWith( + MOCK_FORM_ID, + ) + }) + }) }) diff --git a/src/app/modules/form/admin-form/admin-form.controller.ts b/src/app/modules/form/admin-form/admin-form.controller.ts index 45fce0e6e6..8f8a9490ec 100644 --- a/src/app/modules/form/admin-form/admin-form.controller.ts +++ b/src/app/modules/form/admin-form/admin-form.controller.ts @@ -357,3 +357,46 @@ export const handleStreamFormFeedback: RequestHandler<{ return res.end() }) } + +/** + * Handler for GET /{formId}/adminform/feedback. + * @security session + * + * @returns 200 with feedback response + * @returns 403 when user does not have permissions to access form + * @returns 404 when form cannot be found + * @returns 410 when form is archived + * @returns 422 when user in session cannot be retrieved from the database + * @returns 500 when database error occurs + */ +export const handleGetFormFeedbacks: RequestHandler<{ + formId: string +}> = (req, res) => { + const { formId } = req.params + const sessionUserId = (req.session as Express.AuthedSession).user._id + + return UserService.getPopulatedUserById(sessionUserId) + .andThen((user) => + AuthService.getFormAfterPermissionChecks({ + user, + formId, + level: PermissionLevel.Read, + }), + ) + .andThen(() => FeedbackService.getFormFeedbacks(formId)) + .map((fbResponse) => res.json(fbResponse)) + .mapErr((error) => { + logger.error({ + message: 'Error retrieving form feedbacks', + meta: { + action: 'handleGetFormFeedbacks', + ...createReqMeta(req), + userId: sessionUserId, + formId, + }, + error, + }) + const { errorMessage, statusCode } = mapRouteError(error) + return res.status(statusCode).json({ message: errorMessage }) + }) +} diff --git a/src/app/routes/admin-forms.server.routes.js b/src/app/routes/admin-forms.server.routes.js index 211382112d..881116cedb 100644 --- a/src/app/routes/admin-forms.server.routes.js +++ b/src/app/routes/admin-forms.server.routes.js @@ -269,7 +269,7 @@ module.exports = function (app) { app .route('/:formId([a-fA-F0-9]{24})/adminform/feedback') - .get(authActiveForm(PermissionLevel.Read), adminForms.getFeedback) + .get(withUserAuthentication, AdminFormController.handleGetFormFeedbacks) .post(authActiveForm(PermissionLevel.Read), adminForms.passThroughFeedback) /** diff --git a/src/types/form_feedback.ts b/src/types/form_feedback.ts index 8ab08cac7d..31a8d00d3f 100644 --- a/src/types/form_feedback.ts +++ b/src/types/form_feedback.ts @@ -11,6 +11,8 @@ export interface IFormFeedback { export interface IFormFeedbackSchema extends IFormFeedback, Document { _id: Document['_id'] + created?: Date + lastModified?: Date } export interface IFormFeedbackDocument extends IFormFeedbackSchema { created: Date diff --git a/tests/unit/backend/controllers/admin-forms.server.controller.spec.js b/tests/unit/backend/controllers/admin-forms.server.controller.spec.js index f28ca9165f..1e24985f16 100644 --- a/tests/unit/backend/controllers/admin-forms.server.controller.spec.js +++ b/tests/unit/backend/controllers/admin-forms.server.controller.spec.js @@ -6,10 +6,6 @@ const roles = require('../helpers/roles') const User = dbHandler.makeModel('user.server.model', 'User') const Form = dbHandler.makeModel('form.server.model', 'Form') -const FormFeedback = dbHandler.makeModel( - 'form_feedback.server.model', - 'FormFeedback', -) const EmailForm = mongoose.model('email') const Controller = spec( @@ -291,57 +287,4 @@ describe('Admin-Forms Controller', () => { Controller.transferOwner(req, res) }) }) - - describe('getFeedback', () => { - it('should retrieve correct response based on saved FormFeedbacks', (done) => { - // Define feedback to be added to MongoMemoryServer db - let p1 = new FormFeedback({ - formId: mongoose.Types.ObjectId('4edd40c86762e0fb12000003'), - rating: 2, - comment: 'nice', - created: '2018-05-18 09:12:07.126Z', - }).save() - let p2 = new FormFeedback({ - formId: mongoose.Types.ObjectId('4edd40c86762e0fb12000003'), - rating: 5, - comment: 'great', - created: '2018-03-15 03:52:07.126Z', - }).save() - - Promise.all([p1, p2]).then(([_r1, _r2]) => { - req.form = { _id: '4edd40c86762e0fb12000003' } - let expected = { - average: '3.50', - count: 2, - feedback: [ - { - index: 2, - timestamp: 1526634727126, - rating: 2, - comment: 'nice', - date: '18 May 2018', - dateShort: '18 May', - }, - { - index: 1, - timestamp: 1521085927126, - rating: 5, - comment: 'great', - date: '15 Mar 2018', - dateShort: '15 Mar', - }, - ], - } - res.json.and.callFake((args) => { - expect(args.average).toEqual(expected.average) - expect(args.count).toEqual(expected.count) - expect(args.feedback.length).toEqual(2) - expect(args.feedback).toContain(expected.feedback[0]) - expect(args.feedback).toContain(expected.feedback[1]) - done() - }) - Controller.getFeedback(req, res) - }) - }) - }) }) From 111f1faa4588d2646bf28a18affdb34a62248339 Mon Sep 17 00:00:00 2001 From: Kar Rui Lau Date: Mon, 30 Nov 2020 13:49:47 +0800 Subject: [PATCH 22/34] chore: update travis to have multiple ci stages (#742) * chore: update travis to have multiple build stages * chore: use correct paths * chore: remove create * fix: missing script tag * chore: correct order of stages * chore: experiment with only building node_modules once * chore: add default language without install step * chore: use node_js 12 in all Travis stages * chore: run lint-ci concurrently * chore: copy all in coverage workspace * chore: run coveralls after_success * chore: use node testEnvironment when running Jest tests Allows for speed up of Jest tests See: https://itnext.io/how-to-make-your-sluggish-jest-v23-tests-go-faster-1d4f3388bcdd * chore: increase number of parallel workers in Jest to 4 * chore: remove unnecessary coverage reporters in Jest --- .travis.yml | 206 ++++++++++-------- jest.config.js | 2 + package.json | 13 +- .../__tests__/analytics.controller.spec.ts | 2 +- .../field-validation/email-validation.spec.ts | 3 +- 5 files changed, 127 insertions(+), 99 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5d7c944b65..9d71b22e91 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,17 +2,11 @@ os: linux dist: xenial services: - - docker - xvfb language: node_js node_js: '12' -cache: - - npm - - pip - -addons: - chrome: stable +install: true # skip installation, perform in build stage. notifications: email: @@ -23,90 +17,124 @@ notifications: on_success: always on_failure: always -before_script: - - export NODE_OPTIONS=--max-old-space-size=4096 - -script: - - set -e - - npm_config_mode=yes npx lockfile-lint --type npm --path package.json --validate-https --allowed-hosts npm - - npm run lint-ci - - npm run build - - travis_retry npm run test-ci - - npm run test-e2e-ci - -before_deploy: - # Workaround to run before_deploy only once - - > - if ! [ "$TAG" ]; then - pip install --user awscli - # Put AWS in path - export PATH=$PATH:$HOME/.local/bin - # Login to AWS ECR, credentials defined in $AWS_ACCESS_KEY_ID and $AWS_SECRET_ACCESS_KEY - $(aws ecr get-login --no-include-email --region ap-southeast-1) - export TAG=travis-$TRAVIS_COMMIT-$TRAVIS_BUILD_NUMBER - docker build -f Dockerfile.production -t $REPO:$TAG . - docker tag $REPO:$TAG $REPO:$TRAVIS_BRANCH - docker push $REPO - # Add TAG to Dockerrun - sed -i -e "s/@TAG/$TAG/g" Dockerrun.aws.json - zip -r "$TAG.zip" .ebextensions Dockerrun.aws.json - fi - - export ELASTIC_BEANSTALK_LABEL="$TAG-$(env TZ=Asia/Singapore date "+%Y%m%d%H%M%S")" +jobs: + include: + - stage: Build application + install: npm ci + cache: + - npm + - pip + before_script: + - export NODE_OPTIONS=--max-old-space-size=4096 + script: + - set -e + - npm_config_mode=yes npx lockfile-lint --type npm --path package.json --validate-https --allowed-hosts npm + - npm run lint-ci + - npm run build + workspaces: + create: + name: build + paths: . + - stage: Tests + name: Javascript tests + workspaces: + use: build + script: + - travis_retry npm run test-backend-jasmine + - npm run test-frontend + - name: Typescript tests + workspaces: + use: build + script: + - travis_retry npm run test-backend-jest + after_success: + - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js + - name: End-to-end tests + workspaces: + use: build + addons: + chrome: stable + script: + - npm run test-e2e-ci + - stage: Deploy + services: + - docker + workspaces: + use: build + script: skip + before_deploy: + # Workaround to run before_deploy only once + - > + if ! [ "$TAG" ]; then + pip install --user awscli + # Put AWS in path + export PATH=$PATH:$HOME/.local/bin + # Login to AWS ECR, credentials defined in $AWS_ACCESS_KEY_ID and $AWS_SECRET_ACCESS_KEY + $(aws ecr get-login --no-include-email --region ap-southeast-1) + export TAG=travis-$TRAVIS_COMMIT-$TRAVIS_BUILD_NUMBER + docker build -f Dockerfile.production -t $REPO:$TAG . + docker tag $REPO:$TAG $REPO:$TRAVIS_BRANCH + docker push $REPO + # Add TAG to Dockerrun + sed -i -e "s/@TAG/$TAG/g" Dockerrun.aws.json + zip -r "$TAG.zip" .ebextensions Dockerrun.aws.json + fi + - export ELASTIC_BEANSTALK_LABEL="$TAG-$(env TZ=Asia/Singapore date "+%Y%m%d%H%M%S")" + deploy: + - provider: elasticbeanstalk + access_key_id: $AWS_ACCESS_KEY_ID + secret_access_key: $AWS_SECRET_ACCESS_KEY + region: $AWS_REGION + app: $STAGING_APP_NAME + env: $UAT_DEPLOY_ENV + bucket: $STAGING_BUCKET_NAME + zip_file: '$TAG.zip' + on: + branch: $UAT_BRANCH -deploy: - - provider: elasticbeanstalk - skip_cleanup: true - access_key_id: $AWS_ACCESS_KEY_ID - secret_access_key: $AWS_SECRET_ACCESS_KEY - region: $AWS_REGION - app: $STAGING_APP_NAME - env: $UAT_DEPLOY_ENV - bucket: $STAGING_BUCKET_NAME - zip_file: '$TAG.zip' - on: - branch: $UAT_BRANCH + - provider: elasticbeanstalk + access_key_id: $AWS_ACCESS_KEY_ID + secret_access_key: $AWS_SECRET_ACCESS_KEY + region: $AWS_REGION + app: $STAGING_APP_NAME + env: $STAGING_DEPLOY_ENV + bucket: $STAGING_BUCKET_NAME + zip_file: '$TAG.zip' + on: + branch: $STAGING_BRANCH - - provider: elasticbeanstalk - skip_cleanup: true - access_key_id: $AWS_ACCESS_KEY_ID - secret_access_key: $AWS_SECRET_ACCESS_KEY - region: $AWS_REGION - app: $STAGING_APP_NAME - env: $STAGING_DEPLOY_ENV - bucket: $STAGING_BUCKET_NAME - zip_file: '$TAG.zip' - on: - branch: $STAGING_BRANCH + - provider: elasticbeanstalk + access_key_id: $AWS_ACCESS_KEY_ID + secret_access_key: $AWS_SECRET_ACCESS_KEY + region: $AWS_REGION + app: $STAGING_APP_NAME + env: $STAGING_ALT_DEPLOY_ENV + bucket: $STAGING_BUCKET_NAME + zip_file: '$TAG.zip' + on: + branch: $STAGING_ALT_BRANCH - - provider: elasticbeanstalk - skip_cleanup: true - access_key_id: $AWS_ACCESS_KEY_ID - secret_access_key: $AWS_SECRET_ACCESS_KEY - region: $AWS_REGION - app: $STAGING_APP_NAME - env: $STAGING_ALT_DEPLOY_ENV - bucket: $STAGING_BUCKET_NAME - zip_file: '$TAG.zip' - on: - branch: $STAGING_ALT_BRANCH + - provider: elasticbeanstalk + access_key_id: $AWS_ACCESS_KEY_ID + secret_access_key: $AWS_SECRET_ACCESS_KEY + region: $AWS_REGION + app: $PROD_APP_NAME + env: $PROD_DEPLOY_ENV + bucket: $PROD_BUCKET_NAME + zip_file: '$TAG.zip' + on: + branch: $PROD_BRANCH - - provider: elasticbeanstalk - skip_cleanup: true - access_key_id: $AWS_ACCESS_KEY_ID - secret_access_key: $AWS_SECRET_ACCESS_KEY - region: $AWS_REGION - app: $PROD_APP_NAME - env: $PROD_DEPLOY_ENV - bucket: $PROD_BUCKET_NAME - zip_file: '$TAG.zip' - on: - branch: $PROD_BRANCH + after_deploy: + - > + if [[ "$SENTRY_PROJECT" && "$SENTRY_AUTH_TOKEN" && "$SENTRY_ORG" && "$SENTRY_URL" ]]; then + curl -sL https://sentry.io/get-cli/ | bash + sentry-cli releases --org $SENTRY_ORG --project $SENTRY_PROJECT new "$TAG" + sentry-cli releases files "$TAG" upload-sourcemaps ./ --rewrite --ignore-file .sentryignore + sentry-cli releases finalize "$TAG" + fi -after_deploy: - - > - if [[ "$SENTRY_PROJECT" && "$SENTRY_AUTH_TOKEN" && "$SENTRY_ORG" && "$SENTRY_URL" ]]; then - curl -sL https://sentry.io/get-cli/ | bash - sentry-cli releases --org $SENTRY_ORG --project $SENTRY_PROJECT new "$TAG" - sentry-cli releases files "$TAG" upload-sourcemaps ./ --rewrite --ignore-file .sentryignore - sentry-cli releases finalize "$TAG" - fi +stages: + - Build application + - Tests + - Deploy diff --git a/jest.config.js b/jest.config.js index b41144f060..1fc6dc1b43 100644 --- a/jest.config.js +++ b/jest.config.js @@ -11,9 +11,11 @@ module.exports = { }, collectCoverageFrom: ['./src/**/*.{ts,js}', '!**/__tests__/**'], coveragePathIgnorePatterns: ['./node_modules/', './tests'], + coverageReporters: ['lcov', 'text'], coverageThreshold: { global: { statements: 25, // Increase this percentage as test coverage improves }, }, + testEnvironment: 'node', } diff --git a/package.json b/package.json index 19bf29dc6a..42399210b5 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,11 @@ "npm": "~6.4.0" }, "scripts": { + "test-backend-jest": "env-cmd -f tests/.test-full-env jest --coverage --maxWorkers=4", + "test-backend-jest:watch": "env-cmd -f tests/.test-full-env jest --watch", + "test-backend-jasmine": "env-cmd -f tests/.test-full-env --use-shell \"npm run download-binary && jasmine --config=tests/unit/backend/jasmine.json\"", + "test-frontend": "jest --config=tests/unit/frontend/jest.config.js", + "test-backend": "npm run test-backend-jasmine && npm run test-backend-jest", "build": "npm run build-backend && npm run build-frontend", "build-backend": "tsc -p tsconfig.build.json", "build-frontend": "webpack --config webpack.prod.js", @@ -24,12 +29,6 @@ "start": "node dist/backend/server.js", "dev": "docker-compose up --build", "docker-dev": "npm run build-frontend-dev:watch & ts-node-dev --respawn --transpile-only --inspect=0.0.0.0 --exit-child -- src/server.ts", - "test-backend": "npm run test-backend-jasmine && npm run test-backend-jest", - "test-backend-jest": "env-cmd -f tests/.test-full-env jest --coverage --maxWorkers=2", - "test-backend-jest:watch": "env-cmd -f tests/.test-full-env jest --watch", - "test-backend-jasmine": "env-cmd -f tests/.test-full-env --use-shell \"npm run download-binary && jasmine --config=tests/unit/backend/jasmine.json\"", - "test-frontend": "jest --config=tests/unit/frontend/jest.config.js", - "test-ci": "npm run test-backend && npm run test-frontend && coveralls < coverage/lcov.info", "test": "npm run build-backend && npm run test-backend && npm run test-frontend", "test-e2e-build": "npm run build-backend && npm run build-frontend-dev", "test-run": "concurrently --success last --kill-others \"mockpass\" \"maildev\" \"node dist/backend/server.js\" \"node ./tests/mock-webhook-server.js\"", @@ -44,7 +43,7 @@ "lint-style": "stylelint '*/**/*.css' --quiet --fix", "lint-html": "htmlhint && prettier --write './src/public/**/*.html' --ignore-path './dist/**' --loglevel silent", "lint": "npm run lint-code && npm run lint-style && npm run lint-html", - "lint-ci": "eslint src/ --quiet && stylelint '*/**/*.css' --quiet && htmlhint && prettier --c './src/public/**/*.html' --ignore-path './dist/**'", + "lint-ci": "concurrently \"eslint src/ --quiet\" \"stylelint '*/**/*.css' --quiet\" \"htmlhint\" \"prettier --c './src/public/**/*.html' --ignore-path './dist/**'\"", "version": "auto-changelog -p && git add CHANGELOG.md" }, "husky": { diff --git a/src/app/modules/analytics/__tests__/analytics.controller.spec.ts b/src/app/modules/analytics/__tests__/analytics.controller.spec.ts index 693e04bc99..e04a59ff09 100644 --- a/src/app/modules/analytics/__tests__/analytics.controller.spec.ts +++ b/src/app/modules/analytics/__tests__/analytics.controller.spec.ts @@ -1,8 +1,8 @@ -import { DatabaseError } from 'dist/backend/app/modules/core/core.errors' import { errAsync, okAsync } from 'neverthrow' import expressHandler from 'tests/unit/backend/helpers/jest-express' +import { DatabaseError } from '../../core/core.errors' import * as AnalyticsController from '../analytics.controller' import { AnalyticsFactory } from '../analytics.factory' import * as AnalyticsService from '../analytics.service' diff --git a/tests/unit/backend/utils/field-validation/email-validation.spec.ts b/tests/unit/backend/utils/field-validation/email-validation.spec.ts index 843e3beb61..b2e6c13d64 100644 --- a/tests/unit/backend/utils/field-validation/email-validation.spec.ts +++ b/tests/unit/backend/utils/field-validation/email-validation.spec.ts @@ -1,10 +1,9 @@ +import { ValidateFieldError } from 'src/app/modules/submission/submission.errors' import { validateField } from 'src/app/utils/field-validation' import EmailValidator from 'src/app/utils/field-validation/validators/EmailValidator.class' import { BasicField } from 'src/types/field/fieldTypes' import { ISingleAnswerResponse } from 'src/types/response' -import { ValidateFieldError } from '../../../../../dist/backend/app/modules/submission/submission.errors' - describe('Email field validation', () => { beforeEach(() => { jest From 7c8c42f47d9abc547d94bccb7edc052dbe7b9c72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Nov 2020 13:59:19 +0800 Subject: [PATCH 23/34] chore(deps-dev): bump ts-mock-imports from 1.3.0 to 1.3.1 (#747) Bumps [ts-mock-imports](https://github.com/EmandM/ts-mock-imports) from 1.3.0 to 1.3.1. - [Release notes](https://github.com/EmandM/ts-mock-imports/releases) - [Commits](https://github.com/EmandM/ts-mock-imports/compare/v1.3.0...v1.3.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 228769c3d9..e16ee7ca2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25346,9 +25346,9 @@ } }, "ts-mock-imports": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-mock-imports/-/ts-mock-imports-1.3.0.tgz", - "integrity": "sha512-cCrVcRYsp84eDvPict0ZZD/D7ppQ0/JSx4ve6aEU8DjlsaWRJWV6ADMovp2sCuh6pZcduLFoIYhKTDU2LARo7Q==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/ts-mock-imports/-/ts-mock-imports-1.3.1.tgz", + "integrity": "sha512-MKEGXb40TUbpQ6b/if424zs0gfTyHfsebw+FUBkqbC0kVoPwoXhoe82lJH4dC92j4vDoId6pSjtIvwvtSMnS5w==", "dev": true }, "ts-node": { diff --git a/package.json b/package.json index 42399210b5..713574f120 100644 --- a/package.json +++ b/package.json @@ -248,7 +248,7 @@ "testcafe": "^1.9.4", "ts-jest": "^26.4.4", "ts-loader": "^7.0.5", - "ts-mock-imports": "^1.3.0", + "ts-mock-imports": "^1.3.1", "ts-node": "^9.0.0", "ts-node-dev": "^1.0.0", "type-fest": "^0.18.0", From 5665c824f9dfd0e209fb166e8d403d849911dcbd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Nov 2020 14:02:54 +0800 Subject: [PATCH 24/34] chore(deps-dev): bump @types/mongoose from 5.10.0 to 5.10.1 (#746) Bumps [@types/mongoose](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/mongoose) from 5.10.0 to 5.10.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/mongoose) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index e16ee7ca2e..015c4a5216 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4642,9 +4642,9 @@ "dev": true }, "@types/mongoose": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.10.0.tgz", - "integrity": "sha512-inJhRgmsYC8JWB3ABUjCreNmMoML/ffu/y8LtTYe7GAW6/B8rVLQU16am73kkceasKX75p41WjU8yquUx/CSAw==", + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.10.1.tgz", + "integrity": "sha512-5yqbLHOyCQhUb7GPGW0A2dauUbhwgBvUWMzYcaUQiHdLZ8slgRp2R6i8FETZ+t5xeXpfhylYp9U7dAng7WamqQ==", "dev": true, "requires": { "@types/mongodb": "*", diff --git a/package.json b/package.json index 713574f120..401583a6f8 100644 --- a/package.json +++ b/package.json @@ -185,7 +185,7 @@ "@types/json-stringify-safe": "^5.0.0", "@types/mongodb": "^3.5.34", "@types/mongodb-uri": "^0.9.0", - "@types/mongoose": "^5.10.0", + "@types/mongoose": "^5.10.1", "@types/node": "^14.14.7", "@types/nodemailer": "^6.4.0", "@types/nodemailer-direct-transport": "^1.0.31", From a55f5629324e9afab76492a2e353cc0e6bdaada7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Nov 2020 17:50:58 +0800 Subject: [PATCH 25/34] fix(deps): bump celebrate from 13.0.3 to 13.0.4 (#756) Bumps [celebrate](https://github.com/arb/celebrate) from 13.0.3 to 13.0.4. - [Release notes](https://github.com/arb/celebrate/releases) - [Commits](https://github.com/arb/celebrate/compare/v13.0.3...v13.0.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 56 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 015c4a5216..ca94a8d2a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2911,29 +2911,11 @@ } } }, - "@hapi/address": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@hapi/address/-/address-4.1.0.tgz", - "integrity": "sha512-SkszZf13HVgGmChdHo/PxchnSaCJ6cetVqLzyciudzZRT0jcOouIF/Q93mgjw8cce+D+4F4C1Z/WrfFN+O3VHQ==", - "requires": { - "@hapi/hoek": "^9.0.0" - } - }, - "@hapi/formula": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-2.0.0.tgz", - "integrity": "sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A==" - }, "@hapi/hoek": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.1.0.tgz", "integrity": "sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw==" }, - "@hapi/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw==" - }, "@hapi/topo": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.0.0.tgz", @@ -4177,6 +4159,24 @@ } } }, + "@sideway/address": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.0.tgz", + "integrity": "sha512-wAH/JYRXeIFQRsxerIuLjgUu2Xszam+O5xKeatJ4oudShOOirfmsQ1D6LL54XOU2tizpCYku+s1wmU0SYdpoSA==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@sideway/formula": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", + "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==" + }, + "@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "@sinonjs/commons": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz", @@ -8009,9 +8009,9 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "celebrate": { - "version": "13.0.3", - "resolved": "https://registry.npmjs.org/celebrate/-/celebrate-13.0.3.tgz", - "integrity": "sha512-RJc48EMR0Z3Anp8nXK241NvpXExQaqwf49aQYkzsR+z+X7nBSTORBPZWLBARjiJP1jnfxLJ4XnXDknHDMt8rRA==", + "version": "13.0.4", + "resolved": "https://registry.npmjs.org/celebrate/-/celebrate-13.0.4.tgz", + "integrity": "sha512-gUtAjEtFyY9PvuuQJq1uyuF46gLetVZzyUKXBDBqqvgzCjTSfwXP8L+WcGt1NrLQvUxXdlzhFolW2Bt9DDEV+g==", "requires": { "escape-html": "1.0.3", "joi": "17.x.x", @@ -16600,15 +16600,15 @@ "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" }, "joi": { - "version": "17.2.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.2.1.tgz", - "integrity": "sha512-YT3/4Ln+5YRpacdmfEfrrKh50/kkgX3LgBltjqnlMPIYiZ4hxXZuVJcxmsvxsdeHg9soZfE3qXxHC2tMpCCBOA==", + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.3.0.tgz", + "integrity": "sha512-Qh5gdU6niuYbUIUV5ejbsMiiFmBdw8Kcp8Buj2JntszCkCfxJ9Cz76OtHxOZMPXrt5810iDIXs+n1nNVoquHgg==", "requires": { - "@hapi/address": "^4.1.0", - "@hapi/formula": "^2.0.0", "@hapi/hoek": "^9.0.0", - "@hapi/pinpoint": "^2.0.0", - "@hapi/topo": "^5.0.0" + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.0", + "@sideway/formula": "^3.0.0", + "@sideway/pinpoint": "^2.0.0" } }, "jose": { diff --git a/package.json b/package.json index 401583a6f8..a192769962 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "boxicons": "1.8.0", "bson-ext": "^2.0.5", "busboy": "^0.3.1", - "celebrate": "^13.0.3", + "celebrate": "^13.0.4", "compression": "~1.7.2", "connect-mongo": "^3.2.0", "convict": "^6.0.0", From 20008ed09525fe0424c8445b650e3077882e28a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Nov 2020 17:52:03 +0800 Subject: [PATCH 26/34] fix(deps): bump mongoose from 5.10.15 to 5.10.18 (#758) Bumps [mongoose](https://github.com/Automattic/mongoose) from 5.10.15 to 5.10.18. - [Release notes](https://github.com/Automattic/mongoose/releases) - [Changelog](https://github.com/Automattic/mongoose/blob/master/History.md) - [Commits](https://github.com/Automattic/mongoose/compare/5.10.15...5.10.18) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca94a8d2a3..dab93404fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18813,9 +18813,9 @@ "integrity": "sha1-D3ca0W9IOuZfQoeWlCjp+8SqYYE=" }, "mongoose": { - "version": "5.10.15", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.10.15.tgz", - "integrity": "sha512-3QUWCpMRdFCPIBZkjG/B2OkfMY2WLkR+hv335o4T2mn3ta9kx8qVvXeUDojp3OHMxBZVUyCA+hDyyP4/aKmHuA==", + "version": "5.10.18", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.10.18.tgz", + "integrity": "sha512-vaLUzBpUxqacoCqP/xXWMg/uVwCDrlc8LvYjDXCf8hdApvX/CXa0HLa7v2ieFaVd5Fgv3W2QXODLoC4Z/abbNw==", "requires": { "bson": "^1.1.4", "kareem": "2.3.1", diff --git a/package.json b/package.json index a192769962..8275660cf8 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "lodash": "^4.17.20", "moment-timezone": "0.5.32", "mongodb-uri": "^0.9.7", - "mongoose": "^5.10.15", + "mongoose": "^5.10.18", "multiparty": ">=4.2.2", "neverthrow": "^2.7.1", "ng-infinite-scroll": "^1.3.0", From 0fbfc81f9a9e1fe20d543de0830ebe295893ccf5 Mon Sep 17 00:00:00 2001 From: tshuli <63710093+tshuli@users.noreply.github.com> Date: Tue, 1 Dec 2020 06:52:29 +0800 Subject: [PATCH 27/34] fix: backend validation does not prevent responses on hidden fields (#736) * fix: add check for responses on hidden fields * fix: correct isVisible values for tests * chore: update status code on ValidateFieldError * fix: remove invalid visibility tests from server controller spec because hidden field answers should not even be in the response * chore: add tests for hidden fields in field validator specs * refactor: type FieldResponse as union of Response interfaces * refactor: use else if * chore: tighten type guards for checkbox and table responses * fix: check for table response on hidden field * refactor: TableRow subtype * fix: concatenate flattened array for table response before evaluating length * refactor: beforeAll and afterAll for jasmine clock * chore: add check that all arrays have same length * refactor: type ITableRow is string array * refactor: check if array contains any value by traversing each element instead of using string concatenation * chore: check that answerArray[0] exists and is a tableRow --- .../modules/submission/submission.errors.ts | 4 +- src/app/utils/field-validation/index.ts | 42 +- src/types/response/guards.ts | 54 +++ src/types/response/index.ts | 4 +- ...mail-submissions.server.controller.spec.js | 359 +----------------- .../attachment-validation.spec.js | 11 + .../checkbox-validation.spec.js | 11 + .../field-validation/date-validation.spec.js | 65 ++-- .../decimal-validation.spec.js | 23 ++ .../dropdown-validation.spec.js | 18 + .../field-validation/email-validation.spec.ts | 26 ++ .../home-num-validation.spec.js | 31 +- .../mobile-num-validation.spec.js | 31 +- .../field-validation/nric-validation.spec.js | 19 + .../number-validation.spec.js | 54 ++- .../radio-button-validation.spec.js | 20 + .../rating-validation.spec.js | 21 + .../field-validation/table-validation.spec.js | 11 + .../field-validation/text-validation.spec.js | 30 ++ .../field-validation/yesno-validation.spec.js | 19 + 20 files changed, 438 insertions(+), 415 deletions(-) diff --git a/src/app/modules/submission/submission.errors.ts b/src/app/modules/submission/submission.errors.ts index 5ff7c79636..7449012f4e 100644 --- a/src/app/modules/submission/submission.errors.ts +++ b/src/app/modules/submission/submission.errors.ts @@ -40,7 +40,7 @@ export class ProcessingError extends ApplicationError { * A custom error class returned when given submission has field validation failure */ export class ValidateFieldError extends ApplicationError { - constructor(message = 'Error validating field.') { - super(message) + constructor(message = 'Error validating field.', status = 400) { + super(message, status) } } diff --git a/src/app/utils/field-validation/index.ts b/src/app/utils/field-validation/index.ts index 708b47b21f..af432abc01 100644 --- a/src/app/utils/field-validation/index.ts +++ b/src/app/utils/field-validation/index.ts @@ -9,7 +9,12 @@ import { createLoggerWithLabel } from '../../../config/logger' import { IField } from '../../../types/field/baseField' import { BasicField } from '../../../types/field/fieldTypes' import { FieldResponse } from '../../../types/response' -import { isProcessedSingleAnswerResponse } from '../../../types/response/guards' +import { + isProcessedAttachmentResponse, + isProcessedCheckboxResponse, + isProcessedSingleAnswerResponse, + isProcessedTableResponse, +} from '../../../types/response/guards' import { ValidateFieldError } from '../../modules/submission/submission.errors' import { ALLOWED_VALIDATORS, FIELDS_TO_REJECT } from './config' @@ -41,6 +46,35 @@ const doFieldTypesMatch = ( : right(undefined) } +/** + * Returns true if response appears on a hidden field. + * This may happen if a submission is made programatically to try and bypass form logic. + * @param response The submitted response + */ +const isResponsePresentOnHiddenField = (response: FieldResponse): boolean => { + if (isProcessedSingleAnswerResponse(response)) { + if (!response.isVisible && response.answer.trim() !== '') { + return true + } + } else if (isProcessedCheckboxResponse(response)) { + if (!response.isVisible && response.answerArray.length > 0) { + return true + } + } else if (isProcessedTableResponse(response)) { + if ( + !response.isVisible && + !response.answerArray.every((row) => row.every((elem) => elem === '')) + ) { + return true + } + } else if (isProcessedAttachmentResponse(response)) { + if (!response.isVisible && response.filename.trim() !== '') { + return true + } + } + return false +} + /** * Determines whether a response requires validation. A required field * may not require an answer if it is not visible due to logic. However, @@ -102,6 +136,12 @@ export const validateField = ( return err(new ValidateFieldError(fieldTypeEither.left)) } + if (isResponsePresentOnHiddenField(response)) { + return err( + new ValidateFieldError(`Attempted to submit response on a hidden field`), + ) + } + if (isProcessedSingleAnswerResponse(response)) { if (singleAnswerRequiresValidation(formField, response)) { switch (formField.fieldType) { diff --git a/src/types/response/guards.ts b/src/types/response/guards.ts index 1383ad45b6..22869bd638 100644 --- a/src/types/response/guards.ts +++ b/src/types/response/guards.ts @@ -1,8 +1,13 @@ import { + ProcessedAttachmentResponse, + ProcessedCheckboxResponse, ProcessedFieldResponse, ProcessedSingleAnswerResponse, + ProcessedTableResponse, } from 'src/app/modules/submission/submission.types' +import { ITableRow } from './index' + const isProcessedFieldResponse = ( response: any, ): response is ProcessedFieldResponse => { @@ -23,3 +28,52 @@ export const isProcessedSingleAnswerResponse = ( isProcessedFieldResponse(response) ) } + +export const isProcessedCheckboxResponse = ( + response: any, +): response is ProcessedCheckboxResponse => { + return ( + 'answerArray' in response && + Array.isArray(response.answerArray) && + // Check that all elements in answerArray are string (including empty string) + response.answerArray.every((elem: any) => typeof elem === 'string') && + isProcessedFieldResponse(response) + ) +} + +export const isTableRow = (row: any): row is ITableRow => { + return ( + // Check that the row contains a single array of only string (including empty string) + Array.isArray(row) && row.every((elem: any) => typeof elem === 'string') + ) +} + +export const isProcessedTableResponse = ( + response: any, +): response is ProcessedTableResponse => { + if ( + 'answerArray' in response && + Array.isArray(response.answerArray) && + // Check that all elements in answerArray are table rows + // Necessary to check answerArray[0] separately as every() returns true on empty array + response.answerArray.every((arr: any) => isTableRow(arr)) && + isTableRow(response.answerArray[0]) + ) { + // Check that all arrays in answerArray have the same length + const subArrLength: number = response.answerArray[0].length + return response.answerArray + .map((arr: ITableRow): number => arr.length) + .every((length: number): boolean => length === subArrLength) + } + return false +} + +export const isProcessedAttachmentResponse = ( + response: any, +): response is ProcessedAttachmentResponse => { + return ( + 'filename' in response && + typeof response.filename === 'string' && + isProcessedFieldResponse(response) + ) +} diff --git a/src/types/response/index.ts b/src/types/response/index.ts index b2db0acd55..8ee8c3f515 100644 --- a/src/types/response/index.ts +++ b/src/types/response/index.ts @@ -22,8 +22,10 @@ export interface ICheckboxResponse extends IBaseResponse { answerArray: string[] } +export type ITableRow = Array + export interface ITableResponse extends IBaseResponse { - answerArray: string[][] + answerArray: ITableRow[] } export type FieldResponse = diff --git a/tests/unit/backend/controllers/email-submissions.server.controller.spec.js b/tests/unit/backend/controllers/email-submissions.server.controller.spec.js index fe8503915f..cd07152376 100644 --- a/tests/unit/backend/controllers/email-submissions.server.controller.spec.js +++ b/tests/unit/backend/controllers/email-submissions.server.controller.spec.js @@ -778,7 +778,6 @@ describe('Email Submissions Controller', () => { * @param {String} question * @param {String} answer * @param {Array} [answerArray] array of answers passed in for checkbox and table - * @param {Boolean} [isExpectedToBeVisible] this boolean is used for computing expected output, and should match isVisible injected by server to pass the test */ const makeResponse = ( fieldId, @@ -786,14 +785,12 @@ describe('Email Submissions Controller', () => { question, answer, answerArray = null, - isExpectedToBeVisible = true, ) => { let response = { _id: String(fieldId), fieldType, question, answer, - isExpectedToBeVisible, } if (answerArray) response.answerArray = answerArray return response @@ -807,10 +804,9 @@ describe('Email Submissions Controller', () => { * @param {Array} field.columns * @param {Object} response * @param {Array} response.answerArray - * @param {Boolean} response.isExpectedToBeVisible */ const getExpectedForTable = (field, response) => { - const { answerArray, isExpectedToBeVisible } = response + const { answerArray } = response const { columns, title, fieldType } = field const columnTitles = columns.map(({ title }) => title) const autoReplyQuestion = `${title} (${columnTitles.join(', ')})` @@ -824,13 +820,10 @@ describe('Email Submissions Controller', () => { for (let answer of answerArray) { answer = String(answer) const answerTemplate = answer.split('\n') - if (isExpectedToBeVisible) { - // Auto replies only show visible data - expected.autoReplyData.push({ - question: autoReplyQuestion, - answerTemplate, - }) - } + expected.autoReplyData.push({ + question: autoReplyQuestion, + answerTemplate, + }) expected.jsonData.push({ question, answer, @@ -869,13 +862,10 @@ describe('Email Submissions Controller', () => { expected.replyToEmails.push(...expectedTable.replyToEmails) } else { let question = fields[i].title - if (responses[i].isExpectedToBeVisible) { - // Auto replies only show visible data - expected.autoReplyData.push({ - question, - answerTemplate, - }) - } + expected.autoReplyData.push({ + question, + answerTemplate, + }) expected.jsonData.push({ question, answer, @@ -1066,89 +1056,6 @@ describe('Email Submissions Controller', () => { prepareSubmissionThenCompare(expected, done) }) - it('excludes field if isVisible is false for autoReplyData', (done) => { - const nonVisibleField = { - _id: new ObjectID(), - title: 'not visible to autoReplyData', - fieldType: 'textfield', - } - const yesNoField = { - _id: new ObjectID(), - title: 'Show textfield if this field is yes', - fieldType: 'yes_no', - } - const nonVisibleResponse = { - _id: String(nonVisibleField._id), - question: nonVisibleField.title, - fieldType: nonVisibleField.fieldType, - isHeader: false, - answer: 'abc', - } - const yesNoResponse = { - _id: String(yesNoField._id), - question: yesNoField.title, - fieldType: yesNoField.fieldType, - isHeader: false, - answer: 'No', - } - - reqFixtures.body.responses.push(nonVisibleResponse) - reqFixtures.body.responses.push(yesNoResponse) - reqFixtures.form.form_fields.push(nonVisibleField) - reqFixtures.form.form_fields.push(yesNoField) - reqFixtures.form.form_logics.push({ - show: [nonVisibleField._id], - conditions: [ - { - ifValueType: 'single-select', - _id: '58169', - field: yesNoField._id, - state: 'is equals to', - value: 'Yes', - }, - ], - _id: '5db00a15af2ffb29487d4eb1', - logicType: 'showFields', - }) - const expectedJsonData = [ - { - question: nonVisibleField.title, - answer: nonVisibleResponse.answer, - }, - { - question: yesNoField.title, - answer: yesNoResponse.answer, - }, - ] - const expectedFormData = [ - { - question: nonVisibleField.title, - answerTemplate: [nonVisibleResponse.answer], - answer: nonVisibleResponse.answer, - fieldType: nonVisibleField.fieldType, - }, - { - question: yesNoField.title, - answerTemplate: [yesNoResponse.answer], - answer: yesNoResponse.answer, - fieldType: yesNoField.fieldType, - }, - ] - const expectedAutoReplyData = [ - { - question: yesNoField.title, - answerTemplate: [yesNoResponse.answer], - }, - ] - const expected = { - autoReplyData: expectedAutoReplyData, - jsonData: expectedJsonData, - formData: expectedFormData, - replyToEmails: [], - } - prepareSubmissionThenCompare(expected, done) - }) - it('prefixes attachment fields with [attachment]', (done) => { const validAttachmentName = 'valid.pdf' const fieldId = new ObjectID() @@ -1644,11 +1551,6 @@ describe('Email Submissions Controller', () => { prepareSubmissionThenCompare(expected, done) } - it('hides logic fields for single select value', (done) => { - // Does not fulfill condition, hence second field hidden - singleSelectSuccessTest(showFieldLogics, 'No', false, done) - }) - it('shows logic fields for single select value', (done) => { // Fulfills condition, hence second field shown singleSelectSuccessTest(showFieldLogics, 'Yes', true, done) @@ -1749,10 +1651,6 @@ describe('Email Submissions Controller', () => { ) prepareSubmissionThenCompare(expected, done) } - it('hides logic fields for number value', (done) => { - // Second field hidden - numberValueSuccessTest(showFieldLogics, '11', false, done) - }) it('shows logic for number value', (done) => { // Second field shown @@ -1857,10 +1755,6 @@ describe('Email Submissions Controller', () => { prepareSubmissionThenCompare(expected, done) } - it('hides logic fields for multi-select value', (done) => { - multiSelectSuccessTest(showFieldLogics, 'Option 3', false, done) - }) - it('shows logic for multi-select value', (done) => { multiSelectSuccessTest(showFieldLogics, 'Option 1', true, done) }) @@ -1976,16 +1870,6 @@ describe('Email Submissions Controller', () => { prepareSubmissionThenCompare(expected, done) } - it('hides logic fields if any condition is not fulfilled', (done) => { - multiAndSuccessTest( - showFieldLogics, - 'Yes', - 'Radiobutton', - false, - done, - ) - }) - it('shows logic fields if every condition is fulfilled', (done) => { multiAndSuccessTest(showFieldLogics, 'Yes', 'Textfield', true, done) }) @@ -2139,10 +2023,6 @@ describe('Email Submissions Controller', () => { prepareSubmissionThenCompare(expected, done) } - it('hides logic fields if every condition is not fulfilled', (done) => { - orSuccessTest(showFieldLogics, 'No', 'Radiobutton', false, done) - }) - it('shows logic fields if any condition is fulfilled', (done) => { orSuccessTest(showFieldLogics, 'Yes', 'Radiobutton', true, done) }) @@ -2186,39 +2066,6 @@ describe('Email Submissions Controller', () => { logicType: 'showFields', }, ] - it('hides multiple logic fields when condition is not fulfilled', (done) => { - reqFixtures.form.form_fields = fields - reqFixtures.form.form_logics = formLogics - reqFixtures.body.responses = [ - makeResponse( - conditionField._id, - conditionField.fieldType, - conditionField.title, - 'No', - ), // Does not fulfill condition - makeResponse( - logicField1._id, - logicField1.fieldType, - logicField1.title, - 'lorem', - null, - false, - ), // This field should be hidden - makeResponse( - logicField2._id, - logicField2.fieldType, - logicField2.title, - 'ipsum', - null, - false, - ), // This field should be hidden - ] - const expected = getExpectedOutput( - reqFixtures.form.form_fields, - reqFixtures.body.responses, - ) - prepareSubmissionThenCompare(expected, done) - }) it('shows multiple logic fields when condition is fulfilled', (done) => { reqFixtures.form.form_fields = fields @@ -2254,32 +2101,6 @@ describe('Email Submissions Controller', () => { prepareSubmissionThenCompare(expected, done) }) - it('should hide unfulfilled logic field even when some of the logic fields are missing', (done) => { - reqFixtures.form.form_fields = [conditionField, logicField1] // Missing logicField2 - reqFixtures.form.form_logics = formLogics - reqFixtures.body.responses = [ - makeResponse( - conditionField._id, - conditionField.fieldType, - conditionField.title, - 'No', - ), // Does not fulfill condition - makeResponse( - logicField1._id, - logicField1.fieldType, - logicField1.title, - 'lorem', - null, - false, - ), // This field should be hidden - ] - const expected = getExpectedOutput( - reqFixtures.form.form_fields, - reqFixtures.body.responses, - ) - prepareSubmissionThenCompare(expected, done) - }) - it('should show fulfilled logic field even when some of the logic fields are missing', (done) => { reqFixtures.form.form_fields = [conditionField, logicField1] // Missing logicField2 reqFixtures.form.form_logics = formLogics @@ -2456,17 +2277,6 @@ describe('Email Submissions Controller', () => { ) }) - it('hides chained logic', (done) => { - chainedSuccessTest( - showFieldLogics, - '1', - 'Others: peas', - false, - false, - done, - ) - }) - it('accepts submission not prevented by chained logic', (done) => { chainedSuccessTest( preventSubmitLogics, @@ -2575,157 +2385,6 @@ describe('Email Submissions Controller', () => { prepareSubmissionThenCompare(expected, done) }) }) - describe('circular logic', () => { - const conditionField1 = makeField( - new ObjectID(), - 'yes_no', - 'Show field 2 if yes', - ) - const conditionField2 = makeField( - new ObjectID(), - 'yes_no', - 'Show field 3 if yes', - ) - const conditionField3 = makeField( - new ObjectID(), - 'yes_no', - 'Show field 1 if yes', - ) - const visibleField = makeField( - new ObjectID(), - 'textfield', - 'Text field', - ) - const formLogics = [ - { - show: [conditionField2._id], - _id: '5df11ee1e6b6e7108a939c8a', - conditions: [ - { - ifValueType: 'single-select', - _id: '9577', - field: conditionField1._id, - state: 'is equals to', - value: 'Yes', - }, - ], - logicType: 'showFields', - }, - { - show: [conditionField3._id], - _id: '5df11ee1e6b6e7108a939c8b', - conditions: [ - { - ifValueType: 'single-select', - _id: '9577', - field: conditionField2._id, - state: 'is equals to', - value: 'Yes', - }, - ], - logicType: 'showFields', - }, - { - show: [conditionField1._id], - _id: '5df11ee1e6b6e7108a939c8c', - conditions: [ - { - ifValueType: 'single-select', - _id: '9578', - field: conditionField3._id, - state: 'is equals to', - value: 'Yes', - }, - ], - logicType: 'showFields', - }, - ] - it('correctly parses circular logic where all fields are hidden', (done) => { - reqFixtures.form.form_fields = [ - conditionField1, - conditionField2, - conditionField3, - ] - reqFixtures.form.form_logics = formLogics - reqFixtures.body.responses = [ - makeResponse( - conditionField1._id, - conditionField1.fieldType, - conditionField1.title, - 'Yes', - null, - false, - ), // Circular, never shown - makeResponse( - conditionField2._id, - conditionField2.fieldType, - conditionField2.title, - 'Yes', - null, - false, - ), // Circular, never shown - makeResponse( - conditionField3._id, - conditionField3.fieldType, - conditionField3.title, - 'Yes', - null, - false, - ), // Circular, never shown - ] - const expected = getExpectedOutput( - reqFixtures.form.form_fields, - reqFixtures.body.responses, - ) - prepareSubmissionThenCompare(expected, done) - }) - it('correctly parses circular logic for a mix of hidden and shown fields', (done) => { - reqFixtures.form.form_fields = [ - conditionField1, - conditionField2, - conditionField3, - visibleField, - ] - reqFixtures.form.form_logics = formLogics - reqFixtures.body.responses = [ - makeResponse( - conditionField1._id, - conditionField1.fieldType, - conditionField1.title, - 'Yes', - null, - false, - ), // Circular, never shown - makeResponse( - conditionField2._id, - conditionField2.fieldType, - conditionField2.title, - 'Yes', - null, - false, - ), // Circular, never shown - makeResponse( - conditionField3._id, - conditionField3.fieldType, - conditionField3.title, - 'Yes', - null, - false, - ), // Circular, never shown - makeResponse( - visibleField._id, - visibleField.fieldType, - visibleField.title, - 'lorem', - ), // Always shown - ] - const expected = getExpectedOutput( - reqFixtures.form.form_fields, - reqFixtures.body.responses, - ) - prepareSubmissionThenCompare(expected, done) - }) - }) }) }) diff --git a/tests/unit/backend/utils/field-validation/attachment-validation.spec.js b/tests/unit/backend/utils/field-validation/attachment-validation.spec.js index 0bbc601943..6a3b871f22 100644 --- a/tests/unit/backend/utils/field-validation/attachment-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/attachment-validation.spec.js @@ -108,4 +108,15 @@ describe('Attachment validation', () => { expect(validateResult._unsafeUnwrap()).toEqual(true) }) }) + + it('should disallow responses submitted for hidden fields', () => { + const formField = makeField(fieldId, '3') + const response = makeResponse(fieldId, Buffer.alloc(2000000)) + response.isVisible = false + const validateResult = validateField(formId, formField, response) + expect(validateResult.isErr()).toBe(true) + expect(validateResult._unsafeUnwrapErr()).toEqual( + new ValidateFieldError('Attempted to submit response on a hidden field'), + ) + }) }) diff --git a/tests/unit/backend/utils/field-validation/checkbox-validation.spec.js b/tests/unit/backend/utils/field-validation/checkbox-validation.spec.js index 8cbe784641..41864d7560 100644 --- a/tests/unit/backend/utils/field-validation/checkbox-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/checkbox-validation.spec.js @@ -238,4 +238,15 @@ describe('Checkbox validation', () => { ) }) }) + it('should disallow responses submitted for hidden fields', () => { + const fieldOptions = ['a', 'b', 'c'] + const formField = makeCheckboxField(fieldId, fieldOptions) + const response = makeCheckboxResponse(fieldId, ['a']) + response.isVisible = false + const validateResult = validateField(formId, formField, response) + expect(validateResult.isErr()).toBe(true) + expect(validateResult._unsafeUnwrapErr()).toEqual( + new ValidateFieldError('Attempted to submit response on a hidden field'), + ) + }) }) diff --git a/tests/unit/backend/utils/field-validation/date-validation.spec.js b/tests/unit/backend/utils/field-validation/date-validation.spec.js index cb626545f9..3cd6a9dacb 100644 --- a/tests/unit/backend/utils/field-validation/date-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/date-validation.spec.js @@ -7,6 +7,13 @@ const { } = require('../../../../../dist/backend/app/modules/submission/submission.errors') describe('Date field validation', () => { + beforeAll(() => { + jasmine.clock().install() + jasmine.clock().mockDate(new Date('2020-01-01')) + }) + afterAll(() => { + jasmine.clock().uninstall() + }) it('should allow valid date
', () => { const formField = { _id: 'abc123', @@ -287,9 +294,6 @@ describe('Date field validation', () => { }) it('should allow past dates for normal date fields', () => { - jasmine.clock().install() - jasmine.clock().mockDate(new Date('2020-01-01')) - const formField = { _id: 'abc123', fieldType: 'date', @@ -305,14 +309,9 @@ describe('Date field validation', () => { const validateResult = validateField('formId', formField, response) expect(validateResult.isOk()).toBe(true) expect(validateResult._unsafeUnwrap()).toEqual(true) - - jasmine.clock().uninstall() }) it('should allow past dates if disallow past dates is not set', () => { - jasmine.clock().install() - jasmine.clock().mockDate(new Date('2020-01-01')) - const formField = { _id: 'abc123', fieldType: 'date', @@ -328,14 +327,9 @@ describe('Date field validation', () => { const validateResult = validateField('formId', formField, response) expect(validateResult.isOk()).toBe(true) expect(validateResult._unsafeUnwrap()).toEqual(true) - - jasmine.clock().uninstall() }) it('should disallow past dates if disallow past dates is set', () => { - jasmine.clock().install() - jasmine.clock().mockDate(new Date('2020-01-01')) - const formField = { _id: 'abc123', fieldType: 'date', @@ -354,14 +348,9 @@ describe('Date field validation', () => { expect(validateResult._unsafeUnwrapErr()).toEqual( new ValidateFieldError('Invalid answer submitted'), ) - - jasmine.clock().uninstall() }) it('should allow future dates if disallow future dates is not set', () => { - jasmine.clock().install() - jasmine.clock().mockDate(new Date('2020-01-01')) - const formField = { _id: 'abc123', fieldType: 'date', @@ -377,14 +366,9 @@ describe('Date field validation', () => { const validateResult = validateField('formId', formField, response) expect(validateResult.isOk()).toBe(true) expect(validateResult._unsafeUnwrap()).toEqual(true) - - jasmine.clock().uninstall() }) it('should disallow future dates if disallow future dates is set', () => { - jasmine.clock().install() - jasmine.clock().mockDate(new Date('2020-01-01')) - const formField = { _id: 'abc123', fieldType: 'date', @@ -403,14 +387,9 @@ describe('Date field validation', () => { expect(validateResult._unsafeUnwrapErr()).toEqual( new ValidateFieldError('Invalid answer submitted'), ) - - jasmine.clock().uninstall() }) it('should allow dates inside of Custom Date Range if set', () => { - jasmine.clock().install() - jasmine.clock().mockDate(new Date('2020-01-01')) - const formField = { _id: 'abc123', fieldType: 'date', @@ -430,14 +409,9 @@ describe('Date field validation', () => { const validateResult = validateField('formId', formField, response) expect(validateResult.isOk()).toBe(true) expect(validateResult._unsafeUnwrap()).toEqual(true) - - jasmine.clock().uninstall() }) it('should disallow dates outside of Custom Date Range if set', () => { - jasmine.clock().install() - jasmine.clock().mockDate(new Date('2020-01-01')) - const formField = { _id: 'abc123', fieldType: 'date', @@ -459,7 +433,30 @@ describe('Date field validation', () => { expect(validateResult._unsafeUnwrapErr()).toEqual( new ValidateFieldError('Invalid answer submitted'), ) + }) - jasmine.clock().uninstall() + it('should disallow responses submitted for hidden fields', () => { + const formField = { + _id: 'abc123', + fieldType: 'date', + dateValidation: { + customMinDate: '2020-06-25', + customMaxDate: '2020-06-28', + }, + required: true, + } + + const response = { + _id: 'abc123', + fieldType: 'date', + isVisible: false, + answer: '22 Jun 2020', + } + + const validateResult = validateField('formId', formField, response) + expect(validateResult.isErr()).toBe(true) + expect(validateResult._unsafeUnwrapErr()).toEqual( + new ValidateFieldError('Attempted to submit response on a hidden field'), + ) }) }) diff --git a/tests/unit/backend/utils/field-validation/decimal-validation.spec.js b/tests/unit/backend/utils/field-validation/decimal-validation.spec.js index 2eddb758be..911ee0b883 100644 --- a/tests/unit/backend/utils/field-validation/decimal-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/decimal-validation.spec.js @@ -402,4 +402,27 @@ describe('Decimal Validation', () => { new ValidateFieldError('Invalid answer submitted'), ) }) + + it('should disallow responses submitted for hidden fields', () => { + const formField = { + _id: 'abc123', + fieldType: 'decimal', + required: true, + ValidationOptions: { + customMin: 0, + customMax: null, + }, + } + const response = { + _id: 'abc123', + fieldType: 'decimal', + isVisible: false, + answer: '-0.2', + } + const validateResult = validateField('formId', formField, response) + expect(validateResult.isErr()).toBe(true) + expect(validateResult._unsafeUnwrapErr()).toEqual( + new ValidateFieldError('Attempted to submit response on a hidden field'), + ) + }) }) diff --git a/tests/unit/backend/utils/field-validation/dropdown-validation.spec.js b/tests/unit/backend/utils/field-validation/dropdown-validation.spec.js index 6f9bb578bb..28a3212d57 100644 --- a/tests/unit/backend/utils/field-validation/dropdown-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/dropdown-validation.spec.js @@ -133,4 +133,22 @@ describe('Dropdown validation', () => { new ValidateFieldError('Invalid answer submitted'), ) }) + it('should disallow responses submitted for hidden fields', () => { + const formField = { + _id: 'ddID', + fieldType: 'dropdown', + fieldOptions: ['KISS', 'DRY', 'YAGNI'], + } + const response = { + _id: 'ddID', + fieldType: 'dropdown', + answer: 'KISS', + isVisible: false, + } + const validateResult = validateField('formId', formField, response) + expect(validateResult.isErr()).toBe(true) + expect(validateResult._unsafeUnwrapErr()).toEqual( + new ValidateFieldError('Attempted to submit response on a hidden field'), + ) + }) }) diff --git a/tests/unit/backend/utils/field-validation/email-validation.spec.ts b/tests/unit/backend/utils/field-validation/email-validation.spec.ts index b2e6c13d64..e44f013c7a 100644 --- a/tests/unit/backend/utils/field-validation/email-validation.spec.ts +++ b/tests/unit/backend/utils/field-validation/email-validation.spec.ts @@ -203,4 +203,30 @@ describe('Email field validation', () => { expect(validateResult.isOk()).toBe(true) expect(validateResult._unsafeUnwrap()).toEqual(true) }) + it('should disallow responses submitted for hidden fields', () => { + const formField = { + _id: 'abc123', + fieldType: BasicField.Email, + globalId: 'random', + title: 'random', + description: 'random', + required: true, + disabled: false, + isVerifiable: false, + hasAllowedEmailDomains: true, + allowedEmailDomains: ['@example.com'], + } + const response = { + _id: 'abc123', + fieldType: BasicField.Email, + question: 'random', + isVisible: false, + answer: 'volunteer-testing@test.gov.sg', + } + const validateResult = validateField('formId', formField, response) + expect(validateResult.isErr()).toBe(true) + expect(validateResult._unsafeUnwrapErr()).toEqual( + new ValidateFieldError('Attempted to submit response on a hidden field'), + ) + }) }) diff --git a/tests/unit/backend/utils/field-validation/home-num-validation.spec.js b/tests/unit/backend/utils/field-validation/home-num-validation.spec.js index 1e2e20318d..3780e31351 100644 --- a/tests/unit/backend/utils/field-validation/home-num-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/home-num-validation.spec.js @@ -34,7 +34,7 @@ describe('Home phone number validation tests', () => { const response = { _id: 'abc123', fieldType: 'homeno', - isVisible: false, + isVisible: true, answer: '', } const validateResult = validateField('formId', formField, response) @@ -71,7 +71,7 @@ describe('Home phone number validation tests', () => { const response = { _id: 'abc123', fieldType: 'homeno', - isVisible: false, + isVisible: true, answer: '+6565656565', } const validateResult = validateField('formId', formField, response) @@ -89,7 +89,7 @@ describe('Home phone number validation tests', () => { const response = { _id: 'abc123', fieldType: 'homeno', - isVisible: false, + isVisible: true, answer: '6567772918', } const validateResult = validateField('formId', formField, response) @@ -109,7 +109,7 @@ describe('Home phone number validation tests', () => { const response = { _id: 'abc123', fieldType: 'homeno', - isVisible: false, + isVisible: true, answer: '+6598765432', } const validateResult = validateField('formId', formField, response) @@ -129,7 +129,7 @@ describe('Home phone number validation tests', () => { const response = { _id: 'abc123', fieldType: 'homeno', - isVisible: false, + isVisible: true, answer: '+441285291028', } const validateResult = validateField('formId', formField, response) @@ -149,11 +149,30 @@ describe('Home phone number validation tests', () => { const response = { _id: 'abc123', fieldType: 'homeno', - isVisible: false, + isVisible: true, answer: '+441285291028', } const validateResult = validateField('formId', formField, response) expect(validateResult.isOk()).toBe(true) expect(validateResult._unsafeUnwrap()).toEqual(true) }) + it('should disallow responses submitted for hidden fields', () => { + const formField = { + _id: 'abc123', + fieldType: 'homeno', + required: true, + allowIntlNumbers: true, + } + const response = { + _id: 'abc123', + fieldType: 'homeno', + isVisible: false, + answer: '+441285291028', + } + const validateResult = validateField('formId', formField, response) + expect(validateResult.isErr()).toBe(true) + expect(validateResult._unsafeUnwrapErr()).toEqual( + new ValidateFieldError('Attempted to submit response on a hidden field'), + ) + }) }) diff --git a/tests/unit/backend/utils/field-validation/mobile-num-validation.spec.js b/tests/unit/backend/utils/field-validation/mobile-num-validation.spec.js index ad6c9f4424..30b817b590 100644 --- a/tests/unit/backend/utils/field-validation/mobile-num-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/mobile-num-validation.spec.js @@ -34,7 +34,7 @@ describe('Mobile number validation tests', () => { const response = { _id: 'abc123', fieldType: 'mobile', - isVisible: false, + isVisible: true, answer: '', } const validateResult = validateField('formId', formField, response) @@ -72,7 +72,7 @@ describe('Mobile number validation tests', () => { const response = { _id: 'abc123', fieldType: 'mobile', - isVisible: false, + isVisible: true, answer: '+6598765432', } const validateResult = validateField('formId', formField, response) @@ -90,7 +90,7 @@ describe('Mobile number validation tests', () => { const response = { _id: 'abc123', fieldType: 'mobile', - isVisible: false, + isVisible: true, answer: '6598765432', } const validateResult = validateField('formId', formField, response) @@ -110,7 +110,7 @@ describe('Mobile number validation tests', () => { const response = { _id: 'abc123', fieldType: 'mobile', - isVisible: false, + isVisible: true, answer: '+6565656565', } const validateResult = validateField('formId', formField, response) @@ -130,7 +130,7 @@ describe('Mobile number validation tests', () => { const response = { _id: 'abc123', fieldType: 'mobile', - isVisible: false, + isVisible: true, answer: '+447851315617', } const validateResult = validateField('formId', formField, response) @@ -150,11 +150,30 @@ describe('Mobile number validation tests', () => { const response = { _id: 'abc123', fieldType: 'mobile', - isVisible: false, + isVisible: true, answer: '+447851315617', } const validateResult = validateField('formId', formField, response) expect(validateResult.isOk()).toBe(true) expect(validateResult._unsafeUnwrap()).toEqual(true) }) + it('should disallow responses submitted for hidden fields', () => { + const formField = { + _id: 'abc123', + fieldType: 'mobile', + required: true, + allowIntlNumbers: true, + } + const response = { + _id: 'abc123', + fieldType: 'mobile', + isVisible: false, + answer: '+447851315617', + } + const validateResult = validateField('formId', formField, response) + expect(validateResult.isErr()).toBe(true) + expect(validateResult._unsafeUnwrapErr()).toEqual( + new ValidateFieldError('Attempted to submit response on a hidden field'), + ) + }) }) diff --git a/tests/unit/backend/utils/field-validation/nric-validation.spec.js b/tests/unit/backend/utils/field-validation/nric-validation.spec.js index 62591cc85f..ae60f87811 100644 --- a/tests/unit/backend/utils/field-validation/nric-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/nric-validation.spec.js @@ -186,4 +186,23 @@ describe('NRIC field validation', () => { new ValidateFieldError('Invalid answer submitted'), ) }) + + it('should disallow responses submitted for hidden fields', () => { + const formField = { + _id: 'abc123', + fieldType: 'nric', + required: true, + } + const response = { + _id: 'abc123', + fieldType: 'nric', + answer: 'S0000000X', + isVisible: false, + } + const validateResult = validateField('formId', formField, response) + expect(validateResult.isErr()).toBe(true) + expect(validateResult._unsafeUnwrapErr()).toEqual( + new ValidateFieldError('Attempted to submit response on a hidden field'), + ) + }) }) diff --git a/tests/unit/backend/utils/field-validation/number-validation.spec.js b/tests/unit/backend/utils/field-validation/number-validation.spec.js index 5d42e5f611..f825fcf768 100644 --- a/tests/unit/backend/utils/field-validation/number-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/number-validation.spec.js @@ -20,7 +20,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: false, + isVisible: true, answer: '5', } const validateResult = validateField('formId', formField, response) @@ -43,7 +43,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: false, + isVisible: true, answer: '55', } const validateResult = validateField('formId', formField, response) @@ -66,7 +66,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: false, + isVisible: true, answer: '555', } const validateResult = validateField('formId', formField, response) @@ -91,7 +91,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: false, + isVisible: true, answer: '555', } const validateResult = validateField('formId', formField, response) @@ -114,7 +114,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: false, + isVisible: true, answer: '55', } const validateResult = validateField('formId', formField, response) @@ -137,7 +137,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: false, + isVisible: true, answer: '55', } const validateResult = validateField('formId', formField, response) @@ -160,7 +160,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: false, + isVisible: true, answer: '5', } const validateResult = validateField('formId', formField, response) @@ -185,7 +185,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: false, + isVisible: true, answer: '55', } const validateResult = validateField('formId', formField, response) @@ -208,7 +208,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: false, + isVisible: true, answer: '55', } const validateResult = validateField('formId', formField, response) @@ -231,7 +231,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: false, + isVisible: true, answer: '55', } const validateResult = validateField('formId', formField, response) @@ -254,7 +254,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: false, + isVisible: true, answer: '55', } const validateResult = validateField('formId', formField, response) @@ -277,7 +277,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: false, + isVisible: true, answer: '', } const validateResult = validateField('formId', formField, response) @@ -300,7 +300,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: false, + isVisible: true, answer: '0', } const validateResult = validateField('formId', formField, response) @@ -323,7 +323,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: false, + isVisible: true, answer: '-5', } const validateResult = validateField('formId', formField, response) @@ -348,11 +348,35 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: false, + isVisible: true, answer: '05', } const validateResult = validateField('formId', formField, response) expect(validateResult.isOk()).toBe(true) expect(validateResult._unsafeUnwrap()).toEqual(true) }) + it('should disallow responses submitted for hidden fields', () => { + const formField = { + _id: 'abc123', + fieldType: 'number', + required: false, + ValidationOptions: { + selectedValidation: null, + customMin: null, + customMax: null, + customVal: null, + }, + } + const response = { + _id: 'abc123', + fieldType: 'number', + isVisible: false, + answer: '05', + } + const validateResult = validateField('formId', formField, response) + expect(validateResult.isErr()).toBe(true) + expect(validateResult._unsafeUnwrapErr()).toEqual( + new ValidateFieldError('Attempted to submit response on a hidden field'), + ) + }) }) diff --git a/tests/unit/backend/utils/field-validation/radio-button-validation.spec.js b/tests/unit/backend/utils/field-validation/radio-button-validation.spec.js index 863e220c4a..5696705d53 100644 --- a/tests/unit/backend/utils/field-validation/radio-button-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/radio-button-validation.spec.js @@ -192,4 +192,24 @@ describe('Radio button validation', () => { new ValidateFieldError('Invalid answer submitted'), ) }) + it('should disallow responses submitted for hidden fields', () => { + const formField = { + _id: 'radioID', + fieldType: 'radiobutton', + fieldOptions: ['a', 'b', 'c'], + othersRadioButton: true, + required: true, + } + const response = { + _id: 'radioID', + fieldType: 'radiobutton', + answer: 'Others: ', + isVisible: false, + } + const validateResult = validateField('formId', formField, response) + expect(validateResult.isErr()).toBe(true) + expect(validateResult._unsafeUnwrapErr()).toEqual( + new ValidateFieldError('Attempted to submit response on a hidden field'), + ) + }) }) diff --git a/tests/unit/backend/utils/field-validation/rating-validation.spec.js b/tests/unit/backend/utils/field-validation/rating-validation.spec.js index 2021b6b4e3..ff3a6fa1d0 100644 --- a/tests/unit/backend/utils/field-validation/rating-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/rating-validation.spec.js @@ -192,4 +192,25 @@ describe('Rating field validation', () => { new ValidateFieldError('Invalid answer submitted'), ) }) + it('should disallow responses submitted for hidden fields', () => { + const formField = { + _id: 'abc123', + fieldType: 'rating', + required: true, + ratingOptions: { + steps: 5, + }, + } + const response = { + _id: 'abc123', + fieldType: 'rating', + isVisible: false, + answer: '5', + } + const validateResult = validateField('formId', formField, response) + expect(validateResult.isErr()).toBe(true) + expect(validateResult._unsafeUnwrapErr()).toEqual( + new ValidateFieldError('Attempted to submit response on a hidden field'), + ) + }) }) diff --git a/tests/unit/backend/utils/field-validation/table-validation.spec.js b/tests/unit/backend/utils/field-validation/table-validation.spec.js index f4e43863c1..5ba853c9d6 100644 --- a/tests/unit/backend/utils/field-validation/table-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/table-validation.spec.js @@ -230,4 +230,15 @@ describe('Table validation', () => { ) }) }) + it('should disallow responses submitted for hidden fields', () => { + const columns = [makeTextFieldColumn()] + const formField = makeTableField(fieldId, columns) + const response = makeTableResponse(fieldId, Array(4).fill(['hello'])) + response.isVisible = false + const validateResult = validateField(formId, formField, response) + expect(validateResult.isErr()).toBe(true) + expect(validateResult._unsafeUnwrapErr()).toEqual( + new ValidateFieldError('Attempted to submit response on a hidden field'), + ) + }) }) diff --git a/tests/unit/backend/utils/field-validation/text-validation.spec.js b/tests/unit/backend/utils/field-validation/text-validation.spec.js index 72fb5d7920..fcba1baba4 100644 --- a/tests/unit/backend/utils/field-validation/text-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/text-validation.spec.js @@ -155,6 +155,21 @@ describe('Text validation', () => { new ValidateFieldError('Invalid answer submitted'), ) }) + it('should disallow responses submitted for hidden fields', () => { + const formField = makeShortTextField(fieldId, { + customMax: 10, + selectedValidation: 'Maximum', + }) + const response = makeShortTextResponse(fieldId, 'many more characters') + response.isVisible = false + const validateResult = validateField(formId, formField, response) + expect(validateResult.isErr()).toBe(true) + expect(validateResult._unsafeUnwrapErr()).toEqual( + new ValidateFieldError( + 'Attempted to submit response on a hidden field', + ), + ) + }) }) describe('Long text', () => { @@ -277,5 +292,20 @@ describe('Text validation', () => { new ValidateFieldError('Invalid answer submitted'), ) }) + it('should disallow responses submitted for hidden fields', () => { + const formField = makeLongTextField(fieldId, { + customMax: 10, + selectedValidation: 'Maximum', + }) + const response = makeLongTextResponse(fieldId, 'many more characters') + response.isVisible = false + const validateResult = validateField(formId, formField, response) + expect(validateResult.isErr()).toBe(true) + expect(validateResult._unsafeUnwrapErr()).toEqual( + new ValidateFieldError( + 'Attempted to submit response on a hidden field', + ), + ) + }) }) }) diff --git a/tests/unit/backend/utils/field-validation/yesno-validation.spec.js b/tests/unit/backend/utils/field-validation/yesno-validation.spec.js index 0ac26739c2..ebd6c65d25 100644 --- a/tests/unit/backend/utils/field-validation/yesno-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/yesno-validation.spec.js @@ -89,4 +89,23 @@ describe('Yes/No field validation', () => { new ValidateFieldError('Invalid answer submitted'), ) }) + + it('should disallow responses submitted for hidden fields', () => { + const response = { + _id: 'abc123', + fieldType: 'yes_no', + answer: 'somethingRandom', + isVisible: false, + } + const formField = { + _id: 'abc123', + fieldType: 'yes_no', + required: true, + } + const validateResult = validateField('formId', formField, response) + expect(validateResult.isErr()).toBe(true) + expect(validateResult._unsafeUnwrapErr()).toEqual( + new ValidateFieldError('Attempted to submit response on a hidden field'), + ) + }) }) From 50847072446bcae06406724651569ae780853f92 Mon Sep 17 00:00:00 2001 From: Antariksh Mahajan Date: Tue, 1 Dec 2020 09:46:30 +0800 Subject: [PATCH 28/34] chore: add Go and Postman tips on Share tab (#759) * chore: change copy * style: standardise top and bottom padding * chore: remove period --- .../share-form.client.view.html | 19 +++++++++++++------ .../modules/forms/admin/css/share-form.css | 8 ++++++++ src/public/translations/en-SG/main.json | 1 + 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/public/modules/forms/admin/componentViews/share-form.client.view.html b/src/public/modules/forms/admin/componentViews/share-form.client.view.html index 7613f6ccba..b9c47a1385 100644 --- a/src/public/modules/forms/admin/componentViews/share-form.client.view.html +++ b/src/public/modules/forms/admin/componentViews/share-form.client.view.html @@ -55,14 +55,21 @@ > -
+ -
+ +
', () => { const formField = { _id: 'abc123', @@ -294,6 +287,9 @@ describe('Date field validation', () => { }) it('should allow past dates for normal date fields', () => { + jasmine.clock().install() + jasmine.clock().mockDate(new Date('2020-01-01')) + const formField = { _id: 'abc123', fieldType: 'date', @@ -309,9 +305,14 @@ describe('Date field validation', () => { const validateResult = validateField('formId', formField, response) expect(validateResult.isOk()).toBe(true) expect(validateResult._unsafeUnwrap()).toEqual(true) + + jasmine.clock().uninstall() }) it('should allow past dates if disallow past dates is not set', () => { + jasmine.clock().install() + jasmine.clock().mockDate(new Date('2020-01-01')) + const formField = { _id: 'abc123', fieldType: 'date', @@ -327,9 +328,14 @@ describe('Date field validation', () => { const validateResult = validateField('formId', formField, response) expect(validateResult.isOk()).toBe(true) expect(validateResult._unsafeUnwrap()).toEqual(true) + + jasmine.clock().uninstall() }) it('should disallow past dates if disallow past dates is set', () => { + jasmine.clock().install() + jasmine.clock().mockDate(new Date('2020-01-01')) + const formField = { _id: 'abc123', fieldType: 'date', @@ -348,9 +354,14 @@ describe('Date field validation', () => { expect(validateResult._unsafeUnwrapErr()).toEqual( new ValidateFieldError('Invalid answer submitted'), ) + + jasmine.clock().uninstall() }) it('should allow future dates if disallow future dates is not set', () => { + jasmine.clock().install() + jasmine.clock().mockDate(new Date('2020-01-01')) + const formField = { _id: 'abc123', fieldType: 'date', @@ -366,9 +377,14 @@ describe('Date field validation', () => { const validateResult = validateField('formId', formField, response) expect(validateResult.isOk()).toBe(true) expect(validateResult._unsafeUnwrap()).toEqual(true) + + jasmine.clock().uninstall() }) it('should disallow future dates if disallow future dates is set', () => { + jasmine.clock().install() + jasmine.clock().mockDate(new Date('2020-01-01')) + const formField = { _id: 'abc123', fieldType: 'date', @@ -387,9 +403,14 @@ describe('Date field validation', () => { expect(validateResult._unsafeUnwrapErr()).toEqual( new ValidateFieldError('Invalid answer submitted'), ) + + jasmine.clock().uninstall() }) it('should allow dates inside of Custom Date Range if set', () => { + jasmine.clock().install() + jasmine.clock().mockDate(new Date('2020-01-01')) + const formField = { _id: 'abc123', fieldType: 'date', @@ -409,9 +430,14 @@ describe('Date field validation', () => { const validateResult = validateField('formId', formField, response) expect(validateResult.isOk()).toBe(true) expect(validateResult._unsafeUnwrap()).toEqual(true) + + jasmine.clock().uninstall() }) it('should disallow dates outside of Custom Date Range if set', () => { + jasmine.clock().install() + jasmine.clock().mockDate(new Date('2020-01-01')) + const formField = { _id: 'abc123', fieldType: 'date', @@ -433,30 +459,7 @@ describe('Date field validation', () => { expect(validateResult._unsafeUnwrapErr()).toEqual( new ValidateFieldError('Invalid answer submitted'), ) - }) - - it('should disallow responses submitted for hidden fields', () => { - const formField = { - _id: 'abc123', - fieldType: 'date', - dateValidation: { - customMinDate: '2020-06-25', - customMaxDate: '2020-06-28', - }, - required: true, - } - const response = { - _id: 'abc123', - fieldType: 'date', - isVisible: false, - answer: '22 Jun 2020', - } - - const validateResult = validateField('formId', formField, response) - expect(validateResult.isErr()).toBe(true) - expect(validateResult._unsafeUnwrapErr()).toEqual( - new ValidateFieldError('Attempted to submit response on a hidden field'), - ) + jasmine.clock().uninstall() }) }) diff --git a/tests/unit/backend/utils/field-validation/decimal-validation.spec.js b/tests/unit/backend/utils/field-validation/decimal-validation.spec.js index 911ee0b883..2eddb758be 100644 --- a/tests/unit/backend/utils/field-validation/decimal-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/decimal-validation.spec.js @@ -402,27 +402,4 @@ describe('Decimal Validation', () => { new ValidateFieldError('Invalid answer submitted'), ) }) - - it('should disallow responses submitted for hidden fields', () => { - const formField = { - _id: 'abc123', - fieldType: 'decimal', - required: true, - ValidationOptions: { - customMin: 0, - customMax: null, - }, - } - const response = { - _id: 'abc123', - fieldType: 'decimal', - isVisible: false, - answer: '-0.2', - } - const validateResult = validateField('formId', formField, response) - expect(validateResult.isErr()).toBe(true) - expect(validateResult._unsafeUnwrapErr()).toEqual( - new ValidateFieldError('Attempted to submit response on a hidden field'), - ) - }) }) diff --git a/tests/unit/backend/utils/field-validation/dropdown-validation.spec.js b/tests/unit/backend/utils/field-validation/dropdown-validation.spec.js index 28a3212d57..6f9bb578bb 100644 --- a/tests/unit/backend/utils/field-validation/dropdown-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/dropdown-validation.spec.js @@ -133,22 +133,4 @@ describe('Dropdown validation', () => { new ValidateFieldError('Invalid answer submitted'), ) }) - it('should disallow responses submitted for hidden fields', () => { - const formField = { - _id: 'ddID', - fieldType: 'dropdown', - fieldOptions: ['KISS', 'DRY', 'YAGNI'], - } - const response = { - _id: 'ddID', - fieldType: 'dropdown', - answer: 'KISS', - isVisible: false, - } - const validateResult = validateField('formId', formField, response) - expect(validateResult.isErr()).toBe(true) - expect(validateResult._unsafeUnwrapErr()).toEqual( - new ValidateFieldError('Attempted to submit response on a hidden field'), - ) - }) }) diff --git a/tests/unit/backend/utils/field-validation/email-validation.spec.ts b/tests/unit/backend/utils/field-validation/email-validation.spec.ts index e44f013c7a..b2e6c13d64 100644 --- a/tests/unit/backend/utils/field-validation/email-validation.spec.ts +++ b/tests/unit/backend/utils/field-validation/email-validation.spec.ts @@ -203,30 +203,4 @@ describe('Email field validation', () => { expect(validateResult.isOk()).toBe(true) expect(validateResult._unsafeUnwrap()).toEqual(true) }) - it('should disallow responses submitted for hidden fields', () => { - const formField = { - _id: 'abc123', - fieldType: BasicField.Email, - globalId: 'random', - title: 'random', - description: 'random', - required: true, - disabled: false, - isVerifiable: false, - hasAllowedEmailDomains: true, - allowedEmailDomains: ['@example.com'], - } - const response = { - _id: 'abc123', - fieldType: BasicField.Email, - question: 'random', - isVisible: false, - answer: 'volunteer-testing@test.gov.sg', - } - const validateResult = validateField('formId', formField, response) - expect(validateResult.isErr()).toBe(true) - expect(validateResult._unsafeUnwrapErr()).toEqual( - new ValidateFieldError('Attempted to submit response on a hidden field'), - ) - }) }) diff --git a/tests/unit/backend/utils/field-validation/home-num-validation.spec.js b/tests/unit/backend/utils/field-validation/home-num-validation.spec.js index 3780e31351..1e2e20318d 100644 --- a/tests/unit/backend/utils/field-validation/home-num-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/home-num-validation.spec.js @@ -34,7 +34,7 @@ describe('Home phone number validation tests', () => { const response = { _id: 'abc123', fieldType: 'homeno', - isVisible: true, + isVisible: false, answer: '', } const validateResult = validateField('formId', formField, response) @@ -71,7 +71,7 @@ describe('Home phone number validation tests', () => { const response = { _id: 'abc123', fieldType: 'homeno', - isVisible: true, + isVisible: false, answer: '+6565656565', } const validateResult = validateField('formId', formField, response) @@ -89,7 +89,7 @@ describe('Home phone number validation tests', () => { const response = { _id: 'abc123', fieldType: 'homeno', - isVisible: true, + isVisible: false, answer: '6567772918', } const validateResult = validateField('formId', formField, response) @@ -109,7 +109,7 @@ describe('Home phone number validation tests', () => { const response = { _id: 'abc123', fieldType: 'homeno', - isVisible: true, + isVisible: false, answer: '+6598765432', } const validateResult = validateField('formId', formField, response) @@ -129,7 +129,7 @@ describe('Home phone number validation tests', () => { const response = { _id: 'abc123', fieldType: 'homeno', - isVisible: true, + isVisible: false, answer: '+441285291028', } const validateResult = validateField('formId', formField, response) @@ -149,30 +149,11 @@ describe('Home phone number validation tests', () => { const response = { _id: 'abc123', fieldType: 'homeno', - isVisible: true, + isVisible: false, answer: '+441285291028', } const validateResult = validateField('formId', formField, response) expect(validateResult.isOk()).toBe(true) expect(validateResult._unsafeUnwrap()).toEqual(true) }) - it('should disallow responses submitted for hidden fields', () => { - const formField = { - _id: 'abc123', - fieldType: 'homeno', - required: true, - allowIntlNumbers: true, - } - const response = { - _id: 'abc123', - fieldType: 'homeno', - isVisible: false, - answer: '+441285291028', - } - const validateResult = validateField('formId', formField, response) - expect(validateResult.isErr()).toBe(true) - expect(validateResult._unsafeUnwrapErr()).toEqual( - new ValidateFieldError('Attempted to submit response on a hidden field'), - ) - }) }) diff --git a/tests/unit/backend/utils/field-validation/mobile-num-validation.spec.js b/tests/unit/backend/utils/field-validation/mobile-num-validation.spec.js index 30b817b590..ad6c9f4424 100644 --- a/tests/unit/backend/utils/field-validation/mobile-num-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/mobile-num-validation.spec.js @@ -34,7 +34,7 @@ describe('Mobile number validation tests', () => { const response = { _id: 'abc123', fieldType: 'mobile', - isVisible: true, + isVisible: false, answer: '', } const validateResult = validateField('formId', formField, response) @@ -72,7 +72,7 @@ describe('Mobile number validation tests', () => { const response = { _id: 'abc123', fieldType: 'mobile', - isVisible: true, + isVisible: false, answer: '+6598765432', } const validateResult = validateField('formId', formField, response) @@ -90,7 +90,7 @@ describe('Mobile number validation tests', () => { const response = { _id: 'abc123', fieldType: 'mobile', - isVisible: true, + isVisible: false, answer: '6598765432', } const validateResult = validateField('formId', formField, response) @@ -110,7 +110,7 @@ describe('Mobile number validation tests', () => { const response = { _id: 'abc123', fieldType: 'mobile', - isVisible: true, + isVisible: false, answer: '+6565656565', } const validateResult = validateField('formId', formField, response) @@ -130,7 +130,7 @@ describe('Mobile number validation tests', () => { const response = { _id: 'abc123', fieldType: 'mobile', - isVisible: true, + isVisible: false, answer: '+447851315617', } const validateResult = validateField('formId', formField, response) @@ -150,30 +150,11 @@ describe('Mobile number validation tests', () => { const response = { _id: 'abc123', fieldType: 'mobile', - isVisible: true, + isVisible: false, answer: '+447851315617', } const validateResult = validateField('formId', formField, response) expect(validateResult.isOk()).toBe(true) expect(validateResult._unsafeUnwrap()).toEqual(true) }) - it('should disallow responses submitted for hidden fields', () => { - const formField = { - _id: 'abc123', - fieldType: 'mobile', - required: true, - allowIntlNumbers: true, - } - const response = { - _id: 'abc123', - fieldType: 'mobile', - isVisible: false, - answer: '+447851315617', - } - const validateResult = validateField('formId', formField, response) - expect(validateResult.isErr()).toBe(true) - expect(validateResult._unsafeUnwrapErr()).toEqual( - new ValidateFieldError('Attempted to submit response on a hidden field'), - ) - }) }) diff --git a/tests/unit/backend/utils/field-validation/nric-validation.spec.js b/tests/unit/backend/utils/field-validation/nric-validation.spec.js index ae60f87811..62591cc85f 100644 --- a/tests/unit/backend/utils/field-validation/nric-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/nric-validation.spec.js @@ -186,23 +186,4 @@ describe('NRIC field validation', () => { new ValidateFieldError('Invalid answer submitted'), ) }) - - it('should disallow responses submitted for hidden fields', () => { - const formField = { - _id: 'abc123', - fieldType: 'nric', - required: true, - } - const response = { - _id: 'abc123', - fieldType: 'nric', - answer: 'S0000000X', - isVisible: false, - } - const validateResult = validateField('formId', formField, response) - expect(validateResult.isErr()).toBe(true) - expect(validateResult._unsafeUnwrapErr()).toEqual( - new ValidateFieldError('Attempted to submit response on a hidden field'), - ) - }) }) diff --git a/tests/unit/backend/utils/field-validation/number-validation.spec.js b/tests/unit/backend/utils/field-validation/number-validation.spec.js index f825fcf768..5d42e5f611 100644 --- a/tests/unit/backend/utils/field-validation/number-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/number-validation.spec.js @@ -20,7 +20,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: true, + isVisible: false, answer: '5', } const validateResult = validateField('formId', formField, response) @@ -43,7 +43,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: true, + isVisible: false, answer: '55', } const validateResult = validateField('formId', formField, response) @@ -66,7 +66,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: true, + isVisible: false, answer: '555', } const validateResult = validateField('formId', formField, response) @@ -91,7 +91,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: true, + isVisible: false, answer: '555', } const validateResult = validateField('formId', formField, response) @@ -114,7 +114,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: true, + isVisible: false, answer: '55', } const validateResult = validateField('formId', formField, response) @@ -137,7 +137,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: true, + isVisible: false, answer: '55', } const validateResult = validateField('formId', formField, response) @@ -160,7 +160,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: true, + isVisible: false, answer: '5', } const validateResult = validateField('formId', formField, response) @@ -185,7 +185,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: true, + isVisible: false, answer: '55', } const validateResult = validateField('formId', formField, response) @@ -208,7 +208,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: true, + isVisible: false, answer: '55', } const validateResult = validateField('formId', formField, response) @@ -231,7 +231,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: true, + isVisible: false, answer: '55', } const validateResult = validateField('formId', formField, response) @@ -254,7 +254,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: true, + isVisible: false, answer: '55', } const validateResult = validateField('formId', formField, response) @@ -277,7 +277,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: true, + isVisible: false, answer: '', } const validateResult = validateField('formId', formField, response) @@ -300,7 +300,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: true, + isVisible: false, answer: '0', } const validateResult = validateField('formId', formField, response) @@ -323,7 +323,7 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: true, + isVisible: false, answer: '-5', } const validateResult = validateField('formId', formField, response) @@ -348,35 +348,11 @@ describe('Number field validation', () => { const response = { _id: 'abc123', fieldType: 'number', - isVisible: true, + isVisible: false, answer: '05', } const validateResult = validateField('formId', formField, response) expect(validateResult.isOk()).toBe(true) expect(validateResult._unsafeUnwrap()).toEqual(true) }) - it('should disallow responses submitted for hidden fields', () => { - const formField = { - _id: 'abc123', - fieldType: 'number', - required: false, - ValidationOptions: { - selectedValidation: null, - customMin: null, - customMax: null, - customVal: null, - }, - } - const response = { - _id: 'abc123', - fieldType: 'number', - isVisible: false, - answer: '05', - } - const validateResult = validateField('formId', formField, response) - expect(validateResult.isErr()).toBe(true) - expect(validateResult._unsafeUnwrapErr()).toEqual( - new ValidateFieldError('Attempted to submit response on a hidden field'), - ) - }) }) diff --git a/tests/unit/backend/utils/field-validation/radio-button-validation.spec.js b/tests/unit/backend/utils/field-validation/radio-button-validation.spec.js index 5696705d53..863e220c4a 100644 --- a/tests/unit/backend/utils/field-validation/radio-button-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/radio-button-validation.spec.js @@ -192,24 +192,4 @@ describe('Radio button validation', () => { new ValidateFieldError('Invalid answer submitted'), ) }) - it('should disallow responses submitted for hidden fields', () => { - const formField = { - _id: 'radioID', - fieldType: 'radiobutton', - fieldOptions: ['a', 'b', 'c'], - othersRadioButton: true, - required: true, - } - const response = { - _id: 'radioID', - fieldType: 'radiobutton', - answer: 'Others: ', - isVisible: false, - } - const validateResult = validateField('formId', formField, response) - expect(validateResult.isErr()).toBe(true) - expect(validateResult._unsafeUnwrapErr()).toEqual( - new ValidateFieldError('Attempted to submit response on a hidden field'), - ) - }) }) diff --git a/tests/unit/backend/utils/field-validation/rating-validation.spec.js b/tests/unit/backend/utils/field-validation/rating-validation.spec.js index ff3a6fa1d0..2021b6b4e3 100644 --- a/tests/unit/backend/utils/field-validation/rating-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/rating-validation.spec.js @@ -192,25 +192,4 @@ describe('Rating field validation', () => { new ValidateFieldError('Invalid answer submitted'), ) }) - it('should disallow responses submitted for hidden fields', () => { - const formField = { - _id: 'abc123', - fieldType: 'rating', - required: true, - ratingOptions: { - steps: 5, - }, - } - const response = { - _id: 'abc123', - fieldType: 'rating', - isVisible: false, - answer: '5', - } - const validateResult = validateField('formId', formField, response) - expect(validateResult.isErr()).toBe(true) - expect(validateResult._unsafeUnwrapErr()).toEqual( - new ValidateFieldError('Attempted to submit response on a hidden field'), - ) - }) }) diff --git a/tests/unit/backend/utils/field-validation/table-validation.spec.js b/tests/unit/backend/utils/field-validation/table-validation.spec.js index 5ba853c9d6..f4e43863c1 100644 --- a/tests/unit/backend/utils/field-validation/table-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/table-validation.spec.js @@ -230,15 +230,4 @@ describe('Table validation', () => { ) }) }) - it('should disallow responses submitted for hidden fields', () => { - const columns = [makeTextFieldColumn()] - const formField = makeTableField(fieldId, columns) - const response = makeTableResponse(fieldId, Array(4).fill(['hello'])) - response.isVisible = false - const validateResult = validateField(formId, formField, response) - expect(validateResult.isErr()).toBe(true) - expect(validateResult._unsafeUnwrapErr()).toEqual( - new ValidateFieldError('Attempted to submit response on a hidden field'), - ) - }) }) diff --git a/tests/unit/backend/utils/field-validation/text-validation.spec.js b/tests/unit/backend/utils/field-validation/text-validation.spec.js index fcba1baba4..72fb5d7920 100644 --- a/tests/unit/backend/utils/field-validation/text-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/text-validation.spec.js @@ -155,21 +155,6 @@ describe('Text validation', () => { new ValidateFieldError('Invalid answer submitted'), ) }) - it('should disallow responses submitted for hidden fields', () => { - const formField = makeShortTextField(fieldId, { - customMax: 10, - selectedValidation: 'Maximum', - }) - const response = makeShortTextResponse(fieldId, 'many more characters') - response.isVisible = false - const validateResult = validateField(formId, formField, response) - expect(validateResult.isErr()).toBe(true) - expect(validateResult._unsafeUnwrapErr()).toEqual( - new ValidateFieldError( - 'Attempted to submit response on a hidden field', - ), - ) - }) }) describe('Long text', () => { @@ -292,20 +277,5 @@ describe('Text validation', () => { new ValidateFieldError('Invalid answer submitted'), ) }) - it('should disallow responses submitted for hidden fields', () => { - const formField = makeLongTextField(fieldId, { - customMax: 10, - selectedValidation: 'Maximum', - }) - const response = makeLongTextResponse(fieldId, 'many more characters') - response.isVisible = false - const validateResult = validateField(formId, formField, response) - expect(validateResult.isErr()).toBe(true) - expect(validateResult._unsafeUnwrapErr()).toEqual( - new ValidateFieldError( - 'Attempted to submit response on a hidden field', - ), - ) - }) }) }) diff --git a/tests/unit/backend/utils/field-validation/yesno-validation.spec.js b/tests/unit/backend/utils/field-validation/yesno-validation.spec.js index ebd6c65d25..0ac26739c2 100644 --- a/tests/unit/backend/utils/field-validation/yesno-validation.spec.js +++ b/tests/unit/backend/utils/field-validation/yesno-validation.spec.js @@ -89,23 +89,4 @@ describe('Yes/No field validation', () => { new ValidateFieldError('Invalid answer submitted'), ) }) - - it('should disallow responses submitted for hidden fields', () => { - const response = { - _id: 'abc123', - fieldType: 'yes_no', - answer: 'somethingRandom', - isVisible: false, - } - const formField = { - _id: 'abc123', - fieldType: 'yes_no', - required: true, - } - const validateResult = validateField('formId', formField, response) - expect(validateResult.isErr()).toBe(true) - expect(validateResult._unsafeUnwrapErr()).toEqual( - new ValidateFieldError('Attempted to submit response on a hidden field'), - ) - }) }) From 7e6267d6b291b2f43901ebf9266276823bfda580 Mon Sep 17 00:00:00 2001 From: Kar Rui Lau Date: Tue, 1 Dec 2020 16:30:56 +0800 Subject: [PATCH 32/34] chore: bump version to v4.48.1 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a83c43002c..c6f14ffb47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,14 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [v4.48.1](https://github.com/opengovsg/FormSG/compare/v4.48.0...v4.48.1) + +- Revert "fix: backend validation does not prevent responses on hidden fields (#736)" [`fead8ce`](https://github.com/opengovsg/FormSG/commit/fead8ce6011003660d0269ceeffba8aaae09ab0a) + #### [v4.48.0](https://github.com/opengovsg/FormSG/compare/v4.47.0...v4.48.0) +> 1 December 2020 + - chore: add Go and Postman tips on Share tab [`#759`](https://github.com/opengovsg/FormSG/pull/759) - fix: backend validation does not prevent responses on hidden fields [`#736`](https://github.com/opengovsg/FormSG/pull/736) - fix(deps): bump mongoose from 5.10.15 to 5.10.18 [`#758`](https://github.com/opengovsg/FormSG/pull/758) @@ -35,6 +41,8 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - chore(deps-dev): bump lint-staged from 10.5.1 to 10.5.2 [`#722`](https://github.com/opengovsg/FormSG/pull/722) - docs: add script for unlisting array of forms [`#714`](https://github.com/opengovsg/FormSG/pull/714) - build: merge release 4.47.0 into develop [`#719`](https://github.com/opengovsg/FormSG/pull/719) +- chore: bump version to v4.48.0 [`24866f6`](https://github.com/opengovsg/FormSG/commit/24866f66ae6c95f56ab59a7f5e0cc7db6ba1121a) +- fix: skip travis artifact cleanup [`3bb2d2b`](https://github.com/opengovsg/FormSG/commit/3bb2d2b2875d44dbbef93ecde30597584969ef3e) #### [v4.47.0](https://github.com/opengovsg/FormSG/compare/v4.46.1...v4.47.0) diff --git a/package-lock.json b/package-lock.json index c09b54ca71..192a708bad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "FormSG", - "version": "4.48.0", + "version": "4.48.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 42bd4f9e80..9135d6bd1f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "FormSG", "description": "Form Manager for Government", - "version": "4.48.0", + "version": "4.48.1", "homepage": "https://form.gov.sg", "authors": [ "FormSG " From da7bc45e4c11b9c49a0ce87aab9ec6970e20f6f3 Mon Sep 17 00:00:00 2001 From: tshuli <63710093+tshuli@users.noreply.github.com> Date: Wed, 9 Dec 2020 17:15:54 +0800 Subject: [PATCH 33/34] chore: log errors from concatResponse (#817) --- .../email-submissions.server.controller.js | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/app/controllers/email-submissions.server.controller.js b/src/app/controllers/email-submissions.server.controller.js index ca8ca7262f..1ccc2b6594 100644 --- a/src/app/controllers/email-submissions.server.controller.js +++ b/src/app/controllers/email-submissions.server.controller.js @@ -582,7 +582,25 @@ exports.saveMetadataToDb = function (req, res, next) { }) // Create submission hash - let concatenatedResponse = concatResponse(formData, attachments) + let concatenatedResponse + + try { + concatenatedResponse = concatResponse(formData, attachments) + } catch (err) { + logger.error({ + message: 'Error concatenating response for submission hash', + meta: { + action: 'concatResponse', + ...createReqMeta(req), + formId: req.form._id, + }, + error: err, + }) + return res.status(StatusCodes.BAD_REQUEST).json({ + message: + 'There is something wrong with your form submission. Please check your responses and try again. If the problem persists, please refresh the page.', + }) + } createHash(concatenatedResponse) .then((result) => { From 5cd70788e6250239a838ba64b7168ff347e26aae Mon Sep 17 00:00:00 2001 From: Antariksh Date: Wed, 9 Dec 2020 17:32:13 +0800 Subject: [PATCH 34/34] chore: bump version to 4.48.2 --- CHANGELOG.md | 32 +++++++++++++++++++++++--------- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6f14ffb47..e8f22bf201 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,17 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [v4.48.2](https://github.com/opengovsg/FormSG/compare/v4.48.1...v4.48.2) + +- chore: log errors from concatResponse [`#817`](https://github.com/opengovsg/FormSG/pull/817) + #### [v4.48.1](https://github.com/opengovsg/FormSG/compare/v4.48.0...v4.48.1) +> 1 December 2020 + - Revert "fix: backend validation does not prevent responses on hidden fields (#736)" [`fead8ce`](https://github.com/opengovsg/FormSG/commit/fead8ce6011003660d0269ceeffba8aaae09ab0a) +- chore: bump version to v4.48.1 [`7e6267d`](https://github.com/opengovsg/FormSG/commit/7e6267d6b291b2f43901ebf9266276823bfda580) +- fix: skip travis artifact cleanup [`3bb2d2b`](https://github.com/opengovsg/FormSG/commit/3bb2d2b2875d44dbbef93ecde30597584969ef3e) #### [v4.48.0](https://github.com/opengovsg/FormSG/compare/v4.47.0...v4.48.0) @@ -41,14 +49,13 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - chore(deps-dev): bump lint-staged from 10.5.1 to 10.5.2 [`#722`](https://github.com/opengovsg/FormSG/pull/722) - docs: add script for unlisting array of forms [`#714`](https://github.com/opengovsg/FormSG/pull/714) - build: merge release 4.47.0 into develop [`#719`](https://github.com/opengovsg/FormSG/pull/719) +- fix: convert field ID to string for admin preview MyInfo [`#717`](https://github.com/opengovsg/FormSG/pull/717) - chore: bump version to v4.48.0 [`24866f6`](https://github.com/opengovsg/FormSG/commit/24866f66ae6c95f56ab59a7f5e0cc7db6ba1121a) -- fix: skip travis artifact cleanup [`3bb2d2b`](https://github.com/opengovsg/FormSG/commit/3bb2d2b2875d44dbbef93ecde30597584969ef3e) #### [v4.47.0](https://github.com/opengovsg/FormSG/compare/v4.46.1...v4.47.0) -> 1 December 2020 +> 24 November 2020 -- fix: convert field ID to string for admin preview MyInfo [`#717`](https://github.com/opengovsg/FormSG/pull/717) - feat: Increase attachment size options [`#692`](https://github.com/opengovsg/FormSG/pull/692) - ref: migrate feedback/count endpoint handler flow to TypeScript [`#706`](https://github.com/opengovsg/FormSG/pull/706) - chore(deps-dev): bump sinon from 9.2.0 to 9.2.1 [`#710`](https://github.com/opengovsg/FormSG/pull/710) @@ -87,18 +94,20 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - chore(deps-dev): bump lint-staged from 10.4.0 to 10.5.1 [`#657`](https://github.com/opengovsg/FormSG/pull/657) - build: add lockfile-lint to CI [`#651`](https://github.com/opengovsg/FormSG/pull/651) - chore: bump version to 4.47.0 [`f334474`](https://github.com/opengovsg/FormSG/commit/f334474fd5c251b683f0c78fcb5af643d7cc30cd) +- chore: bump version to v4.46.1 [`375df4f`](https://github.com/opengovsg/FormSG/commit/375df4f23a4a9253a50df6bdb1905cfcb40d83f2) +- fix: early return on undefined verification signature [`e67bb8b`](https://github.com/opengovsg/FormSG/commit/e67bb8b1fc12e41c7c9cf1347a52ed7e30f2fbf5) #### [v4.46.1](https://github.com/opengovsg/FormSG/compare/v4.46.0...v4.46.1) > 19 November 2020 +- chore: bump version to v4.46.0 [`bcdcf03`](https://github.com/opengovsg/FormSG/commit/bcdcf03afdbffb3562386129c793dee5b459b321) - fix: check for undefined-ness on attachmentMetadata [`46d3357`](https://github.com/opengovsg/FormSG/commit/46d335785f51f40722ec0ba87510a17e7cf82edd) -- chore: bump version to v4.46.1 [`375df4f`](https://github.com/opengovsg/FormSG/commit/375df4f23a4a9253a50df6bdb1905cfcb40d83f2) -- fix: early return on undefined verification signature [`e67bb8b`](https://github.com/opengovsg/FormSG/commit/e67bb8b1fc12e41c7c9cf1347a52ed7e30f2fbf5) +- chore: bump version to v4.46.1 [`37db034`](https://github.com/opengovsg/FormSG/commit/37db034ebafd7388a36762c3ba4fde405bca27de) -#### [v4.46.0](https://github.com/opengovsg/FormSG/compare/v4.45.0...v4.46.0) +#### [v4.46.0](https://github.com/opengovsg/FormSG/compare/v4.45.1...v4.46.0) -> 18 November 2020 +> 17 November 2020 - chore: use travis_retry to retry flaky tests automatically [`#641`](https://github.com/opengovsg/FormSG/pull/641) - refactor: migrate MyInfo functionality to TypeScript [`#560`](https://github.com/opengovsg/FormSG/pull/560) @@ -138,9 +147,14 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - chore(deps-dev): bump @types/node from 14.14.6 to 14.14.7 [`#611`](https://github.com/opengovsg/FormSG/pull/611) - build: merge release 4.45.0 into develop [`#608`](https://github.com/opengovsg/FormSG/pull/608) - ref: migrate inline queries for retrieval of submissions metadata to model static methods [`#601`](https://github.com/opengovsg/FormSG/pull/601) -- chore: bump version to v4.46.0 [`bcdcf03`](https://github.com/opengovsg/FormSG/commit/bcdcf03afdbffb3562386129c793dee5b459b321) +- chore: bump version to v4.46.9 [`31d6b9b`](https://github.com/opengovsg/FormSG/commit/31d6b9b00ab1cbc5e8a59373fb7a65e52b795f69) + +#### [v4.45.1](https://github.com/opengovsg/FormSG/compare/v4.45.0...v4.45.1) + +> 16 November 2020 + - chore: bump version to 4.45.1 [`d2ec536`](https://github.com/opengovsg/FormSG/commit/d2ec536e5154c9459b97d6977724928dc68e2a19) -- fix: add minimal polyfill for ie11 in decryption worker [`a9d2068`](https://github.com/opengovsg/FormSG/commit/a9d2068d70c61c09910599a37bb8d5e14ead2ffc) +- fix: update email list correctly when deleting emails [`5776c2d`](https://github.com/opengovsg/FormSG/commit/5776c2d3d079e2876aacb17c41c398031a8ff939) #### [v4.45.0](https://github.com/opengovsg/FormSG/compare/v4.44.0...v4.45.0) diff --git a/package-lock.json b/package-lock.json index 192a708bad..f6c55111fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "FormSG", - "version": "4.48.1", + "version": "4.48.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9135d6bd1f..168752e1ca 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "FormSG", "description": "Form Manager for Government", - "version": "4.48.1", + "version": "4.48.2", "homepage": "https://form.gov.sg", "authors": [ "FormSG "