diff --git a/package-lock.json b/package-lock.json index b99f70a088..57ee2bd947 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4619,14 +4619,6 @@ "@types/node": "*" } }, - "@types/bson": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.4.tgz", - "integrity": "sha512-awqorHvQS0DqxkHQ/FxcPX9E+H7Du51Qw/2F+5TBMSaE3G0hm+8D3eXJ6MAzFw75nE8V7xF0QvzUSdxIjJb/GA==", - "requires": { - "@types/node": "*" - } - }, "@types/busboy": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-0.3.1.tgz", @@ -5026,15 +5018,6 @@ "integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==", "dev": true }, - "@types/mongodb": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.20.tgz", - "integrity": "sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==", - "requires": { - "@types/bson": "*", - "@types/node": "*" - } - }, "@types/mongodb-uri": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/@types/mongodb-uri/-/mongodb-uri-0.9.1.tgz", @@ -5195,9 +5178,9 @@ } }, "@types/tmp": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.0.tgz", - "integrity": "sha512-flgpHJjntpBAdJD43ShRosQvNC0ME97DCfGvZEDlAThQmnerRXrLbX6YgzRBQCZTthET9eAWFAMaYP0m0Y4HzQ==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.2.tgz", + "integrity": "sha512-MhSa0yylXtVMsyT8qFpHA1DLHj4DvQGH5ntxrhHSh8PxUVNi35Wk+P5hVgqbO2qZqOotqr9jaoPRL+iRjWYm/A==", "dev": true }, "@types/triple-beam": { @@ -5230,6 +5213,20 @@ "integrity": "sha512-+jBxVvXVuggZOrm04NR8z+5+bgoW4VZyLzUO+hmPPW1mVFL/HaitLAkizfv4yg9TbG8lkfHWVMQ11yDqrVVCzA==", "dev": true }, + "@types/webidl-conversions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz", + "integrity": "sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q==" + }, + "@types/whatwg-url": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.1.tgz", + "integrity": "sha512-2YubE1sjj5ifxievI5Ge1sckb9k/Er66HyR2c+3+I6VDUUg1TLPdYYTEbQ+DjRkS4nTxMJhgWfSfMRD2sl2EYQ==", + "requires": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, "@types/yargs": { "version": "15.0.5", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz", @@ -6208,6 +6205,23 @@ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", "dev": true }, + "async-mutex": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz", + "integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==", + "dev": true, + "requires": { + "tslib": "^2.3.1" + }, + "dependencies": { + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "dev": true + } + } + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -6823,12 +6837,27 @@ } }, "bl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", - "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "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" + } + } } }, "blob": { @@ -7123,19 +7152,29 @@ } }, "bson": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/bson/-/bson-2.0.8.tgz", - "integrity": "sha512-0F0T3gHeOwJzHWcN60BZomqj5hCBDRk4b3fANuruvDTnyJJ8sggABKSaePM2F34THNZZSIlB2P1mk2nQWgBr9w==" + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.6.0.tgz", + "integrity": "sha512-8jw1NU1hglS+Da1jDOUYuNcBJ4cNHCFIqzlwoFNnsTOg2R/ox0aTYcTiBN4dzRa9q7Cvy6XErh3L8ReTEb9AQQ==", + "requires": { + "buffer": "^5.6.0" + } }, "bson-ext": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/bson-ext/-/bson-ext-2.0.6.tgz", - "integrity": "sha512-UFQFJDC0Hf2NLncLoO+ht/8tm63C5xlvFm55ur4zCpIJqZqZTdbY50uGy2pSraTEbLbCrqLGV/m4gSNlc8tQFw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bson-ext/-/bson-ext-4.0.2.tgz", + "integrity": "sha512-Nrf6KyyBtTLVrns1++ODGkRgR4UDU2LkkVb7UZvr1Q7YxNd23omlyDdh1yva2Nm4ehGJ9hceIJzFfIZaf2UoCQ==", "requires": { - "bindings": "^1.3.0", - "bson": "^2.0.2", - "nan": "^2.14.0", - "prebuild-install": "6.1.2" + "bindings": "^1.5.0", + "bson": "^4.5.2", + "nan": "^2.14.2", + "prebuild-install": "^6.1.2" + }, + "dependencies": { + "nan": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" + } } }, "buffer": { @@ -8194,40 +8233,21 @@ } }, "connect-mongo": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-4.4.1.tgz", - "integrity": "sha512-I1QUE2tSGPtIBDAL2sFqUEPspDeJOR0u4g+N41ARJZk958pncu2PBG48Ev++fnldljobpIfdafak7hSlPYarvA==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-4.6.0.tgz", + "integrity": "sha512-8new4Z7NLP3CGP65Aw6ls3xDBeKVvHRSh39CXuDZTQsvpeeU9oNMzfFgvqmHqZ6gWpxIl663RyoVEmCAGf1yOg==", "requires": { "debug": "^4.3.1", - "kruptein": "^3.0.0", - "mongodb": "3.6.5" + "kruptein": "^3.0.0" }, "dependencies": { - "bson": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz", - "integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==" - }, "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" } - }, - "mongodb": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.5.tgz", - "integrity": "sha512-mQlYKw1iGbvJJejcPuyTaytq0xxlYbIoVDm2FODR+OHxyEiMR021vc32bTvamgBjCswsD54XIRwhg3yBaWqJjg==", - "requires": { - "bl": "^2.2.1", - "bson": "^1.1.4", - "denque": "^1.4.1", - "require_optional": "^1.0.1", - "safe-buffer": "^5.1.2", - "saslprep": "^1.0.0" - } } } }, @@ -9028,6 +9048,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, "requires": { "ms": "2.0.0" }, @@ -9035,7 +9056,8 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true } } }, @@ -9276,9 +9298,9 @@ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "denque": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", - "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz", + "integrity": "sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==" }, "depd": { "version": "1.1.2", @@ -11366,12 +11388,6 @@ "pkg-dir": "^3.0.0" } }, - "find-package-json": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/find-package-json/-/find-package-json-1.2.0.tgz", - "integrity": "sha512-+SOGcLGYDJHtyqHd87ysBhmaeQ95oWspDKnMXBrnQ9Eq4OkLNqejgoaD8xVWu6GPa0B6roa6KinCMEMcVeqONw==", - "dev": true - }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -15267,9 +15283,9 @@ "dev": true }, "kruptein": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.0.tgz", - "integrity": "sha512-Fh5sIb+3XI9L12GsgeBQqXVRPLB1HVViKSUkqPPOcqTEX4NwoF8Z3pEfMSl3Psd1j+QlloV8Uxxwp4gk3aFBGA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.3.tgz", + "integrity": "sha512-v5mqSHKS2M1xWUo5V7Q6TMcj1vjTgKWvfspizn6Z939Cmv8NNn5E+Z4LeGBEKDL3yT4pMXaRTjh98oksGTDntA==", "requires": { "asn1.js": "^5.4.1" } @@ -15680,15 +15696,6 @@ "path-exists": "^3.0.0" } }, - "lockfile": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", - "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", - "dev": true, - "requires": { - "signal-exit": "^3.0.2" - } - }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -16522,12 +16529,6 @@ "integrity": "sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==", "dev": true }, - "mockingoose": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/mockingoose/-/mockingoose-2.13.2.tgz", - "integrity": "sha512-KP+rweeXjP5P1SMoBQTS7NQG4xIZLbrlJ24dKDjuRRX3kzn+pCAg4w1e+Er6UQEoBtqjbXDM9PTbQkXUk4S7iw==", - "dev": true - }, "module-not-found-error": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", @@ -16548,51 +16549,70 @@ } }, "mongodb": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.3.tgz", - "integrity": "sha512-rOZuR0QkodZiM+UbQE5kDsJykBqWi0CL4Ec2i1nrGrUI3KO11r6Fbxskqmq3JK2NH7aW4dcccBuUujAP0ERl5w==", - "dev": true, - "optional": true, + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.2.1.tgz", + "integrity": "sha512-nDC+ulM/Ea3Q2VG5eemuGfB7T4ORwrtKegH2XW9OLlUBgQF6OTNrzFCS1Z3SJGVA+T0Sr1xBYV6DMnp0A7us0g==", "requires": { - "bl": "^2.2.1", - "bson": "^1.1.4", - "denque": "^1.4.1", - "require_optional": "^1.0.1", - "safe-buffer": "^5.1.2", - "saslprep": "^1.0.0" + "bson": "^4.6.0", + "denque": "^2.0.1", + "mongodb-connection-string-url": "^2.2.0", + "saslprep": "^1.0.3" + } + }, + "mongodb-connection-string-url": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.2.0.tgz", + "integrity": "sha512-U0cDxLUrQrl7DZA828CA+o69EuWPWEJTwdMPozyd7cy/dbtncUZczMw7wRHcwMD7oKOn0NM2tF9jdf5FFVW9CA==", + "requires": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" }, "dependencies": { - "bson": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz", - "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==", - "dev": true, - "optional": true + "tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "requires": { + "punycode": "^2.1.1" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" + }, + "whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "requires": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + } } } }, "mongodb-memory-server-core": { - "version": "6.9.6", - "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-6.9.6.tgz", - "integrity": "sha512-ZcXHTI2TccH3L5N9JyAMGm8bbAsfLn8SUWOeYGHx/vDx7vu4qshyaNXTIxeHjpUQA29N+Z1LtTXA6vXjl1eg6w==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-8.0.4.tgz", + "integrity": "sha512-glIeXOmPWVbmbwMZ8hp2UP87w9AFU1VA+YZo77xIS13YYYsAJj+6vZ29Xkud4bebHjBuktcjr0cqf8yHNIEvVg==", "dev": true, "requires": { - "@types/tmp": "^0.2.0", - "camelcase": "^6.0.0", - "cross-spawn": "^7.0.3", + "@types/tmp": "^0.2.2", + "async-mutex": "^0.3.2", + "camelcase": "^6.1.0", "debug": "^4.2.0", - "find-cache-dir": "^3.3.1", - "find-package-json": "^1.2.0", + "find-cache-dir": "^3.3.2", "get-port": "^5.1.1", "https-proxy-agent": "^5.0.0", - "lockfile": "^1.0.4", "md5-file": "^5.0.0", - "mkdirp": "^1.0.4", - "mongodb": "^3.6.2", - "semver": "^7.3.2", + "mongodb": "^4.1.3", + "new-find-package-json": "^1.1.0", + "semver": "^7.3.5", "tar-stream": "^2.1.4", "tmp": "^0.2.1", - "uuid": "^8.3.0", + "tslib": "^2.3.1", + "uuid": "^8.3.1", "yauzl": "^2.10.0" }, "dependencies": { @@ -16605,47 +16625,25 @@ "debug": "4" } }, - "bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, "camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", + "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", "dev": true }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dev": true, "requires": { "ms": "2.1.2" } }, "find-cache-dir": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", - "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "dev": true, "requires": { "commondir": "^1.0.1", @@ -16682,15 +16680,6 @@ "p-locate": "^4.1.0" } }, - "lru-cache": { - "version": "6.0.0", - "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" - } - }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -16708,12 +16697,6 @@ } } }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -16744,12 +16727,6 @@ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -16771,29 +16748,14 @@ } }, "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" } }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, "tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -16807,28 +16769,10 @@ "readable-stream": "^3.1.1" } }, - "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "requires": { - "rimraf": "^3.0.0" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", "dev": true } } @@ -16839,50 +16783,34 @@ "integrity": "sha1-D3ca0W9IOuZfQoeWlCjp+8SqYYE=" }, "mongoose": { - "version": "5.13.5", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.13.5.tgz", - "integrity": "sha512-sSUAk9GWgA8r3w3nVNrNjBaDem86aevwXO8ltDMKzCf+rjnteMMQkXHQdn1ePkt7alROEPZYCAjiRjptWRSPiQ==", + "version": "6.0.14", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.0.14.tgz", + "integrity": "sha512-SZ0kBlHrz/G70yWdVXLfM/gH4NsY85+as4MZRdtWxBTDEcmoE3rCFAz1/Ho2ycg5mJAeOBwdGZw4a5sn/WrwUA==", "requires": { - "@types/mongodb": "^3.5.27", - "bson": "^1.1.4", + "bson": "^4.2.2", "kareem": "2.3.2", - "mongodb": "3.6.10", - "mongoose-legacy-pluralize": "1.0.2", - "mpath": "0.8.3", - "mquery": "3.2.5", + "mongodb": "4.1.4", + "mpath": "0.8.4", + "mquery": "4.0.0", "ms": "2.1.2", - "optional-require": "1.0.x", "regexp-clone": "1.0.0", - "safe-buffer": "5.2.1", "sift": "13.5.2", "sliced": "1.0.1" }, "dependencies": { - "bson": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz", - "integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==" - }, "mongodb": { - "version": "3.6.10", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.10.tgz", - "integrity": "sha512-fvIBQBF7KwCJnDZUnFFy4WqEFP8ibdXeFANnylW19+vOwdjOAvqIzPdsNCEMT6VKTHnYu4K64AWRih0mkFms6Q==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.4.tgz", + "integrity": "sha512-Cv/sk8on/tpvvqbEvR1h03mdyNdyvvO+WhtFlL4jrZ+DSsN/oSQHVqmJQI/sBCqqbOArFcYCAYDfyzqFwV4GSQ==", "requires": { - "bl": "^2.2.1", - "bson": "^1.1.4", - "denque": "^1.4.1", - "optional-require": "^1.0.3", - "safe-buffer": "^5.1.2", - "saslprep": "^1.0.0" + "bson": "^4.5.4", + "denque": "^2.0.1", + "mongodb-connection-string-url": "^2.1.0", + "saslprep": "^1.0.3" } } } }, - "mongoose-legacy-pluralize": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", - "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" - }, "morgan": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", @@ -16945,31 +16873,27 @@ } }, "mpath": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.3.tgz", - "integrity": "sha512-eb9rRvhDltXVNL6Fxd2zM9D4vKBxjVVQNLNijlj7uoXUy19zNDsIif5zR+pWmPCWNKwAtqyo4JveQm4nfD5+eA==" + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.4.tgz", + "integrity": "sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g==" }, "mquery": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.5.tgz", - "integrity": "sha512-VjOKHHgU84wij7IUoZzFRU07IAxd5kWJaDmyUzQlbjHjyoeK5TNeeo8ZsFDtTYnSgpW6n/nMNIHvE3u8Lbrf4A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.0.tgz", + "integrity": "sha512-nGjm89lHja+T/b8cybAby6H0YgA4qYC/lx6UlwvHGqvTq8bDaNeCwl1sY8uRELrNbVWJzIihxVd+vphGGn1vBw==", "requires": { - "bluebird": "3.5.1", - "debug": "3.1.0", + "debug": "4.x", "regexp-clone": "^1.0.0", - "safe-buffer": "5.1.2", "sliced": "1.0.1" }, "dependencies": { - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + } } } }, @@ -17016,7 +16940,8 @@ "nan": { "version": "2.14.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "optional": true }, "nanomatch": { "version": "1.2.13", @@ -17075,6 +17000,33 @@ "resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-4.3.1.tgz", "integrity": "sha512-+vxjSaiDWjAj6kR6KKW0YDuV6O4UCNWGAO8m8ITjFKPWcTmU1GVnL+J5TAUTKpPnUAHCKDxXpOHVaERid223Ww==" }, + "new-find-package-json": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-1.1.0.tgz", + "integrity": "sha512-KOH3BNZcTKPzEkaJgG2iSUaurxKmefqRKmCOYH+8xqJytNIgjqU4J88BHfK+gy/UlEzlhccLyuJDJAcCgexSwA==", + "dev": true, + "requires": { + "debug": "^4.3.2", + "tslib": "^2.3.0" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "dev": true + } + } + }, "ng-infinite-scroll": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ng-infinite-scroll/-/ng-infinite-scroll-1.3.0.tgz", @@ -17347,11 +17299,6 @@ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.2.tgz", "integrity": "sha512-Dz7zVwlef4k5R71fdmxwR8Q39fiboGbu3xgswkzGwczUfjp873rVxt1O46+Fh0j1ORnAC6L9+heI8uUpO6DT7Q==" }, - "noop-logger": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", - "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" - }, "nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -17694,11 +17641,6 @@ "last-call-webpack-plugin": "^3.0.0" } }, - "optional-require": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.3.tgz", - "integrity": "sha512-RV2Zp2MY2aeYK5G+B/Sps8lW5NHAzE5QClbFP15j+PWmP+T9PxlJXBOOLoSAdgwFvS4t0aMR4vpedMkbHfh0nA==" - }, "optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -18651,9 +18593,9 @@ "dev": true }, "prebuild-install": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.2.tgz", - "integrity": "sha512-PzYWIKZeP+967WuKYXlTOhYBgGOvTRSfaKI89XnfJ0ansRAH7hDU45X+K+FZeI1Wb/7p/NnuctPH3g0IqKUuSQ==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", + "integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==", "requires": { "detect-libc": "^1.0.3", "expand-template": "^2.0.3", @@ -18662,7 +18604,6 @@ "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", "node-abi": "^2.21.0", - "noop-logger": "^0.1.1", "npmlog": "^4.0.1", "pump": "^3.0.0", "rc": "^1.2.7", @@ -18925,8 +18866,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "puppeteer-core": { "version": "5.3.1", @@ -19561,15 +19501,6 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, - "require_optional": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", - "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", - "requires": { - "resolve-from": "^2.0.0", - "semver": "^5.1.0" - } - }, "requireindex": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", @@ -19648,11 +19579,6 @@ } } }, - "resolve-from": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", - "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" - }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -21906,6 +21832,15 @@ "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -22411,9 +22346,9 @@ } }, "typescript": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.3.tgz", - "integrity": "sha512-eVYaEHALSt+s9LbvgEv4Ef+Tdq7hBiIZgii12xXJnukryt3pMgJf6aKhoCZ3FWQsu6sydEnkg11fYXLzhLBjeQ==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", + "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", "dev": true }, "uglify-js": { diff --git a/package.json b/package.json index e8a9fe6d16..cb33ac958c 100644 --- a/package.json +++ b/package.json @@ -83,12 +83,12 @@ "body-parser": "^1.19.1", "bootstrap": "3.4.1", "boxicons": "1.8.0", - "bson-ext": "^2.0.6", + "bson-ext": "^4.0.2", "busboy": "^0.3.1", "celebrate": "^15.0.0", "compression": "~1.7.2", "connect-datadog": "0.0.9", - "connect-mongo": "^4.4.1", + "connect-mongo": "^4.6.0", "convict": "^6.2.1", "convict-format-with-validator": "^6.2.0", "cookie-parser": "~1.4.6", @@ -117,8 +117,9 @@ "libphonenumber-js": "^1.9.44", "lodash": "^4.17.21", "moment-timezone": "0.5.34", + "mongodb": "^4.1.4", "mongodb-uri": "^0.9.7", - "mongoose": "^5.13.5", + "mongoose": "^6.0.13", "multiparty": ">=4.2.2", "neverthrow": "^4.3.1", "ng-infinite-scroll": "^1.3.0", @@ -176,7 +177,6 @@ "@types/jest": "^27.0.3", "@types/json-stringify-safe": "^5.0.0", "@types/lodash": "^4.14.178", - "@types/mongodb": "^3.6.20", "@types/mongodb-uri": "^0.9.1", "@types/node": "^14.18.0", "@types/nodemailer": "^6.4.4", @@ -223,8 +223,7 @@ "maildev": "^1.1.0", "mini-css-extract-plugin": "^0.5.0", "mockdate": "^3.0.5", - "mockingoose": "^2.13.2", - "mongodb-memory-server-core": "^6.9.6", + "mongodb-memory-server-core": "^8.0.4", "ngrok": "^4.2.2", "optimize-css-assets-webpack-plugin": "^5.0.8", "prettier": "^2.5.1", diff --git a/src/app/config/config.ts b/src/app/config/config.ts index 2b7a916c13..6ce896dd47 100644 --- a/src/app/config/config.ts +++ b/src/app/config/config.ts @@ -110,14 +110,6 @@ const dbConfig: DbConfig = { pass: '', // Only create indexes in dev env to avoid adverse production impact. autoIndex: isDev, - // Avoid using deprecated URL string parser in MongoDB driver - useNewUrlParser: true, - useUnifiedTopology: true, - // Avoid using deprecated collection.ensureIndex internally - useCreateIndex: true, - // upgrade to mongo driver's native findOneAndUpdate function instead of - // findAndModify. - useFindAndModify: false, promiseLibrary: global.Promise, }, } @@ -180,7 +172,7 @@ const cookieSettings: SessionOptions['cookie'] = { /** * Fetches AWS credentials */ -const configureAws = async () => { +const configureAws = async (): Promise => { if (!isDev) { const getCredentials = () => { return new Promise((resolve, reject) => { diff --git a/src/app/loaders/express/session.ts b/src/app/loaders/express/session.ts index fb00658852..259bc68f9f 100644 --- a/src/app/loaders/express/session.ts +++ b/src/app/loaders/express/session.ts @@ -2,6 +2,7 @@ import MongoStore from 'connect-mongo' import cookieParser from 'cookie-parser' import { RequestHandler } from 'express' import session from 'express-session' +import { MongoClient } from 'mongodb' import { Connection } from 'mongoose' import config from '../../config/config' @@ -15,7 +16,7 @@ const sessionMiddlewares = (connection: Connection): RequestHandler[] => { cookie: config.cookieSettings, name: 'connect.sid', store: MongoStore.create({ - client: connection.getClient(), + client: connection.getClient() as MongoClient, }), }) diff --git a/src/app/loaders/mongoose.ts b/src/app/loaders/mongoose.ts index 0ac7dbdde4..d76b7a0d84 100644 --- a/src/app/loaders/mongoose.ts +++ b/src/app/loaders/mongoose.ts @@ -28,7 +28,7 @@ export default async (): Promise => { process.exit(1) } - const mongod = new MongoMemoryServer({ + const mongod = await MongoMemoryServer.create({ binary: { version: String(process.env.MONGO_BINARY_VERSION) }, instance: { port: 3000, @@ -38,7 +38,7 @@ export default async (): Promise => { }) // Store the uri to connect to later on - config.db.uri = await mongod.getConnectionString() + config.db.uri = mongod.getUri() } // Actually connect to the database diff --git a/src/app/models/__tests__/admin_verification.server.model.spec.ts b/src/app/models/__tests__/admin_verification.server.model.spec.ts index 930f9cdd3d..7708fccd43 100644 --- a/src/app/models/__tests__/admin_verification.server.model.spec.ts +++ b/src/app/models/__tests__/admin_verification.server.model.spec.ts @@ -1,4 +1,3 @@ -import { ObjectID } from 'bson' import mongoose from 'mongoose' import getAdminVerificationModel from 'src/app/models/admin_verification.server.model' @@ -18,7 +17,7 @@ describe('AdminVerification Model', () => { describe('Schema', () => { const DEFAULT_PARAMS: IAdminVerification = { - admin: new ObjectID(), + admin: new mongoose.Types.ObjectId(), expireAt: new Date(), hashedContact: 'mockHashedContact', hashedOtp: 'mockHashedOtp', @@ -57,13 +56,11 @@ describe('AdminVerification Model', () => { expect(actual._id).toBeDefined() expect(actual.createdAt).toBeInstanceOf(Date) expect(actual.updatedAt).toBeInstanceOf(Date) - expect(actual).toEqual( - expect.objectContaining({ - ...customParams, - // Add defaults that has not been overridden. - numOtpSent: 0, - }), - ) + expect(actual).toMatchObject({ + ...customParams, + // Add defaults that has not been overridden. + numOtpSent: 0, + }) }) it('should throw validation error on missing admin', async () => { @@ -133,7 +130,7 @@ describe('AdminVerification Model', () => { it('should create successfully when document does not exist', async () => { // Arrange const params: UpsertOtpParams = { - admin: new ObjectID(), + admin: new mongoose.Types.ObjectId(), expireAt: new Date(), hashedContact: 'mockHashedContact', hashedOtp: 'mockHashedOtp', @@ -149,13 +146,13 @@ describe('AdminVerification Model', () => { const expected = { ...params, numOtpAttempts: 0, numOtpSent: 1 } // Should now have one document. await expect(AdminVerification.countDocuments()).resolves.toEqual(1) - expect(actual).toEqual(expect.objectContaining(expected)) + expect(actual).toMatchObject(expected) }) it('should update successfully when a document already exists', async () => { // Arrange // Insert mock document into collection. - const adminId = new ObjectID() + const adminId = new mongoose.Types.ObjectId() const oldExpireAt = new Date() const newExpireAt = new Date(Date.now() + 9000000) const oldNumOtpSent = 3 @@ -190,14 +187,14 @@ describe('AdminVerification Model', () => { } // Should still only have one document. await expect(AdminVerification.countDocuments()).resolves.toEqual(1) - expect(actual).toEqual(expect.objectContaining(expected)) + expect(actual).toMatchObject(expected) }) it('should throw error if validation fails due to invalid upsert parameters', async () => { // Arrange const invalidParams: UpsertOtpParams = { // Invalid admin parameter. - admin: undefined, + admin: null, expireAt: new Date(), hashedContact: 'mockHashedContact', hashedOtp: 'mockHashedOtp', @@ -219,7 +216,7 @@ describe('AdminVerification Model', () => { it('should increment successfully', async () => { // Arrange // Insert mock document into collection. - const adminId = new ObjectID() + const adminId = new mongoose.Types.ObjectId() const initialOtpAttempts = 5 const adminVerificationParams = { admin: adminId, @@ -240,19 +237,17 @@ describe('AdminVerification Model', () => { // Assert // Exactly the same as initial params, but with numOtpAttempts // incremented by 1. - await expect(actualPromise).resolves.toEqual( - expect.objectContaining({ - ...adminVerificationParams, - numOtpAttempts: initialOtpAttempts + 1, - }), - ) + await expect(actualPromise).resolves.toMatchObject({ + ...adminVerificationParams, + numOtpAttempts: initialOtpAttempts + 1, + }) }) it('should return null if document cannot be retrieved', async () => { // Arrange // Should have no documents yet. await expect(AdminVerification.countDocuments()).resolves.toEqual(0) - const freshAdminId = new ObjectID() + const freshAdminId = new mongoose.Types.ObjectId() // Act const actualPromise = diff --git a/src/app/models/__tests__/encrypt-submission.server.model.spec.ts b/src/app/models/__tests__/encrypt-submission.server.model.spec.ts index a497d164db..bdae0d708e 100644 --- a/src/app/models/__tests__/encrypt-submission.server.model.spec.ts +++ b/src/app/models/__tests__/encrypt-submission.server.model.spec.ts @@ -53,7 +53,7 @@ describe('Encrypt Submission Model', () => { .tz('Asia/Singapore') .format('Do MMM YYYY, h:mm:ss a'), } - expect(result).toEqual(expected) + expect(result).toMatchObject(expected) }) it('should return null when submission is of SubmissionType.Email', async () => { @@ -137,7 +137,7 @@ describe('Encrypt Submission Model', () => { })) .reverse(), } - expect(actual).toEqual(expected) + expect(actual).toMatchObject(expected) }) it('should return offset metadata with correct count when page number is provided', async () => { @@ -179,7 +179,7 @@ describe('Encrypt Submission Model', () => { }, ], } - expect(actual).toEqual(expected) + expect(actual).toMatchObject(expected) }) it('should return offset metadata with correct count when page size is provided', async () => { @@ -221,7 +221,7 @@ describe('Encrypt Submission Model', () => { }, ], } - expect(actual).toEqual(expected) + expect(actual).toMatchObject(expected) }) it('should return empty metadata array when given page has no metadata', async () => { @@ -254,7 +254,7 @@ describe('Encrypt Submission Model', () => { // show metadata: [], } - expect(actual).toEqual(expected) + expect(actual).toMatchObject(expected) }) it('should return empty metadata array when formId has no metadata', async () => { @@ -272,7 +272,7 @@ describe('Encrypt Submission Model', () => { // show metadata: [], } - expect(actual).toEqual(expected) + expect(actual).toMatchObject(expected) }) }) @@ -284,6 +284,7 @@ describe('Encrypt Submission Model', () => { submissionType: SubmissionType.Encrypt, form: validFormId, encryptedContent: 'mock encrypted content abc', + verifiedContent: 'mock verified content', version: 3, }) const expectedSubmission = pick( @@ -309,7 +310,7 @@ describe('Encrypt Submission Model', () => { retrievedSubmissions.push(submission) } // Cursor stream should contain only that single submission. - expect(retrievedSubmissions).toEqual([expectedSubmission]) + expect(retrievedSubmissions).toMatchObject([expectedSubmission]) }) it('should return cursor even if no submissions are found', async () => { @@ -329,7 +330,7 @@ describe('Encrypt Submission Model', () => { retrievedSubmissions.push(submission) } // Cursor stream should return nothing. - expect(retrievedSubmissions).toEqual([]) + expect(retrievedSubmissions).toMatchObject([]) }) }) @@ -362,7 +363,7 @@ describe('Encrypt Submission Model', () => { 'version', ) expect(actual).not.toBeNull() - expect(actual?.toJSON()).toEqual(expected) + expect(actual?.toJSON()).toMatchObject(expected) }) it('should return null when submission id does not exist', async () => { diff --git a/src/app/models/__tests__/form.server.model.spec.ts b/src/app/models/__tests__/form.server.model.spec.ts index c96933c9e3..84a69ca01f 100644 --- a/src/app/models/__tests__/form.server.model.spec.ts +++ b/src/app/models/__tests__/form.server.model.spec.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { ObjectId } from 'bson-ext' import { cloneDeep, map, merge, omit, orderBy, pick, range } from 'lodash' import mongoose, { Types } from 'mongoose' import { @@ -42,7 +41,7 @@ const Form = getFormModel(mongoose) const EncryptedForm = getEncryptedFormModel(mongoose) const EmailForm = getEmailFormModel(mongoose) -const MOCK_ADMIN_OBJ_ID = new ObjectId() +const MOCK_ADMIN_OBJ_ID = new mongoose.Types.ObjectId() const MOCK_ADMIN_DOMAIN = 'example.com' const MOCK_ADMIN_EMAIL = `test@${MOCK_ADMIN_DOMAIN}` @@ -126,7 +125,7 @@ describe('Form Model', () => { '__v', ]) const expectedObject = merge({}, FORM_DEFAULTS, MOCK_FORM_PARAMS) - expect(actualSavedObject).toEqual(expectedObject) + expect(actualSavedObject).toMatchObject(expectedObject) }) it('should create and save successfully with valid esrvcId', async () => { @@ -154,7 +153,7 @@ describe('Form Model', () => { '__v', ]) const expectedObject = merge({}, FORM_DEFAULTS, validFormParams) - expect(actualSavedObject).toEqual(expectedObject) + expect(actualSavedObject).toMatchObject(expectedObject) }) it('should save successfully, but not save fields that is not defined in the schema', async () => { @@ -181,7 +180,7 @@ describe('Form Model', () => { '__v', ]) const expectedObject = merge({}, FORM_DEFAULTS, MOCK_FORM_PARAMS) - expect(actualSavedObject).toEqual(expectedObject) + expect(actualSavedObject).toMatchObject(expectedObject) // Extra key should not be saved expect(Object.keys(saved)).not.toContain('extra') @@ -195,7 +194,7 @@ describe('Form Model', () => { conditions: [ { _id: '', - field: new ObjectId(), + field: new mongoose.Types.ObjectId(), state: 'is equals to', value: '', ifValueType: 'number', @@ -235,7 +234,7 @@ describe('Form Model', () => { MOCK_FORM_PARAMS, FORM_LOGICS, ) - expect(actualSavedObject).toEqual(expectedObject) + expect(actualSavedObject).toMatchObject(expectedObject) }) it('should create and save successfully with valid permissionList emails', async () => { @@ -270,7 +269,7 @@ describe('Form Model', () => { omit(FORM_DEFAULTS, 'permissionList'), MOCK_FORM_PARAMS, ) - expect(actualSavedObject).toEqual(expectedObject) + expect(actualSavedObject).toMatchObject(expectedObject) // Remove indeterministic id from actual permission list const actualPermissionList = saved @@ -306,7 +305,7 @@ describe('Form Model', () => { it('should reject when admin id is invalid', async () => { // Arrange - const invalidAdminId = new ObjectId() + const invalidAdminId = new mongoose.Types.ObjectId() const paramsWithInvalidAdmin = merge({}, MOCK_FORM_PARAMS, { admin: invalidAdminId, }) @@ -428,7 +427,7 @@ describe('Form Model', () => { ENCRYPT_FORM_DEFAULTS, MOCK_ENCRYPTED_FORM_PARAMS, ) - expect(actualSavedObject).toEqual(expectedObject) + expect(actualSavedObject).toMatchObject(expectedObject) }) it('should save successfully, but not save fields that is not defined in the schema', async () => { @@ -459,7 +458,7 @@ describe('Form Model', () => { ENCRYPT_FORM_DEFAULTS, MOCK_ENCRYPTED_FORM_PARAMS, ) - expect(actualSavedObject).toEqual(expectedObject) + expect(actualSavedObject).toMatchObject(expectedObject) // Extra key should not be saved expect(Object.keys(saved)).not.toContain('extra') @@ -497,7 +496,7 @@ describe('Form Model', () => { omit(ENCRYPT_FORM_DEFAULTS, 'permissionList'), MOCK_ENCRYPTED_FORM_PARAMS, ) - expect(actualSavedObject).toEqual(expectedObject) + expect(actualSavedObject).toMatchObject(expectedObject) // Remove indeterministic id from actual permission list const actualPermissionList = ( @@ -547,7 +546,7 @@ describe('Form Model', () => { it('should reject when admin id is invalid', async () => { // Arrange - const invalidAdminId = new ObjectId() + const invalidAdminId = new mongoose.Types.ObjectId() const paramsWithInvalidAdmin = merge({}, MOCK_ENCRYPTED_FORM_PARAMS, { admin: invalidAdminId, }) @@ -679,7 +678,7 @@ describe('Form Model', () => { EMAIL_FORM_DEFAULTS, MOCK_EMAIL_FORM_PARAMS, ) - expect(actualSavedObject).toEqual(expectedObject) + expect(actualSavedObject).toMatchObject(expectedObject) }) it('should save successfully, but not save fields that is not defined in the schema', async () => { @@ -710,7 +709,7 @@ describe('Form Model', () => { EMAIL_FORM_DEFAULTS, MOCK_EMAIL_FORM_PARAMS, ) - expect(actualSavedObject).toEqual(expectedObject) + expect(actualSavedObject).toMatchObject(expectedObject) // Extra key should not be saved expect(Object.keys(saved)).not.toContain('extra') @@ -747,7 +746,7 @@ describe('Form Model', () => { omit(EMAIL_FORM_DEFAULTS, 'permissionList'), omit(MOCK_EMAIL_FORM_PARAMS, 'emails'), ) - expect(actualSavedObject).toEqual(expectedObject) + expect(actualSavedObject).toMatchObject(expectedObject) const actualEmails = saved.toObject().emails expect(actualEmails).toEqual(mockEmailsArray) @@ -785,7 +784,7 @@ describe('Form Model', () => { omit(EMAIL_FORM_DEFAULTS, 'permissionList'), MOCK_EMAIL_FORM_PARAMS, ) - expect(actualSavedObject).toEqual(expectedObject) + expect(actualSavedObject).toMatchObject(expectedObject) // Remove indeterministic id from actual permission list const actualPermissionList = saved @@ -850,7 +849,7 @@ describe('Form Model', () => { it('should reject when admin id is invalid', async () => { // Arrange - const invalidAdminId = new ObjectId() + const invalidAdminId = new mongoose.Types.ObjectId() const paramsWithInvalidAdmin = merge({}, MOCK_EMAIL_FORM_PARAMS, { admin: invalidAdminId, }) @@ -953,7 +952,9 @@ describe('Form Model', () => { }) it('should return null for invalid form ID', async () => { - const returned = await Form.deactivateById(String(new ObjectId())) + const returned = await Form.deactivateById( + String(new mongoose.Types.ObjectId()), + ) expect(returned).toBeNull() }) }) @@ -961,7 +962,7 @@ describe('Form Model', () => { describe('getFullFormById', () => { it('should return null when the formId is invalid', async () => { // Arrange - const invalidFormId = new ObjectId() + const invalidFormId = new mongoose.Types.ObjectId() // Act const form = await Form.getFullFormById(String(invalidFormId)) @@ -985,7 +986,7 @@ describe('Form Model', () => { // Form should be returned expect(actualForm).not.toBeNull() // Omit admin key since it is populated is not ObjectId anymore. - expect(omit(actualForm, 'admin')).toEqual(omit(form, 'admin')) + expect(omit(actualForm, 'admin')).toMatchObject(omit(form, 'admin')) // Verify populated admin shape expect(actualForm?.admin).not.toBeNull() expect(actualForm?.admin.email).toEqual(populatedAdmin.email) @@ -996,9 +997,7 @@ describe('Form Model', () => { 'lastModified', '__v', ]) - expect(actualForm?.admin.agency).toEqual( - expect.objectContaining(expectedAgency), - ) + expect(actualForm?.admin.agency).toMatchObject(expectedAgency) }) it('should return the populated encrypt form when formId is valid', async () => { @@ -1016,7 +1015,7 @@ describe('Form Model', () => { // Form should be returned expect(actualForm).not.toBeNull() // Omit admin key since it is populated is not ObjectId anymore. - expect(omit(actualForm, 'admin')).toEqual(omit(form, 'admin')) + expect(omit(actualForm, 'admin')).toMatchObject(omit(form, 'admin')) // Verify populated admin shape expect(actualForm?.admin).not.toBeNull() expect(actualForm?.admin.email).toEqual(populatedAdmin.email) @@ -1027,16 +1026,14 @@ describe('Form Model', () => { 'lastModified', '__v', ]) - expect(actualForm?.admin.agency).toEqual( - expect.objectContaining(expectedAgency), - ) + expect(actualForm?.admin.agency).toMatchObject(expectedAgency) }) }) describe('getOtpData', () => { it('should return null when formId does not exist', async () => { // Arrange - const invalidFormId = new ObjectId() + const invalidFormId = new mongoose.Types.ObjectId() // Act const form = await Form.getOtpData(String(invalidFormId)) @@ -1068,7 +1065,7 @@ describe('Form Model', () => { }, msgSrvcName: emailFormParams.msgSrvcName, } - expect(actualOtpData).toEqual(expectedOtpData) + expect(actualOtpData).toMatchObject(expectedOtpData) }) it('should return otpData of an encrypt form when formId is valid', async () => { @@ -1094,14 +1091,14 @@ describe('Form Model', () => { }, msgSrvcName: encryptFormParams.msgSrvcName, } - expect(actualOtpData).toEqual(expectedOtpData) + expect(actualOtpData).toMatchObject(expectedOtpData) }) }) describe('getMetaByUserIdOrEmail', () => { it('should return empty array when user has no forms to view', async () => { // Arrange - const randomUserId = new ObjectId() + const randomUserId = new mongoose.Types.ObjectId() const invalidEmail = 'not-valid@example.com' // Act @@ -1117,7 +1114,7 @@ describe('Form Model', () => { it('should return array of forms user is permitted to view', async () => { // Arrange // Add additional user. - const differentUserId = new ObjectId() + const differentUserId = new mongoose.Types.ObjectId() const diffPreload = await dbHandler.insertFormCollectionReqs({ userId: differentUserId, mailName: 'something-else', @@ -1192,12 +1189,12 @@ describe('Form Model', () => { // even though there are 5 forms in the collection. await expect(Form.countDocuments()).resolves.toEqual(5) expect(actual.length).toEqual(3) - expect(actual).toEqual(expected) + expect(actual).toMatchObject(expected) }) }) describe('createFormLogic', () => { - const logicId = new ObjectId().toHexString() + const logicId = new mongoose.Types.ObjectId().toHexString() const mockExistingFormLogic = { form_logics: [ @@ -1325,7 +1322,7 @@ describe('Form Model', () => { it('should return null if formId is invalid', async () => { // arrange - const invalidFormId = new ObjectId().toHexString() + const invalidFormId = new mongoose.Types.ObjectId().toHexString() // act const modifiedForm = await Form.createFormLogic( @@ -1340,7 +1337,7 @@ describe('Form Model', () => { }) describe('deleteFormLogic', () => { - const logicId = new ObjectId().toHexString() + const logicId = new mongoose.Types.ObjectId().toHexString() const mockFormLogic = { form_logics: [ { @@ -1380,7 +1377,7 @@ describe('Form Model', () => { it('should return form with remaining logic upon successful delete of one logic', async () => { // arrange - const logicId2 = new ObjectId().toHexString() + const logicId2 = new mongoose.Types.ObjectId().toHexString() const mockFormLogicMultiple = { form_logics: [ { @@ -1425,7 +1422,7 @@ describe('Form Model', () => { it('should return null if formId is invalid', async () => { // arrange - const invalidFormId = new ObjectId().toHexString() + const invalidFormId = new mongoose.Types.ObjectId().toHexString() // act const modifiedForm = await Form.deleteFormLogic(invalidFormId, logicId) @@ -1460,7 +1457,7 @@ describe('Form Model', () => { expect(actual?.toObject().form_fields).toEqual(expectedFormFields) // Check db state expect(retrievedForm).not.toBeNull() - expect(retrievedForm?.form_fields).toEqual(expectedFormFields) + expect(retrievedForm?.form_fields).toMatchObject(expectedFormFields) }) it('should return form unchanged when field id is invalid', async () => { @@ -1476,11 +1473,11 @@ describe('Form Model', () => { // Act const actual = await Form.deleteFormFieldById( form._id, - new ObjectId().toHexString(), + new mongoose.Types.ObjectId().toHexString(), ) // Assert - expect(actual?.toObject()).toEqual({ + expect(actual?.toObject()).toMatchObject({ ...form.toObject(), lastModified: expect.any(Date), }) @@ -1508,7 +1505,7 @@ describe('Form Model', () => { // Assert // Should have defaults populated but also replace the endpage with the new params - expect(actual?.toObject()).toEqual({ + expect(actual?.toObject()).toMatchObject({ ...form, lastModified: expect.any(Date), endPage: { ...updatedEndPage }, @@ -1531,7 +1528,7 @@ describe('Form Model', () => { // Assert // Should have defaults populated but also replace the endpage with the new params - expect(actual?.toObject()).toEqual({ + expect(actual?.toObject()).toMatchObject({ ...form, lastModified: expect.any(Date), endPage: { @@ -1554,7 +1551,7 @@ describe('Form Model', () => { // Act const actual = await Form.updateEndPageById( - new ObjectId().toHexString(), + new mongoose.Types.ObjectId().toHexString(), updatedEndPage, ) @@ -1565,8 +1562,8 @@ describe('Form Model', () => { }) describe('updateFormLogic', () => { - const logicId1 = new ObjectId().toHexString() - const logicId2 = new ObjectId().toHexString() + const logicId1 = new mongoose.Types.ObjectId().toHexString() + const logicId2 = new mongoose.Types.ObjectId().toHexString() const mockExistingFormLogic = { form_logics: [ @@ -1670,7 +1667,7 @@ describe('Form Model', () => { it('should return null if formId is invalid', async () => { // arrange - const invalidFormId = new ObjectId().toHexString() + const invalidFormId = new mongoose.Types.ObjectId().toHexString() // act const modifiedForm = await Form.updateFormLogic( @@ -1686,7 +1683,7 @@ describe('Form Model', () => { it('should return unmodified form if logicId is invalid', async () => { // arrange - const invalidLogicId = new ObjectId().toHexString() + const invalidLogicId = new mongoose.Types.ObjectId().toHexString() const mockExistingFormLogicSingle = { form_logics: [ { @@ -1789,7 +1786,7 @@ describe('Form Model', () => { // Assert // Should have defaults populated but also replace the start page with the new params - expect(actual?.toObject()).toEqual({ + expect(actual?.toObject()).toMatchObject({ ...form, lastModified: expect.any(Date), startPage: { @@ -1816,7 +1813,7 @@ describe('Form Model', () => { // Act const actual = await Form.updateStartPageById( - new ObjectId().toHexString(), + new mongoose.Types.ObjectId().toHexString(), updatedStartPage, ) @@ -1910,7 +1907,7 @@ describe('Form Model', () => { it('should only disable sms verifications for a particular user', async () => { // Arrange - const MOCK_USER_ID = new ObjectId() + const MOCK_USER_ID = new mongoose.Types.ObjectId() await dbHandler.insertFormCollectionReqs({ userId: MOCK_USER_ID, mailDomain: 'something.com', @@ -2113,7 +2110,7 @@ describe('Form Model', () => { expect(form).toBeDefined() // Add additional user. const diffPreload = await dbHandler.insertFormCollectionReqs({ - userId: new ObjectId(), + userId: new mongoose.Types.ObjectId(), mailName: 'another', mailDomain: MOCK_ADMIN_DOMAIN, }) @@ -2125,7 +2122,7 @@ describe('Form Model', () => { const actual = form.getDashboardView(diffPopulatedAdmin) // Assert - expect(actual).toEqual({ + expect(actual).toMatchObject({ _id: form._id, title: form.title, status: form.status, @@ -2147,7 +2144,7 @@ describe('Form Model', () => { expect(form).toBeDefined() // Add additional user. const diffPreload = await dbHandler.insertFormCollectionReqs({ - userId: new ObjectId(), + userId: new mongoose.Types.ObjectId(), mailName: 'another-thing', mailDomain: MOCK_ADMIN_DOMAIN, }) @@ -2159,7 +2156,7 @@ describe('Form Model', () => { const actual = form.getDashboardView(diffPopulatedAdmin) // Assert - expect(actual).toEqual({ + expect(actual).toMatchObject({ _id: form._id, title: form.title, status: form.status, @@ -2208,9 +2205,9 @@ describe('Form Model', () => { const actual = emailForm.getPublicView() // Assert - expect(actual).toEqual(pick(emailForm, EMAIL_PUBLIC_FORM_FIELDS)) + expect(actual).toMatchObject(pick(emailForm, EMAIL_PUBLIC_FORM_FIELDS)) // Admin should be plain admin id since form is not populated. - expect(actual.admin).toBeInstanceOf(ObjectId) + expect(actual.admin).toBeInstanceOf(mongoose.Types.ObjectId) }) it('should correctly return public view of populated email mode form', async () => { @@ -2230,15 +2227,13 @@ describe('Form Model', () => { // Assert const expectedPublicAgencyView = populatedAdmin.agency.getPublicView() - expect(JSON.stringify(actual)).toEqual( - JSON.stringify({ - ...pick(populatedEmailForm, STORAGE_PUBLIC_FORM_FIELDS), - // Admin should only contain public view of agency since agency is populated. - admin: { - agency: expectedPublicAgencyView, - }, - }), - ) + expect(actual).toMatchObject({ + ...pick(populatedEmailForm, STORAGE_PUBLIC_FORM_FIELDS), + // Admin should only contain public view of agency since agency is populated. + admin: { + agency: expectedPublicAgencyView, + }, + }) }) it('should correctly return public view of unpopulated encrypt mode form', async () => { @@ -2254,9 +2249,11 @@ describe('Form Model', () => { const actual = encryptForm.getPublicView() // Assert - expect(actual).toEqual(pick(encryptForm, STORAGE_PUBLIC_FORM_FIELDS)) + expect(actual).toMatchObject( + pick(encryptForm, STORAGE_PUBLIC_FORM_FIELDS), + ) // Admin should be plain admin id since form is not populated. - expect(actual.admin).toBeInstanceOf(ObjectId) + expect(actual.admin).toBeInstanceOf(mongoose.Types.ObjectId) }) it('should correctly return public view of populated encrypt mode form', async () => { @@ -2276,15 +2273,13 @@ describe('Form Model', () => { // Assert const expectedPublicAgencyView = populatedAdmin.agency.getPublicView() - expect(JSON.stringify(actual)).toEqual( - JSON.stringify({ - ...pick(populatedEncryptForm, STORAGE_PUBLIC_FORM_FIELDS), - // Admin should only contain public view of agency since agency is populated. - admin: { - agency: expectedPublicAgencyView, - }, - }), - ) + expect(actual).toMatchObject({ + ...pick(populatedEncryptForm, STORAGE_PUBLIC_FORM_FIELDS), + // Admin should only contain public view of agency since agency is populated. + admin: { + agency: expectedPublicAgencyView, + }, + }) }) }) @@ -2331,7 +2326,7 @@ describe('Form Model', () => { it('should return null if fieldId does not correspond to any field in the form', async () => { // Arrange - const invalidFieldId = new ObjectId().toHexString() + const invalidFieldId = new mongoose.Types.ObjectId().toHexString() const someNewField = { description: 'this does not matter', } as FormFieldDto @@ -2406,10 +2401,9 @@ describe('Form Model', () => { // Assert const expectedField = { ...omit(newField, 'getQuestion'), - _id: new ObjectId(newField._id), + _id: new mongoose.Types.ObjectId(newField._id), } - // @ts-ignore - expect(actual?.form_fields.toObject()).toEqual([expectedField]) + expect(actual?.form_fields).toMatchObject([expectedField]) }) it('should return validation error if model validation fails whilst creating field', async () => { @@ -2450,7 +2444,7 @@ describe('Form Model', () => { // Assert const expectedOriginalField = { ...omit(fieldToDuplicate, ['getQuestion']), - _id: new ObjectId(fieldToDuplicate._id), + _id: new mongoose.Types.ObjectId(fieldToDuplicate._id), } const expectedDuplicatedField = omit(fieldToDuplicate, [ '_id', @@ -2458,14 +2452,13 @@ describe('Form Model', () => { 'getQuestion', ]) - // @ts-ignore - expect(actual?.form_fields.toObject()[0]).toEqual(expectedOriginalField) + expect(actual?.form_fields?.[0]).toMatchObject(expectedOriginalField) expect(actualDuplicatedField).toEqual(expectedDuplicatedField) }) it('should return null if given fieldId is invalid', async () => { const updatedForm = await validForm.duplicateFormFieldById( - new ObjectId().toHexString(), + new mongoose.Types.ObjectId().toHexString(), ) // Assert @@ -2475,7 +2468,7 @@ describe('Form Model', () => { describe('reorderFormFieldById', () => { let form: IFormSchema - const FIELD_ID_TO_REORDER = new ObjectId().toHexString() + const FIELD_ID_TO_REORDER = new mongoose.Types.ObjectId().toHexString() beforeEach(async () => { form = await Form.create({ @@ -2548,7 +2541,7 @@ describe('Form Model', () => { it('should return null if given fieldId is invalid', async () => { const updatedForm = await form.reorderFormFieldById( - new ObjectId().toHexString(), + new mongoose.Types.ObjectId().toHexString(), 3, ) diff --git a/src/app/models/__tests__/form_feedback.server.model.spec.ts b/src/app/models/__tests__/form_feedback.server.model.spec.ts index 444a73dfc3..cbd5d3e7e1 100644 --- a/src/app/models/__tests__/form_feedback.server.model.spec.ts +++ b/src/app/models/__tests__/form_feedback.server.model.spec.ts @@ -26,13 +26,11 @@ describe('form_feedback.server.model', () => { const actual = await FeedbackModel.create(DEFAULT_PARAMS) // Assert - expect(actual).toEqual( - expect.objectContaining({ - ...DEFAULT_PARAMS, - created: expect.any(Date), - lastModified: expect.any(Date), - }), - ) + expect(actual).toMatchObject({ + ...DEFAULT_PARAMS, + created: expect.any(Date), + lastModified: expect.any(Date), + }) }) it('should save successfully even when comment param is missing', async () => { @@ -42,13 +40,11 @@ describe('form_feedback.server.model', () => { const actual = await FeedbackModel.create(paramsWithoutComment) // Assert - expect(actual).toEqual( - expect.objectContaining({ - ...paramsWithoutComment, - created: expect.any(Date), - lastModified: expect.any(Date), - }), - ) + expect(actual).toMatchObject({ + ...paramsWithoutComment, + created: expect.any(Date), + lastModified: expect.any(Date), + }) }) it('should throw validation error when formId param is missing', async () => { @@ -99,7 +95,7 @@ describe('form_feedback.server.model', () => { // Assert // Cursor should return a lean object instead of a document. - expect(actualFeedback).toEqual([mockFeedbackDoc.toObject()]) + expect(actualFeedback).toMatchObject([mockFeedbackDoc.toObject()]) }) }) }) diff --git a/src/app/models/__tests__/login.server.model.spec.ts b/src/app/models/__tests__/login.server.model.spec.ts index 7d4e3ce828..923c22224d 100644 --- a/src/app/models/__tests__/login.server.model.spec.ts +++ b/src/app/models/__tests__/login.server.model.spec.ts @@ -37,12 +37,10 @@ describe('login.server.model', () => { const actual = await LoginModel.create(DEFAULT_PARAMS) // Assert - expect(actual).toEqual( - expect.objectContaining({ - ...DEFAULT_PARAMS, - created: expect.any(Date), - }), - ) + expect(actual).toMatchObject({ + ...DEFAULT_PARAMS, + created: expect.any(Date), + }) }) it('should throw validation error when admin param is missing', async () => { @@ -129,18 +127,19 @@ describe('login.server.model', () => { it('should save the correct form data', async () => { const saved = await LoginModel.addLoginFromForm(fullForm) const found = await LoginModel.findOne({ form: formId }) + + const expected = { + form: formId, + admin: adminId, + agency: agencyId, + authType: mockAuthType, + esrvcId: mockEsrvcId, + } + // Returned document should match - expect(saved.form).toEqual(formId) - expect(saved.admin).toEqual(adminId) - expect(saved.agency).toEqual(agencyId) - expect(saved.authType).toBe(mockAuthType) - expect(saved.esrvcId).toBe(mockEsrvcId) + expect(saved).toMatchObject(expected) // Found document should match - expect(found!.form).toEqual(formId) - expect(found!.admin).toEqual(adminId) - expect(found!.agency).toEqual(agencyId) - expect(found!.authType).toBe(mockAuthType) - expect(found!.esrvcId).toBe(mockEsrvcId) + expect(found).toMatchObject(expected) }) it('should reject when the form does not contain an e-service ID', async () => { @@ -256,7 +255,7 @@ describe('login.server.model', () => { total: loginsInRange.length, }, ] - expect(result).toEqual(expected) + expect(result).toMatchObject(expected) }) it('should return empty array when given dates do not correspond to any login documents', async () => { diff --git a/src/app/models/__tests__/submission.server.model.spec.ts b/src/app/models/__tests__/submission.server.model.spec.ts index 4aa6773359..91faf3af53 100644 --- a/src/app/models/__tests__/submission.server.model.spec.ts +++ b/src/app/models/__tests__/submission.server.model.spec.ts @@ -116,7 +116,7 @@ describe('Submission Model', () => { String(submission._id), ) - expect(result).toEqual({ + expect(result).toMatchObject({ webhookUrl: '', isRetryEnabled: false, webhookView: { @@ -152,7 +152,7 @@ describe('Submission Model', () => { String(submission._id), ) - expect(result).toEqual({ + expect(result).toMatchObject({ webhookUrl: MOCK_WEBHOOK_URL, isRetryEnabled: false, webhookView: { @@ -175,7 +175,7 @@ describe('Submission Model', () => { // Arrange const formCounts = [4, 2, 4] const formIdsAndCounts = times(formCounts.length, (it) => ({ - _id: mongoose.Types.ObjectId(), + _id: new mongoose.Types.ObjectId(), count: formCounts[it], })) const submissionPromises: Promise[] = [] @@ -204,14 +204,14 @@ describe('Submission Model', () => { const expectedResult = formIdsAndCounts.filter( ({ count }) => count > minSubCount, ) - expect(actualResult).toEqual(expect.arrayContaining(expectedResult)) + expect(actualResult).toMatchObject(expectedResult) }) it('should return an empty array if no forms have submission counts higher than given count', async () => { // Arrange const formCounts = [1, 1, 2] const formIdsAndCounts = times(formCounts.length, (it) => ({ - _id: mongoose.Types.ObjectId(), + _id: new mongoose.Types.ObjectId(), count: formCounts[it], })) const submissionPromises: Promise[] = [] @@ -264,7 +264,7 @@ describe('Submission Model', () => { const actualWebhookView = submission.getWebhookView() // Assert - expect(actualWebhookView).toEqual({ + expect(actualWebhookView).toMatchObject({ data: { formId: expect.any(String), submissionId: expect.any(String), @@ -296,7 +296,7 @@ describe('Submission Model', () => { const actualWebhookView = submission.getWebhookView() // Assert - expect(actualWebhookView).toEqual({ + expect(actualWebhookView).toMatchObject({ data: { attachmentDownloadUrls: {}, formId: expect.any(String), @@ -339,7 +339,7 @@ describe('Submission Model', () => { const actualWebhookView = populatedSubmission!.getWebhookView() // Assert - expect(actualWebhookView).toEqual({ + expect(actualWebhookView).toMatchObject({ data: { attachmentDownloadUrls: {}, formId: expect.any(String), diff --git a/src/app/models/__tests__/user.server.model.spec.ts b/src/app/models/__tests__/user.server.model.spec.ts index 1c6e83c2d4..24f2b0350b 100644 --- a/src/app/models/__tests__/user.server.model.spec.ts +++ b/src/app/models/__tests__/user.server.model.spec.ts @@ -175,9 +175,7 @@ describe('User Model', () => { agency: agency._id, email: VALID_USER_EMAIL, }) - const populatedUser = await user - .populate({ path: 'agency' }) - .execPopulate() + const populatedUser = await user.populate({ path: 'agency' }) expect(populatedUser).toBeDefined() // Act diff --git a/src/app/models/admin_verification.server.model.ts b/src/app/models/admin_verification.server.model.ts index e6ff3e895c..7d5d0133ef 100644 --- a/src/app/models/admin_verification.server.model.ts +++ b/src/app/models/admin_verification.server.model.ts @@ -20,7 +20,7 @@ const AdminVerificationSchema = new Schema< admin: { type: Schema.Types.ObjectId, ref: USER_SCHEMA_ID, - required: 'AdminVerificationSchema must have an Admin', + required: [true, 'AdminVerificationSchema must have an Admin'], }, hashedContact: { type: String, diff --git a/src/app/models/agency.server.model.ts b/src/app/models/agency.server.model.ts index 8d159bf3ed..8061651678 100644 --- a/src/app/models/agency.server.model.ts +++ b/src/app/models/agency.server.model.ts @@ -2,11 +2,7 @@ import { pick } from 'lodash' import { Mongoose, Schema } from 'mongoose' import { PublicAgencyDto } from '../../../shared/types' -import { - AgencyInstanceMethods, - IAgencyDocument, - IAgencyModel, -} from '../../types' +import { AgencyInstanceMethods, IAgencyModel, IAgencySchema } from '../../types' export const AGENCY_SCHEMA_ID = 'Agency' @@ -19,9 +15,8 @@ export const AGENCY_PUBLIC_FIELDS = [ ] const AgencySchema = new Schema< - IAgencyDocument, + IAgencySchema, IAgencyModel, - undefined, AgencyInstanceMethods >( { @@ -65,7 +60,7 @@ AgencySchema.methods.getPublicView = function (): PublicAgencyDto { } const compileAgencyModel = (db: Mongoose): IAgencyModel => { - return db.model(AGENCY_SCHEMA_ID, AgencySchema) + return db.model(AGENCY_SCHEMA_ID, AgencySchema) as IAgencyModel } /** diff --git a/src/app/models/form.server.model.ts b/src/app/models/form.server.model.ts index fb1b76b921..fe8306d25e 100644 --- a/src/app/models/form.server.model.ts +++ b/src/app/models/form.server.model.ts @@ -1,12 +1,6 @@ -import BSON, { ObjectId } from 'bson-ext' +import { calculateObjectSize } from 'bson-ext' import { compact, omit, pick, uniq } from 'lodash' -import mongoose, { - Mongoose, - Query, - Schema, - SchemaOptions, - Types, -} from 'mongoose' +import mongoose, { Mongoose, Schema, SchemaOptions, Types } from 'mongoose' import validator from 'validator' import { @@ -93,23 +87,6 @@ import getUserModel from './user.server.model' export const FORM_SCHEMA_ID = 'Form' -const bson = new BSON([ - BSON.Binary, - BSON.Code, - BSON.DBRef, - BSON.Decimal128, - BSON.Double, - BSON.Int32, - BSON.Long, - BSON.Map, - BSON.MaxKey, - BSON.MinKey, - BSON.ObjectId, - BSON.BSONRegExp, - BSON.Symbol, - BSON.Timestamp, -]) - const formSchemaOptions: SchemaOptions = { id: false, toJSON: { @@ -132,15 +109,10 @@ const EncryptedFormSchema = new Schema({ const EmailFormSchema = new Schema({ emails: { - type: [ - { - type: String, - trim: true, - }, - ], + type: [{ type: String, trim: true }], set: transformEmails, validate: { - validator: (v: string[]) => { + validator: (v: unknown) => { if (!Array.isArray(v)) return false if (v.length === 0) return false return v.every((email) => validator.isEmail(email)) @@ -162,11 +134,13 @@ const compileFormModel = (db: Mongoose): IFormModel => { { title: { type: String, - validate: [ - /^[a-zA-Z0-9_\-./() &`;'"]*$/, - 'Form name cannot contain special characters', - ], - required: 'Form name cannot be blank', + validate: { + validator: (v: string) => { + return /^[a-zA-Z0-9_\-./() &`;'"]*$/.test(v) + }, + message: 'Form name cannot contain special characters', + }, + required: [true, 'Form name cannot be blank'], minlength: [4, 'Form name must be at least 4 characters'], maxlength: [200, 'Form name can have a maximum of 200 characters'], // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -225,8 +199,10 @@ const compileFormModel = (db: Mongoose): IFormModel => { const { field, state, - }: { field: ObjectId | string; state: LogicConditionState } = - condition + }: { + field: Types.ObjectId | string + state: LogicConditionState + } = condition return { state, fieldType: this.form_fields?.find( @@ -359,6 +335,16 @@ const compileFormModel = (db: Mongoose): IFormModel => { }, }, + // This must be before `status` since `status` has setters reliant on + // whether esrvcId is available, and mongoose@v6 now saves objects with keys + // in the order the keys are specifified in the schema instead of the object. + // See https://mongoosejs.com/docs/migrating_to_6.html#schema-defined-document-key-order. + esrvcId: { + type: String, + required: false, + validate: [/^\S*$/i, 'e-service ID must not contain whitespace'], + }, + status: { type: String, enum: Object.values(FormStatus), @@ -389,11 +375,6 @@ const compileFormModel = (db: Mongoose): IFormModel => { type: Boolean, default: true, }, - esrvcId: { - type: String, - required: false, - validate: [/^\S*$/i, 'e-service ID must not contain whitespace'], - }, webhook: { // TODO: URL validation, encrypt mode validation @@ -433,6 +414,17 @@ const compileFormModel = (db: Mongoose): IFormModel => { ) as Schema.Types.DocumentArray const TableFieldSchema = createTableFieldSchema() + const TableColumnPath = TableFieldSchema.path( + 'columns', + ) as Schema.Types.DocumentArray + TableColumnPath.discriminator( + BasicField.ShortText, + createShortTextFieldSchema(), + ) + TableColumnPath.discriminator( + BasicField.Dropdown, + createDropdownFieldSchema(), + ) FormFieldPath.discriminator(BasicField.Email, createEmailFieldSchema()) FormFieldPath.discriminator(BasicField.Rating, createRatingFieldSchema()) @@ -463,17 +455,6 @@ const compileFormModel = (db: Mongoose): IFormModel => { ) FormFieldPath.discriminator(BasicField.Section, createSectionFieldSchema()) FormFieldPath.discriminator(BasicField.Table, TableFieldSchema) - const TableColumnPath = TableFieldSchema.path( - 'columns', - ) as Schema.Types.DocumentArray - TableColumnPath.discriminator( - BasicField.ShortText, - createShortTextFieldSchema(), - ) - TableColumnPath.discriminator( - BasicField.Dropdown, - createDropdownFieldSchema(), - ) // Discriminator defines all possible values of startPage.logo const StartPageLogoPath = FormSchema.path( @@ -679,12 +660,17 @@ const compileFormModel = (db: Mongoose): IFormModel => { formId: string, fields?: (keyof IPopulatedForm)[], ): Promise { - return this.findById(formId, fields).populate({ - path: 'admin', - populate: { - path: 'agency', - }, - }) as Query + return ( + // @ts-expect-error Type instantiation excessively deep, mongoose type bug. + this.findById(formId, fields) + .populate({ + path: 'admin', + populate: { + path: 'agency', + }, + }) + .exec() as Promise + ) } // Deactivate form by ID @@ -734,7 +720,7 @@ const compileFormModel = (db: Mongoose): IFormModel => { logicId: string, ): Promise { return this.findByIdAndUpdate( - mongoose.Types.ObjectId(formId), + formId, { $pull: { form_logics: { _id: logicId } }, }, @@ -860,7 +846,7 @@ const compileFormModel = (db: Mongoose): IFormModel => { // Hooks FormSchema.pre('validate', function (next) { // Reject save if form document is too large - if (bson.calculateObjectSize(this) > 10 * MB) { + if (calculateObjectSize(this) > 10 * MB) { const err = new Error('Form size exceeded.') err.name = 'FormSizeError' return next(err) diff --git a/src/app/models/form_logic.server.schema.ts b/src/app/models/form_logic.server.schema.ts index b50d7df861..e505700626 100644 --- a/src/app/models/form_logic.server.schema.ts +++ b/src/app/models/form_logic.server.schema.ts @@ -26,7 +26,8 @@ const LogicConditionSchema = new Schema({ enum: Object.values(LogicConditionState), }, value: { - type: Schema.Types.Mixed, + // Bug where Mixed is not an `any` type, resulting in TypeScript errors. + type: Schema.Types.Mixed as any, required: true, }, ifValueType: { diff --git a/src/app/models/form_statistics_total.server.model.ts b/src/app/models/form_statistics_total.server.model.ts index 76aa6962c7..20dec2c88b 100644 --- a/src/app/models/form_statistics_total.server.model.ts +++ b/src/app/models/form_statistics_total.server.model.ts @@ -53,7 +53,7 @@ const compileFormStatisticsTotalModel = (db: Mongoose) => { FormStatisticsTotalSchema.statics.aggregateFormCount = function ( minSubCount: number, ): Promise { - return this.aggregate([ + return this.aggregate<{ numActiveForms: number }>([ { $match: { totalCount: { @@ -64,7 +64,7 @@ const compileFormStatisticsTotalModel = (db: Mongoose) => { { $count: 'numActiveForms', }, - ]).exec() + ]).exec() as Promise } const FormStatisticsTotalModel = db.model< diff --git a/src/app/models/submission.server.model.ts b/src/app/models/submission.server.model.ts index db42913419..e42e5c536a 100644 --- a/src/app/models/submission.server.model.ts +++ b/src/app/models/submission.server.model.ts @@ -299,6 +299,7 @@ EncryptSubmissionSchema.statics.findAllMetadataByFormId = function ( .match({ // Casting to ObjectId as Mongoose does not cast pipeline stages. // See https://mongoosejs.com/docs/api.html#aggregate_Aggregate. + // @ts-expect-error Type error in definitions, see https://github.com/Automattic/mongoose/issues/10960. form: mongoose.Types.ObjectId(formId), submissionType: SubmissionType.Encrypt, }) diff --git a/src/app/models/user.server.model.ts b/src/app/models/user.server.model.ts index a6b063910f..9920e6127a 100644 --- a/src/app/models/user.server.model.ts +++ b/src/app/models/user.server.model.ts @@ -1,5 +1,5 @@ import { parsePhoneNumberFromString } from 'libphonenumber-js/mobile' -import { MongoError } from 'mongodb' +import { MongoServerError } from 'mongodb' import { CallbackError, Mongoose, Schema } from 'mongoose' import validator from 'validator' @@ -22,11 +22,9 @@ const compileUserModel = (db: Mongoose) => { { email: { type: String, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore trim: true, unique: true, - required: 'Please enter your email', + required: [true, 'Please enter your email'], validate: { // Check if email entered exists in the Agency collection validator: async (value: string) => { @@ -94,7 +92,11 @@ const compileUserModel = (db: Mongoose) => { 'save', function (err: Error, _doc: unknown, next: (err?: CallbackError) => void) { if (err) { - if (err.name === 'MongoError' && (err as MongoError)?.code === 11000) { + if ( + // https://mongoosejs.com/docs/migrating_to_6.html#mongoerror-is-now-mongoservererror + (err.name === 'MongoError' || err.name === 'MongoServerError') && + (err as MongoServerError)?.code === 11000 + ) { next(new Error('Account already exists with this email')) } else { next(err) diff --git a/src/app/modules/auth/auth.service.ts b/src/app/modules/auth/auth.service.ts index 197b9d5a70..901445f0da 100644 --- a/src/app/modules/auth/auth.service.ts +++ b/src/app/modules/auth/auth.service.ts @@ -55,7 +55,7 @@ export const validateEmailDomain = ( const emailDomain = email.split('@').pop() return ResultAsync.fromPromise( - AgencyModel.findOne({ emailDomain }).exec(), + AgencyModel.findOne({ emailDomain }).exec() as Promise, (error) => { logger.error({ message: 'Database error when retrieving agency', diff --git a/src/app/modules/bounce/__tests__/bounce.controller.spec.ts b/src/app/modules/bounce/__tests__/bounce.controller.spec.ts index e835515e2c..7f66a62e62 100644 --- a/src/app/modules/bounce/__tests__/bounce.controller.spec.ts +++ b/src/app/modules/bounce/__tests__/bounce.controller.spec.ts @@ -90,9 +90,7 @@ describe('handleSns', () => { _id: bounceDoc.formId, admin: user._id, title: 'mockTitle', - }) - .populate('admin') - .execPopulate()) as IPopulatedForm + }).populate('admin')) as IPopulatedForm }) afterAll(async () => await dbHandler.closeDatabase()) diff --git a/src/app/modules/bounce/__tests__/bounce.model.spec.ts b/src/app/modules/bounce/__tests__/bounce.model.spec.ts index ca99883323..f596a9ada5 100644 --- a/src/app/modules/bounce/__tests__/bounce.model.spec.ts +++ b/src/app/modules/bounce/__tests__/bounce.model.spec.ts @@ -36,7 +36,7 @@ describe('Bounce Model', () => { const savedBounceObject = extractBounceObject(savedBounce) expect(savedBounce._id).toBeDefined() expect(savedBounce.expireAt).toBeInstanceOf(Date) - expect(omit(savedBounceObject, 'expireAt')).toEqual({ + expect(omit(savedBounceObject, 'expireAt')).toMatchObject({ formId, bounces: [{ email: MOCK_EMAIL, hasBounced: false }], hasAutoEmailed: false, @@ -56,7 +56,7 @@ describe('Bounce Model', () => { } const savedBounce = await new Bounce(params).save() const savedBounceObject = extractBounceObject(savedBounce) - expect(savedBounceObject).toEqual(params) + expect(savedBounceObject).toMatchObject(params) }) it('should reject emails when they are invalid', async () => { @@ -195,7 +195,7 @@ describe('Bounce Model', () => { { email: MOCK_EMAIL_2, hasBounced: false }, ], }) - expect(bounce.getEmails()).toEqual([MOCK_EMAIL, MOCK_EMAIL_2]) + expect(bounce.getEmails()).toMatchObject([MOCK_EMAIL, MOCK_EMAIL_2]) }) it('should return the full email list when hasBounced is true for all', () => { @@ -206,7 +206,7 @@ describe('Bounce Model', () => { { email: MOCK_EMAIL_2, hasBounced: true, bounceType: 'Transient' }, ], }) - expect(bounce.getEmails()).toEqual([MOCK_EMAIL, MOCK_EMAIL_2]) + expect(bounce.getEmails()).toMatchObject([MOCK_EMAIL, MOCK_EMAIL_2]) }) it('should return the full email list when hasBounced is mixed', () => { @@ -217,7 +217,7 @@ describe('Bounce Model', () => { { email: MOCK_EMAIL_2, hasBounced: true, bounceType: 'Transient' }, ], }) - expect(bounce.getEmails()).toEqual([MOCK_EMAIL, MOCK_EMAIL_2]) + expect(bounce.getEmails()).toMatchObject([MOCK_EMAIL, MOCK_EMAIL_2]) }) }) @@ -318,7 +318,7 @@ describe('Bounce Model', () => { bounceType: BounceType.Transient, }) const updated = bounce.updateBounceInfo(snsInfo) - expect(pick(updated.toObject(), ['formId', 'bounces'])).toEqual({ + expect(pick(updated.toObject(), ['formId', 'bounces'])).toMatchObject({ formId, bounces: [ { email: MOCK_EMAIL, hasBounced: true, bounceType: 'Transient' }, @@ -339,7 +339,7 @@ describe('Bounce Model', () => { bounceType: BounceType.Permanent, }) const updated = bounce.updateBounceInfo(snsInfo) - expect(pick(updated.toObject(), ['formId', 'bounces'])).toEqual({ + expect(pick(updated.toObject(), ['formId', 'bounces'])).toMatchObject({ formId, bounces: [ { email: MOCK_EMAIL, hasBounced: true, bounceType: 'Permanent' }, @@ -360,7 +360,7 @@ describe('Bounce Model', () => { bounceType: BounceType.Permanent, }) const updated = bounce.updateBounceInfo(snsInfo) - expect(pick(updated.toObject(), ['formId', 'bounces'])).toEqual({ + expect(pick(updated.toObject(), ['formId', 'bounces'])).toMatchObject({ formId, bounces: [ { email: MOCK_EMAIL, hasBounced: false }, @@ -384,7 +384,7 @@ describe('Bounce Model', () => { bounceType: BounceType.Permanent, }) const updated = bounce.updateBounceInfo(snsInfo) - expect(pick(updated.toObject(), ['formId', 'bounces'])).toEqual({ + expect(pick(updated.toObject(), ['formId', 'bounces'])).toMatchObject({ formId, bounces: [ { email: MOCK_EMAIL, hasBounced: true, bounceType: 'Transient' }, @@ -406,7 +406,7 @@ describe('Bounce Model', () => { bounceType: BounceType.Permanent, }) const updated = bounce.updateBounceInfo(snsInfo) - expect(pick(updated.toObject(), ['formId', 'bounces'])).toEqual({ + expect(pick(updated.toObject(), ['formId', 'bounces'])).toMatchObject({ formId, bounces: [ { email: MOCK_EMAIL, hasBounced: false }, @@ -429,7 +429,7 @@ describe('Bounce Model', () => { bounceType: BounceType.Permanent, }) const updated = bounce.updateBounceInfo(snsInfo) - expect(pick(updated.toObject(), ['formId', 'bounces'])).toEqual({ + expect(pick(updated.toObject(), ['formId', 'bounces'])).toMatchObject({ formId, bounces: [ { email: MOCK_EMAIL, hasBounced: true, bounceType: 'Permanent' }, @@ -452,7 +452,7 @@ describe('Bounce Model', () => { deliveredList: [MOCK_EMAIL], }) const updated = bounce.updateBounceInfo(snsInfo) - expect(pick(updated.toObject(), ['formId', 'bounces'])).toEqual({ + expect(pick(updated.toObject(), ['formId', 'bounces'])).toMatchObject({ formId, bounces: [{ email: MOCK_EMAIL, hasBounced: false }], }) @@ -470,7 +470,7 @@ describe('Bounce Model', () => { deliveredList: [MOCK_EMAIL], }) const updated = bounce.updateBounceInfo(snsInfo) - expect(pick(updated.toObject(), ['formId', 'bounces'])).toEqual({ + expect(pick(updated.toObject(), ['formId', 'bounces'])).toMatchObject({ formId, bounces: [{ email: MOCK_EMAIL, hasBounced: false }], }) @@ -488,7 +488,7 @@ describe('Bounce Model', () => { deliveredList: [MOCK_EMAIL_2], }) const updated = bounce.updateBounceInfo(snsInfo) - expect(pick(updated.toObject(), ['formId', 'bounces'])).toEqual({ + expect(pick(updated.toObject(), ['formId', 'bounces'])).toMatchObject({ formId, bounces: [ { email: MOCK_EMAIL, hasBounced: false }, @@ -511,7 +511,7 @@ describe('Bounce Model', () => { deliveredList: [MOCK_EMAIL_2], }) const updated = bounce.updateBounceInfo(snsInfo) - expect(pick(updated.toObject(), ['formId', 'bounces'])).toEqual({ + expect(pick(updated.toObject(), ['formId', 'bounces'])).toMatchObject({ formId, bounces: [ { email: MOCK_EMAIL, hasBounced: true, bounceType: 'Transient' }, @@ -532,7 +532,7 @@ describe('Bounce Model', () => { deliveredList: [MOCK_EMAIL_2], }) const updated = bounce.updateBounceInfo(snsInfo) - expect(pick(updated.toObject(), ['formId', 'bounces'])).toEqual({ + expect(pick(updated.toObject(), ['formId', 'bounces'])).toMatchObject({ formId, bounces: [ { email: MOCK_EMAIL, hasBounced: false }, @@ -553,7 +553,7 @@ describe('Bounce Model', () => { deliveredList: [MOCK_EMAIL_2, MOCK_EMAIL_2], }) const updated = bounce.updateBounceInfo(snsInfo) - expect(pick(updated.toObject(), ['formId', 'bounces'])).toEqual({ + expect(pick(updated.toObject(), ['formId', 'bounces'])).toMatchObject({ formId, bounces: [ { email: MOCK_EMAIL, hasBounced: false }, @@ -574,7 +574,7 @@ describe('Bounce Model', () => { deliveredList: [MOCK_EMAIL_2], }) const updated = bounce.updateBounceInfo(snsInfo) - expect(pick(updated.toObject(), ['formId', 'bounces'])).toEqual({ + expect(pick(updated.toObject(), ['formId', 'bounces'])).toMatchObject({ formId, bounces: [{ email: MOCK_EMAIL_2, hasBounced: false }], }) @@ -595,7 +595,7 @@ describe('Bounce Model', () => { }) const actual = Bounce.fromSnsNotification(notification, String(formId)) - expect(omit(extractBounceObject(actual!), 'expireAt')).toEqual({ + expect(omit(extractBounceObject(actual!), 'expireAt')).toMatchObject({ formId, bounces: [{ email: MOCK_EMAIL, hasBounced: false }], hasAutoEmailed: false, @@ -616,7 +616,7 @@ describe('Bounce Model', () => { }) const actual = Bounce.fromSnsNotification(notification, String(formId)) - expect(omit(extractBounceObject(actual!), 'expireAt')).toEqual({ + expect(omit(extractBounceObject(actual!), 'expireAt')).toMatchObject({ formId, bounces: [ { email: MOCK_EMAIL, hasBounced: true, bounceType: 'Transient' }, @@ -639,7 +639,7 @@ describe('Bounce Model', () => { }) const actual = Bounce.fromSnsNotification(notification, String(formId)) - expect(omit(extractBounceObject(actual!), 'expireAt')).toEqual({ + expect(omit(extractBounceObject(actual!), 'expireAt')).toMatchObject({ formId, bounces: [ { email: MOCK_EMAIL, hasBounced: true, bounceType: 'Permanent' }, @@ -662,7 +662,7 @@ describe('Bounce Model', () => { }) const actual = Bounce.fromSnsNotification(notification, String(formId)) - expect(omit(extractBounceObject(actual!), 'expireAt')).toEqual({ + expect(omit(extractBounceObject(actual!), 'expireAt')).toMatchObject({ formId, bounces: [ { email: MOCK_EMAIL, hasBounced: true, bounceType: 'Permanent' }, diff --git a/src/app/modules/bounce/__tests__/bounce.service.spec.ts b/src/app/modules/bounce/__tests__/bounce.service.spec.ts index 845fbb42a0..dc577b5344 100644 --- a/src/app/modules/bounce/__tests__/bounce.service.spec.ts +++ b/src/app/modules/bounce/__tests__/bounce.service.spec.ts @@ -152,7 +152,7 @@ describe('BounceService', () => { 'bounces', 'hasAutoEmailed', ]) - expect(actual).toEqual({ + expect(actual).toMatchObject({ formId: MOCK_FORM_ID, bounces: [], hasAutoEmailed: false, @@ -288,9 +288,7 @@ describe('BounceService', () => { const form = (await new Form({ admin: testUser._id, title: MOCK_FORM_TITLE, - }) - .populate('admin') - .execPopulate()) as IPopulatedForm + }).populate('admin')) as IPopulatedForm const bounceDoc = new Bounce({ formId: form._id, bounces: [ @@ -317,9 +315,7 @@ describe('BounceService', () => { admin: testUser._id, title: MOCK_FORM_TITLE, permissionList: [{ email: collabEmail, write: true }], - }) - .populate('admin') - .execPopulate()) as IPopulatedForm + }).populate('admin')) as IPopulatedForm const bounceDoc = new Bounce({ formId: form._id, bounces: [ @@ -344,9 +340,7 @@ describe('BounceService', () => { const form = (await new Form({ admin: testUser._id, title: MOCK_FORM_TITLE, - }) - .populate('admin') - .execPopulate()) as IPopulatedForm + }).populate('admin')) as IPopulatedForm const bounceDoc = new Bounce({ formId: form._id, bounces: [ @@ -367,9 +361,7 @@ describe('BounceService', () => { admin: testUser._id, title: MOCK_FORM_TITLE, permissionList: [{ email: collabEmail, write: false }], - }) - .populate('admin') - .execPopulate()) as IPopulatedForm + }).populate('admin')) as IPopulatedForm const bounceDoc = new Bounce({ formId: form._id, bounces: [ @@ -405,9 +397,7 @@ describe('BounceService', () => { const form = (await new Form({ admin: testUser._id, title: MOCK_FORM_TITLE, - }) - .populate('admin') - .execPopulate()) as IPopulatedForm + }).populate('admin')) as IPopulatedForm const bounceDoc = new Bounce({ formId: form._id, bounces: [], @@ -447,9 +437,7 @@ describe('BounceService', () => { const form = (await new Form({ admin: testUser._id, title: MOCK_FORM_TITLE, - }) - .populate('admin') - .execPopulate()) as IPopulatedForm + }).populate('admin')) as IPopulatedForm const bounceDoc = new Bounce({ formId: form._id, bounces: [], @@ -814,9 +802,7 @@ describe('BounceService', () => { { email: MOCK_EMAIL, write: true }, { email: MOCK_EMAIL_2, write: false }, ], - }) - .populate('admin') - .execPopulate()) as IPopulatedForm + }).populate('admin')) as IPopulatedForm MockUserService.findContactsForEmails.mockReturnValueOnce( okAsync([MOCK_CONTACT]), ) @@ -838,9 +824,7 @@ describe('BounceService', () => { { email: MOCK_EMAIL, write: true }, { email: MOCK_EMAIL_2, write: false }, ], - }) - .populate('admin') - .execPopulate()) as IPopulatedForm + }).populate('admin')) as IPopulatedForm MockUserService.findContactsForEmails.mockReturnValueOnce( okAsync([omit(MOCK_CONTACT, 'contact'), MOCK_CONTACT_2]), ) @@ -862,9 +846,7 @@ describe('BounceService', () => { { email: MOCK_EMAIL, write: true }, { email: MOCK_EMAIL_2, write: false }, ], - }) - .populate('admin') - .execPopulate()) as IPopulatedForm + }).populate('admin')) as IPopulatedForm MockUserService.findContactsForEmails.mockReturnValueOnce( errAsync(new DatabaseError()), ) @@ -898,9 +880,7 @@ describe('BounceService', () => { const form = (await new Form({ admin: testUser._id, title: MOCK_FORM_TITLE, - }) - .populate('admin') - .execPopulate()) as IPopulatedForm + }).populate('admin')) as IPopulatedForm MockSmsFactory.sendFormDeactivatedSms.mockReturnValue(okAsync(true)) const result = await BounceService.notifyAdminsOfDeactivation(form, [ @@ -932,9 +912,7 @@ describe('BounceService', () => { const form = (await new Form({ admin: testUser._id, title: MOCK_FORM_TITLE, - }) - .populate('admin') - .execPopulate()) as IPopulatedForm + }).populate('admin')) as IPopulatedForm MockSmsFactory.sendFormDeactivatedSms .mockReturnValueOnce(okAsync(true)) .mockReturnValueOnce(errAsync(new SmsSendError())) diff --git a/src/app/modules/examples/__tests__/examples.routes.spec.ts b/src/app/modules/examples/__tests__/examples.routes.spec.ts index 13f9a1a4f4..b8f8b71cd1 100644 --- a/src/app/modules/examples/__tests__/examples.routes.spec.ts +++ b/src/app/modules/examples/__tests__/examples.routes.spec.ts @@ -1,5 +1,6 @@ import { ObjectId } from 'bson-ext' import { keyBy } from 'lodash' +import MockDate from 'mockdate' import { errAsync } from 'neverthrow' import supertest, { Session } from 'supertest-session' @@ -9,11 +10,11 @@ import { createAuthedSession } from 'tests/integration/helpers/express-auth' import { setupApp } from 'tests/integration/helpers/express-setup' import { buildCelebrateError } from 'tests/unit/backend/helpers/celebrate' import dbHandler from 'tests/unit/backend/helpers/jest-db' +import { jsonParseStringify } from 'tests/unit/backend/helpers/serialize-data' import { DatabaseError } from '../../core/core.errors' import { ExamplesRouter } from '../examples.routes' import * as ExamplesService from '../examples.service' -import { FormInfo } from '../examples.types' import prepareTestData, { SearchTerm, @@ -39,6 +40,8 @@ describe('examples.routes', () => { let orgAgency: IAgencySchema beforeAll(async () => { + // Mockdate requires for deterministic return of example objects. + MockDate.set(new Date()) await dbHandler.connect() const orgData = await dbHandler.insertFormCollectionReqs({ mailDomain: 'example.org', @@ -63,7 +66,10 @@ describe('examples.routes', () => { afterEach(async () => { jest.clearAllMocks() }) - afterAll(async () => await dbHandler.closeDatabase()) + afterAll(async () => { + MockDate.reset() + await dbHandler.closeDatabase() + }) describe('GET /examples', () => { it('should return 200 with array of example forms for all agencies when query.agency is missing', async () => { @@ -80,8 +86,8 @@ describe('examples.routes', () => { // agencies. const expectedBody = keyBy( [ - ...stringifyFormInfoArray(comTestData.total.expectedFormInfo), - ...stringifyFormInfoArray(orgTestData.total.expectedFormInfo), + ...jsonParseStringify(comTestData.total.expectedFormInfo), + ...jsonParseStringify(orgTestData.total.expectedFormInfo), ], '_id', ) @@ -107,7 +113,7 @@ describe('examples.routes', () => { // Assert // Should only have orgTestData since its agency id is provided. const expectedBody = keyBy( - stringifyFormInfoArray(orgTestData.total.expectedFormInfo), + jsonParseStringify(orgTestData.total.expectedFormInfo), '_id', ) const actualBody = keyBy(response.body.forms, '_id') @@ -152,7 +158,7 @@ describe('examples.routes', () => { // Assert // Should only have comTestData since its agency id is provided. const expectedBody = keyBy( - stringifyFormInfoArray(comTestData.first.expectedFormInfo), + jsonParseStringify(comTestData.first.expectedFormInfo), '_id', ) const actualBody = keyBy(response.body.forms, '_id') @@ -205,7 +211,7 @@ describe('examples.routes', () => { // Assert // Should only have comTestData since its agency id is provided. const expectedBody = keyBy( - stringifyFormInfoArray(comTestData.total.expectedFormInfo), + jsonParseStringify(comTestData.total.expectedFormInfo), '_id', ) const actualBody = keyBy(response.body.forms, '_id') @@ -347,7 +353,7 @@ describe('examples.routes', () => { const response = await session.get(`/examples/${validFormId}`) // Assert - const expectedFormInfo = stringifyFormInfo( + const expectedFormInfo = jsonParseStringify( comTestData.second.expectedFormInfo[0], ) @@ -402,20 +408,3 @@ describe('examples.routes', () => { }) }) }) - -// Helper functions -/** - * Stringifies expected form info arrays. Unable to do the usual JSON.stringify - * -> parse combination due to mongoose dates being converted to an empty - * object. - */ -const stringifyFormInfoArray = (array: FormInfo[]) => { - return array.map(stringifyFormInfo) -} - -const stringifyFormInfo = (formInfo: FormInfo) => { - return { - ...formInfo, - _id: formInfo._id.toString(), - } -} diff --git a/src/app/modules/examples/__tests__/examples.service.spec.ts b/src/app/modules/examples/__tests__/examples.service.spec.ts index 35b7783009..1cecb9d667 100644 --- a/src/app/modules/examples/__tests__/examples.service.spec.ts +++ b/src/app/modules/examples/__tests__/examples.service.spec.ts @@ -1,13 +1,16 @@ import { ObjectId } from 'bson-ext' +import MockDate from 'mockdate' import mongoose from 'mongoose' import getFormStatisticsTotalModel from 'src/app/models/form_statistics_total.server.model' import dbHandler from 'tests/unit/backend/helpers/jest-db' +import { jsonParseStringify } from 'tests/unit/backend/helpers/serialize-data' import { PAGE_SIZE } from '../examples.constants' import { ResultsNotFoundError } from '../examples.errors' import * as ExamplesService from '../examples.service' +import { QueryPageResultWithTotal } from '../examples.types' import prepareTestData, { TestData } from './helpers/prepareTestData' @@ -24,11 +27,15 @@ describe('examples.service', () => { let testData: TestData beforeAll(async () => { + MockDate.set(new Date()) await dbHandler.connect() const { user, agency } = await dbHandler.insertFormCollectionReqs() testData = await prepareTestData(user, agency) }) - afterAll(async () => await dbHandler.closeDatabase()) + afterAll(async () => { + MockDate.reset() + await dbHandler.closeDatabase() + }) describe('getExampleForms', () => { describe('when query.searchTerm exists', () => { @@ -42,11 +49,19 @@ describe('examples.service', () => { }) // Assert - expect(actualResults.isOk()).toEqual(true) - expect(actualResults._unsafeUnwrap()).toEqual({ - totalNumResults: testData.second.formCount, - forms: expect.arrayContaining(testData.second.expectedFormInfo), - }) + const actualParsed = jsonParseStringify( + actualResults._unsafeUnwrap() as QueryPageResultWithTotal, + ) + const actualFormsSorted = actualParsed.forms.sort((a, b) => + String(a._id).localeCompare(String(b._id)), + ) + const expectedFormsSorted = jsonParseStringify( + testData.second.expectedFormInfo, + ).sort((a, b) => String(a._id).localeCompare(String(b._id))) + expect(actualParsed.totalNumResults).toEqual( + testData.second.formCount, + ) + expect(actualFormsSorted).toEqual(expectedFormsSorted) }) it('should return empty list if no forms match search term with 0 result count', async () => { @@ -76,10 +91,14 @@ describe('examples.service', () => { }) // Assert - expect(actualResults.isOk()).toEqual(true) - expect(actualResults._unsafeUnwrap()).toEqual({ - forms: expect.arrayContaining(testData.first.expectedFormInfo), - }) + const actualParsed = jsonParseStringify(actualResults._unsafeUnwrap()) + const actualFormsSorted = actualParsed.forms.sort((a, b) => + String(a._id).localeCompare(String(b._id)), + ) + const expectedFormsSorted = jsonParseStringify( + testData.first.expectedFormInfo, + ).sort((a, b) => String(a._id).localeCompare(String(b._id))) + expect(actualFormsSorted).toEqual(expectedFormsSorted) }) it('should return empty list if no forms match search term', async () => { @@ -109,11 +128,17 @@ describe('examples.service', () => { }) // Assert - expect(actualResults.isOk()).toEqual(true) - expect(actualResults._unsafeUnwrap()).toEqual({ - totalNumResults: testData.total.formCount, - forms: expect.arrayContaining(testData.total.expectedFormInfo), - }) + const actualParsed = jsonParseStringify( + actualResults._unsafeUnwrap() as QueryPageResultWithTotal, + ) + const actualFormsSorted = actualParsed.forms.sort((a, b) => + String(a._id).localeCompare(String(b._id)), + ) + const expectedFormsSorted = jsonParseStringify( + testData.total.expectedFormInfo, + ).sort((a, b) => String(a._id).localeCompare(String(b._id))) + expect(actualParsed.totalNumResults).toEqual(testData.total.formCount) + expect(actualFormsSorted).toEqual(expectedFormsSorted) }) it('should return empty list with number of forms with submissions when offset is more than number of documents in collection', async () => { @@ -144,10 +169,14 @@ describe('examples.service', () => { }) // Assert - expect(actualResults.isOk()).toEqual(true) - expect(actualResults._unsafeUnwrap()).toEqual({ - forms: expect.arrayContaining(testData.total.expectedFormInfo), - }) + const actualParsed = jsonParseStringify(actualResults._unsafeUnwrap()) + const actualFormsSorted = actualParsed.forms.sort((a, b) => + String(a._id).localeCompare(String(b._id)), + ) + const expectedFormsSorted = jsonParseStringify( + testData.total.expectedFormInfo, + ).sort((a, b) => String(a._id).localeCompare(String(b._id))) + expect(actualFormsSorted).toEqual(expectedFormsSorted) }) it('should return empty list when offset is more than number of documents', async () => { @@ -182,8 +211,8 @@ describe('examples.service', () => { // Assert expect(actualResults.isOk()).toEqual(true) - expect(actualResults._unsafeUnwrap()).toEqual({ - form: expectedFormInfo, + expect(jsonParseStringify(actualResults._unsafeUnwrap())).toEqual({ + form: jsonParseStringify(expectedFormInfo), }) }) diff --git a/src/app/modules/examples/__tests__/helpers/prepareTestData.ts b/src/app/modules/examples/__tests__/helpers/prepareTestData.ts index 32b221fda0..f53b4d6d45 100644 --- a/src/app/modules/examples/__tests__/helpers/prepareTestData.ts +++ b/src/app/modules/examples/__tests__/helpers/prepareTestData.ts @@ -209,7 +209,7 @@ const prepareTestData = async ( form_fields: [], logo: agency.logo, timeText: 'less than 1 day ago', - lastSubmission: expect.anything(), + lastSubmission: new Date(), title: form.title, authType: form.authType, })) diff --git a/src/app/modules/examples/examples.queries.ts b/src/app/modules/examples/examples.queries.ts index af8287aee0..1c0892228b 100644 --- a/src/app/modules/examples/examples.queries.ts +++ b/src/app/modules/examples/examples.queries.ts @@ -33,6 +33,7 @@ export const searchFormsWithText = ( export const searchFormsById = (formId: string): Record[] => [ { $match: { + // @ts-expect-error Type error in definitions, see https://github.com/Automattic/mongoose/issues/10960. _id: mongoose.Types.ObjectId(formId), }, }, @@ -107,6 +108,7 @@ export const filterByAgencyId = ( ): Record[] => [ { $match: { + // @ts-expect-error Type error in definitions, see https://github.com/Automattic/mongoose/issues/10960. 'agencyInfo._id': mongoose.Types.ObjectId(agencyId), }, }, @@ -348,6 +350,7 @@ export const searchSubmissionsForForm = ( ): Record[] => [ { $match: { + // @ts-expect-error Type error in definitions, see https://github.com/Automattic/mongoose/issues/10960. [key]: mongoose.Types.ObjectId(formId), }, }, diff --git a/src/app/modules/form/admin-form/__tests__/admin-form.routes.spec.ts b/src/app/modules/form/admin-form/__tests__/admin-form.routes.spec.ts index 602d39e6ff..838ca3f5f3 100644 --- a/src/app/modules/form/admin-form/__tests__/admin-form.routes.spec.ts +++ b/src/app/modules/form/admin-form/__tests__/admin-form.routes.spec.ts @@ -2,6 +2,7 @@ import { ObjectId } from 'bson-ext' import { format, subDays } from 'date-fns' import { cloneDeep, omit, times } from 'lodash' +import MockDate from 'mockdate' import mongoose from 'mongoose' import { errAsync, okAsync } from 'neverthrow' import SparkMD5 from 'spark-md5' @@ -1287,8 +1288,8 @@ describe('admin-form.routes', () => { .lean() expect(response.status).toEqual(200) expect(response.body).not.toBeNull() - expect(response.body).toEqual({ - form: jsonParseStringify(expected), + expect(response.body).toMatchObject({ + form: expected, }) }) @@ -1324,8 +1325,8 @@ describe('admin-form.routes', () => { .lean() expect(response.status).toEqual(200) expect(response.body).not.toBeNull() - expect(response.body).toEqual({ - form: jsonParseStringify(expected), + expect(response.body).toMatchObject({ + form: expected, }) }) @@ -1472,7 +1473,7 @@ describe('admin-form.routes', () => { expect(response.status).toEqual(200) expect(expected?.form_fields![0].description).toEqual(updatedDescription) expect(expected?.__v).toEqual(1) - expect(response.body).toEqual(jsonParseStringify(expected)) + expect(response.body).toMatchObject(expected) }) it('should return 401 when user is not logged in', async () => { @@ -2424,9 +2425,11 @@ describe('admin-form.routes', () => { ) // Assert - const populatedForm = await publicForm - .populate({ path: 'admin', populate: { path: 'agency' } }) - .execPopulate() + const populatedForm = await publicForm.populate({ + path: 'admin', + populate: { path: 'agency' }, + }) + expect(response.status).toEqual(200) expect(response.body).toEqual({ form: jsonParseStringify(populatedForm.getPublicView()), @@ -2935,14 +2938,12 @@ describe('admin-form.routes', () => { // Assert const expectedForm = ( - await formToPreview - .populate({ - path: 'admin', - populate: { - path: 'agency', - }, - }) - .execPopulate() + await formToPreview.populate({ + path: 'admin', + populate: { + path: 'agency', + }, + }) ).getPublicView() expect(response.status).toEqual(200) expect(response.body).toEqual({ @@ -2974,14 +2975,12 @@ describe('admin-form.routes', () => { // Assert const expectedForm = ( - await collabFormToPreview - .populate({ - path: 'admin', - populate: { - path: 'agency', - }, - }) - .execPopulate() + await collabFormToPreview.populate({ + path: 'admin', + populate: { + path: 'agency', + }, + }) ).getPublicView() expect(response.status).toEqual(200) expect(response.body).toEqual({ form: jsonParseStringify(expectedForm) }) @@ -3969,7 +3968,6 @@ describe('admin-form.routes', () => { it('should return 200 with counts of submissions made between given start and end dates.', async () => { // Arrange - const expectedSubmissionCount = 3 const newForm = (await EmailFormModel.create({ title: 'new form', responseMode: FormResponseMode.Email, @@ -3981,16 +3979,15 @@ describe('admin-form.routes', () => { hash: 'some hash', salt: 'some salt', } - const results = await Promise.all( - times(expectedSubmissionCount, () => - saveSubmissionMetadata(newForm, mockSubmissionHash), - ), + // Inconsequential submissions + await Promise.all( + times(2, () => saveSubmissionMetadata(newForm, mockSubmissionHash)), ) - // Update first submission to be 5 days ago. + + // Add submission with specific date. const now = new Date() - const firstSubmission = results[0]._unsafeUnwrap() - firstSubmission.created = subDays(now, 5) - await firstSubmission.save() + MockDate.set(subDays(now, 5)) + await saveSubmissionMetadata(newForm, mockSubmissionHash) // Act const response = await request @@ -4002,12 +3999,14 @@ describe('admin-form.routes', () => { // Assert expect(response.status).toEqual(200) + // Should have a single submission that fits the date range expect(response.body).toEqual(1) + + MockDate.reset() }) it('should return 200 with counts of submissions made with same start and end dates', async () => { // Arrange - const expectedSubmissionCount = 3 const newForm = (await EmailFormModel.create({ title: 'new form', responseMode: FormResponseMode.Email, @@ -4019,15 +4018,15 @@ describe('admin-form.routes', () => { hash: 'some hash', salt: 'some salt', } - const results = await Promise.all( - times(expectedSubmissionCount, () => - saveSubmissionMetadata(newForm, mockSubmissionHash), - ), + + // Save two inconsequential submissions. + await Promise.all( + times(2, () => saveSubmissionMetadata(newForm, mockSubmissionHash)), ) + // Save a submission with a specific expected date. const expectedDate = '2021-04-04' - const firstSubmission = results[0]._unsafeUnwrap() - firstSubmission.created = new Date(expectedDate) - await firstSubmission.save() + MockDate.set(expectedDate) + await saveSubmissionMetadata(newForm, mockSubmissionHash) // Act const response = await request @@ -4039,7 +4038,10 @@ describe('admin-form.routes', () => { // Assert expect(response.status).toEqual(200) + // Should only have 1 submission from the specific date. expect(response.body).toEqual(1) + + MockDate.reset() }) it('should return 400 when query.startDate is missing when query.endDate is provided', async () => { @@ -4590,6 +4592,18 @@ describe('admin-form.routes', () => { })) as IFormDocument }) + const createEncryptSubmissionBody = (metaString: unknown) => { + return { + form: defaultForm, + encryptedContent: `any encrypted content ${metaString}`, + verifiedContent: `any verified content ${metaString}`, + attachmentMetadata: new Map([ + ['fieldId1', `some.attachment.url.${metaString}`], + ['fieldId2', `some.other.attachment.url.${metaString}`], + ]), + } + } + it('should return 200 with stream of encrypted responses without attachment URLs when query.downloadAttachments is false', async () => { // Arrange const submissions = await Promise.all( @@ -4714,31 +4728,24 @@ describe('admin-form.routes', () => { it('should return 200 with stream of encrypted responses between given query.startDate and query.endDate', async () => { // Arrange - const submissions = await Promise.all( - times(5, (count) => - createEncryptSubmission({ - form: defaultForm, - encryptedContent: `any encrypted content ${count}`, - verifiedContent: `any verified content ${count}`, - attachmentMetadata: new Map([ - ['fieldId1', `some.attachment.url.${count}`], - ['fieldId2', `some.other.attachment.url.${count}`], - ]), - }), + // Inconsequential submissions + await Promise.all( + times(3, (count) => + createEncryptSubmission(createEncryptSubmissionBody(count)), ), ) const startDateStr = '2020-02-03' const endDateStr = '2020-02-04' + MockDate.set(startDateStr) // Set 2 submissions to be submitted with specific date - submissions[2].created = new Date(startDateStr) - submissions[4].created = new Date(endDateStr) - await submissions[2].save() - await submissions[4].save() - const expectedSubmissionIds = [ - String(submissions[2]._id), - String(submissions[4]._id), - ] + const specificSubmission1 = await createEncryptSubmission( + createEncryptSubmissionBody(startDateStr), + ) + MockDate.set(endDateStr) + const specificSubmission2 = await createEncryptSubmission( + createEncryptSubmissionBody(endDateStr), + ) // Act const response = await request @@ -4757,7 +4764,7 @@ describe('admin-form.routes', () => { }) // Assert - const expectedSorted = submissions + const expectedSorted = [specificSubmission1, specificSubmission2] .map((s) => jsonParseStringify({ _id: s._id, @@ -4770,7 +4777,6 @@ describe('admin-form.routes', () => { version: s.version, }), ) - .filter((s) => expectedSubmissionIds.includes(s._id)) .sort((a, b) => String(a._id).localeCompare(String(b._id))) const actualSorted = (response.body as string) @@ -4783,6 +4789,8 @@ describe('admin-form.routes', () => { expect(response.status).toEqual(200) expect(actualSorted).toEqual(expectedSorted) + + MockDate.reset() }) it('should return 400 when form of given formId is not an encrypt mode form', async () => { diff --git a/src/app/modules/form/admin-form/__tests__/admin-form.service.spec.ts b/src/app/modules/form/admin-form/__tests__/admin-form.service.spec.ts index a02558147c..db77935d91 100644 --- a/src/app/modules/form/admin-form/__tests__/admin-form.service.spec.ts +++ b/src/app/modules/form/admin-form/__tests__/admin-form.service.spec.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { PresignedPost } from 'aws-sdk/clients/s3' -import { ObjectId } from 'bson-ext' import { assignIn, cloneDeep, merge, omit, pick } from 'lodash' import mongoose from 'mongoose' import { err, errAsync, ok, okAsync } from 'neverthrow' @@ -351,8 +350,8 @@ describe('admin-form.service', () => { it('should true when form is successfully archived', async () => { // Arrange const mockArchivedForm = { - _id: new ObjectId(), - admin: new ObjectId(), + _id: new mongoose.Types.ObjectId(), + admin: new mongoose.Types.ObjectId(), status: FormStatus.Archived, } as IEmailFormSchema const mockArchiveFn = jest.fn().mockResolvedValue(mockArchivedForm) @@ -390,10 +389,10 @@ describe('admin-form.service', () => { }) describe('duplicateForm', () => { - const MOCK_NEW_ADMIN_ID = new ObjectId().toHexString() + const MOCK_NEW_ADMIN_ID = new mongoose.Types.ObjectId().toHexString() const MOCK_VALID_FORM = { - _id: new ObjectId(), - admin: new ObjectId(), + _id: new mongoose.Types.ObjectId(), + admin: new mongoose.Types.ObjectId(), endPage: { buttonLink: 'original form endpage link', }, @@ -424,7 +423,7 @@ describe('admin-form.service', () => { it('should successfully duplicate form', async () => { // Arrange - const mockNewAdminId = new ObjectId().toHexString() + const mockNewAdminId = new mongoose.Types.ObjectId().toHexString() const expectedParams: PickDuplicateForm & OverrideProps = { admin: MOCK_NEW_ADMIN_ID, ...MOCK_ENCRYPT_OVERRIDE_PARAMS, @@ -465,7 +464,7 @@ describe('admin-form.service', () => { it('should omit buttonLink if original form link is to the form itself', async () => { // Arrange - const mockNewAdminId = new ObjectId().toHexString() + const mockNewAdminId = new mongoose.Types.ObjectId().toHexString() const expectedParams: PickDuplicateForm & OverrideProps = { admin: MOCK_NEW_ADMIN_ID, ...omit(MOCK_ENCRYPT_OVERRIDE_PARAMS, 'isTemplate'), @@ -514,7 +513,7 @@ describe('admin-form.service', () => { it('should return DatabaseError if error occurred during the duplication', async () => { // Arrange - const mockNewAdminId = new ObjectId().toHexString() + const mockNewAdminId = new mongoose.Types.ObjectId().toHexString() const expectedParams: PickDuplicateForm & OverrideProps = { admin: MOCK_NEW_ADMIN_ID, ...omit(MOCK_ENCRYPT_OVERRIDE_PARAMS, 'isTemplate'), @@ -559,11 +558,11 @@ describe('admin-form.service', () => { describe('transferFormOwnership', () => { const MOCK_NEW_OWNER_EMAIL = 'random@example.com' const MOCK_CURRENT_OWNER = { - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), email: 'someemail@example.com', } as IUserSchema const MOCK_NEW_OWNER = { - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), email: MOCK_NEW_OWNER_EMAIL, } as IUserSchema @@ -574,14 +573,12 @@ describe('admin-form.service', () => { } as IPopulatedForm const mockUpdatedForm = { - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), admin: MOCK_CURRENT_OWNER, emails: [MOCK_NEW_OWNER_EMAIL], responseMode: FormResponseMode.Email, title: 'some mock form', - populate: jest.fn().mockReturnValue({ - execPopulate: jest.fn().mockResolvedValue(expectedPopulateResult), - }), + populate: jest.fn().mockResolvedValue(expectedPopulateResult), } as unknown as IFormSchema const mockValidForm = { @@ -751,17 +748,12 @@ describe('admin-form.service', () => { // Arrange const mockPopulateErrorStr = 'population failed!' const mockUpdatedForm = { - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), admin: MOCK_CURRENT_OWNER, emails: [MOCK_NEW_OWNER_EMAIL], responseMode: FormResponseMode.Email, title: 'some mock form', - populate: jest.fn().mockReturnValue({ - // Mock populate error. - execPopulate: jest - .fn() - .mockRejectedValue(new Error(mockPopulateErrorStr)), - }), + populate: jest.fn().mockRejectedValue(new Error(mockPopulateErrorStr)), } as unknown as IFormSchema const mockValidForm = { @@ -801,12 +793,12 @@ describe('admin-form.service', () => { // Arrange const formParams: Parameters[0] = { title: 'create form title', - admin: new ObjectId().toHexString(), + admin: new mongoose.Types.ObjectId().toHexString(), responseMode: FormResponseMode.Email, emails: 'example@example.com', } const expectedForm = { - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), ...formParams, } as IFormSchema const createSpy = jest @@ -825,7 +817,7 @@ describe('admin-form.service', () => { // Arrange const formParams: Parameters[0] = { title: 'create form title', - admin: new ObjectId().toHexString(), + admin: new mongoose.Types.ObjectId().toHexString(), responseMode: FormResponseMode.Encrypt, publicKey: 'some key', } @@ -849,7 +841,7 @@ describe('admin-form.service', () => { // Arrange const formParams: Parameters[0] = { title: 'create form title', - admin: new ObjectId().toHexString(), + admin: new mongoose.Types.ObjectId().toHexString(), responseMode: FormResponseMode.Encrypt, publicKey: 'some key', } @@ -872,7 +864,7 @@ describe('admin-form.service', () => { // Arrange const formParams: Parameters[0] = { title: 'create form title', - admin: new ObjectId().toHexString(), + admin: new mongoose.Types.ObjectId().toHexString(), responseMode: FormResponseMode.Encrypt, publicKey: 'some key', } @@ -900,7 +892,7 @@ describe('admin-form.service', () => { // Arrange const formParams: Parameters[0] = { title: 'create form title', - admin: new ObjectId().toHexString(), + admin: new mongoose.Types.ObjectId().toHexString(), responseMode: FormResponseMode.Encrypt, publicKey: 'some key', } @@ -922,8 +914,8 @@ describe('admin-form.service', () => { describe('editFormFields', () => { const MOCK_UPDATED_FORM = { - _id: new ObjectId(), - admin: new ObjectId(), + _id: new mongoose.Types.ObjectId(), + admin: new mongoose.Types.ObjectId(), form_fields: [ generateDefaultField(BasicField.Email), generateDefaultField(BasicField.Mobile), @@ -1025,8 +1017,8 @@ describe('admin-form.service', () => { describe('updateForm', () => { const MOCK_UPDATED_FORM = { - _id: new ObjectId(), - admin: new ObjectId(), + _id: new mongoose.Types.ObjectId(), + admin: new mongoose.Types.ObjectId(), status: FormStatus.Private, form_fields: [ generateDefaultField(BasicField.Mobile), @@ -1115,12 +1107,12 @@ describe('admin-form.service', () => { } as unknown as IFormDocument const MOCK_EMAIL_FORM = mocked({ - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), status: FormStatus.Public, responseMode: FormResponseMode.Email, } as unknown as IPopulatedForm) const MOCK_ENCRYPT_FORM = mocked({ - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), status: FormStatus.Public, responseMode: FormResponseMode.Encrypt, } as unknown as IPopulatedForm) @@ -1266,7 +1258,7 @@ describe('admin-form.service', () => { updateFormFieldById: jest.fn().mockResolvedValue(null), } as unknown as IPopulatedForm - const invalidFieldId = new ObjectId().toHexString() + const invalidFieldId = new mongoose.Types.ObjectId().toHexString() const mockNewField = generateDefaultField( BasicField.Number, ) as FieldUpdateDto @@ -1293,7 +1285,7 @@ describe('admin-form.service', () => { ), } as unknown as IPopulatedForm - const invalidFieldId = new ObjectId().toHexString() + const invalidFieldId = new mongoose.Types.ObjectId().toHexString() const mockNewField = generateDefaultField( BasicField.Number, ) as FieldUpdateDto @@ -1377,7 +1369,7 @@ describe('admin-form.service', () => { }) describe('deleteFormLogic', () => { - const logicId = new ObjectId().toHexString() + const logicId = new mongoose.Types.ObjectId().toHexString() const mockFormLogic = { form_logics: [ { @@ -1393,13 +1385,13 @@ describe('admin-form.service', () => { beforeEach(() => { mockEmailForm = { - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), status: FormStatus.Public, responseMode: FormResponseMode.Email, ...mockFormLogic, } as unknown as IPopulatedForm mockEncryptForm = { - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), status: FormStatus.Public, responseMode: FormResponseMode.Encrypt, ...mockFormLogic, @@ -1427,7 +1419,7 @@ describe('admin-form.service', () => { expect(actualResult._unsafeUnwrap()).toEqual(mockEmailForm) expect(UPDATE_SPY).toHaveBeenCalledWith( - mockEmailForm._id, + String(mockEmailForm._id), { $pull: { form_logics: { _id: logicId } }, }, @@ -1464,7 +1456,7 @@ describe('admin-form.service', () => { expect(actualResult._unsafeUnwrap()).toEqual(mockEncryptForm) expect(UPDATE_SPY).toHaveBeenCalledWith( - mockEncryptForm._id, + String(mockEncryptForm._id), { $pull: { form_logics: { _id: logicId } }, }, @@ -1482,7 +1474,7 @@ describe('admin-form.service', () => { it('should return LogicNotFoundError if logic does not exist on form', async () => { // Act - const wrongLogicId = new ObjectId().toHexString() + const wrongLogicId = new mongoose.Types.ObjectId().toHexString() const actualResult = await AdminFormService.deleteFormLogic( mockEmailForm, wrongLogicId, @@ -1508,7 +1500,7 @@ describe('admin-form.service', () => { const mockForm = { title: 'some mock form', form_fields: [fieldToDuplicate], - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), duplicateFormFieldById: jest.fn().mockResolvedValue(mockUpdatedForm), } as unknown as IPopulatedForm @@ -1538,7 +1530,7 @@ describe('admin-form.service', () => { const mockForm = { title: 'some mock form', form_fields: [fieldToDuplicate], - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), duplicateFormFieldById: jest.fn().mockResolvedValue(null), } as unknown as IPopulatedForm @@ -1613,7 +1605,7 @@ describe('admin-form.service', () => { form_fields: [generateDefaultField(BasicField.YesNo)], reorderFormFieldById: jest.fn().mockResolvedValue(null), } as unknown as IPopulatedForm - const fieldToReorder = new ObjectId().toHexString() + const fieldToReorder = new mongoose.Types.ObjectId().toHexString() const newPosition = 2 // Act @@ -1640,7 +1632,7 @@ describe('admin-form.service', () => { .fn() .mockRejectedValue(new Error('some error')), } as unknown as IPopulatedForm - const fieldToReorder = new ObjectId().toHexString() + const fieldToReorder = new mongoose.Types.ObjectId().toHexString() const newPosition = 2 // Act @@ -1720,11 +1712,11 @@ describe('admin-form.service', () => { }) describe('createFormLogic', () => { - const logicId1 = new ObjectId() - const logicId2 = new ObjectId() - const logicId3 = new ObjectId() - const mockEmailFormId = new ObjectId() - const mockEncryptFormId = new ObjectId() + const logicId1 = new mongoose.Types.ObjectId() + const logicId2 = new mongoose.Types.ObjectId() + const logicId3 = new mongoose.Types.ObjectId() + const mockEmailFormId = new mongoose.Types.ObjectId() + const mockEncryptFormId = new mongoose.Types.ObjectId() const mockFormLogicOld = { form_logics: [ @@ -1924,7 +1916,7 @@ describe('admin-form.service', () => { const mockForm = { title: 'some mock form', form_fields: initialFields, - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), } as unknown as IPopulatedForm deleteSpy.mockResolvedValueOnce(mockUpdatedForm) @@ -1947,13 +1939,13 @@ describe('admin-form.service', () => { const mockForm = { title: 'some mock form', form_fields: [generateDefaultField(BasicField.Nric)], - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), } as unknown as IPopulatedForm // Act const actual = await AdminFormService.deleteFormField( mockForm, - new ObjectId().toHexString(), + new mongoose.Types.ObjectId().toHexString(), ) // Assert @@ -1967,7 +1959,7 @@ describe('admin-form.service', () => { const mockForm = { title: 'some mock form', form_fields: [fieldToDelete], - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), } as unknown as IPopulatedForm deleteSpy.mockResolvedValueOnce(null) @@ -1988,7 +1980,7 @@ describe('admin-form.service', () => { describe('updateEndPage', () => { const updateSpy = jest.spyOn(FormModel, 'updateEndPageById') - const MOCK_FORM_ID = new ObjectId().toHexString() + const MOCK_FORM_ID = new mongoose.Types.ObjectId().toHexString() const MOCK_NEW_END_PAGE: FormEndPage = { title: 'expected end page title', buttonLink: 'https://some-button-link.example.com', @@ -2050,7 +2042,7 @@ describe('admin-form.service', () => { describe('updateStartPage', () => { const updateSpy = jest.spyOn(FormModel, 'updateStartPageById') - const MOCK_FORM_ID = new ObjectId().toHexString() + const MOCK_FORM_ID = new mongoose.Types.ObjectId().toHexString() const MOCK_NEW_START_PAGE: FormStartPage = { colorTheme: FormColorTheme.Blue, paragraph: 'some paragraph', @@ -2113,10 +2105,10 @@ describe('admin-form.service', () => { }) describe('updateFormLogic', () => { - const logicId1 = new ObjectId() - const logicId2 = new ObjectId() - const mockEmailFormId = new ObjectId() - const mockEncryptFormId = new ObjectId() + const logicId1 = new mongoose.Types.ObjectId() + const logicId2 = new mongoose.Types.ObjectId() + const mockEmailFormId = new mongoose.Types.ObjectId() + const mockEncryptFormId = new mongoose.Types.ObjectId() const mockFormLogicOld = { form_logics: [ @@ -2230,7 +2222,7 @@ describe('admin-form.service', () => { it('should return LogicNotFoundError if logic does not exist on form', async () => { // Act - const wrongLogicId = new ObjectId().toHexString() + const wrongLogicId = new mongoose.Types.ObjectId().toHexString() const actualResult = await AdminFormService.updateFormLogic( mockEmailForm, wrongLogicId, @@ -2252,7 +2244,7 @@ describe('admin-form.service', () => { title: 'some mock form', // Append created field to end of form_fields. form_fields: [MOCK_FIELD], - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), } as IPopulatedForm // Act @@ -2267,12 +2259,12 @@ describe('admin-form.service', () => { it("should return FieldNotFoundError when the fieldId does not exist in the form's fields", async () => { // Arrange - const MOCK_ID = new ObjectId().toHexString() + const MOCK_ID = new mongoose.Types.ObjectId().toHexString() const MOCK_FORM = { title: 'some mock form', // Append created field to end of form_fields. form_fields: [], - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), } as unknown as IPopulatedForm const expectedError = new FieldNotFoundError( `Attempted to retrieve field ${MOCK_ID} from ${MOCK_FORM._id} but field was not present`, @@ -2289,7 +2281,7 @@ describe('admin-form.service', () => { describe('disableSmsVerificationsForUser', () => { it('should return true when the forms are updated successfully', async () => { // Arrange - const MOCK_ADMIN_ID = new ObjectId().toHexString() + const MOCK_ADMIN_ID = new mongoose.Types.ObjectId().toHexString() const disableSpy = jest.spyOn(FormModel, 'disableSmsVerificationsForUser') disableSpy.mockResolvedValueOnce({ n: 0, nModified: 0, ok: 0 }) @@ -2305,7 +2297,7 @@ describe('admin-form.service', () => { it('should return a database error when the operation fails', async () => { // Arrange - const MOCK_ADMIN_ID = new ObjectId().toHexString() + const MOCK_ADMIN_ID = new mongoose.Types.ObjectId().toHexString() const disableSpy = jest.spyOn(FormModel, 'disableSmsVerificationsForUser') disableSpy.mockRejectedValueOnce('whoops') @@ -2323,7 +2315,7 @@ describe('admin-form.service', () => { describe('shouldUpdateFormField', () => { const MOCK_FORM = { admin: { - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), }, } as unknown as IPopulatedForm diff --git a/src/app/modules/form/admin-form/admin-form.service.ts b/src/app/modules/form/admin-form/admin-form.service.ts index e6e3716c81..211db2aa76 100644 --- a/src/app/modules/form/admin-form/admin-form.service.ts +++ b/src/app/modules/form/admin-form/admin-form.service.ts @@ -336,23 +336,21 @@ export const transferFormOwnership = ( ), ), ) - // Step 4: Populate updated form. - .andThen((updatedForm) => - ResultAsync.fromPromise( - updatedForm - .populate({ path: 'admin', populate: { path: 'agency' } }) - .execPopulate(), - (error) => { - logger.error({ - message: 'Error occurred whilst populating form with admin', - meta: logMeta, - error, - }) + .andThen((updatedForm) => { + const populatePromise = updatedForm.populate({ + path: 'admin', + populate: { path: 'agency' }, + }) as Promise + return ResultAsync.fromPromise(populatePromise, (error) => { + logger.error({ + message: 'Error occurred whilst populating form with admin', + meta: logMeta, + error, + }) - return new DatabaseError(getMongoErrorMessage(error)) - }, - ), - ) + return new DatabaseError(getMongoErrorMessage(error)) + }) + }) ) } @@ -642,7 +640,9 @@ export const editFormFields = ( ).asyncAndThen((newFormFields) => { // Update form fields of original form. originalForm.form_fields = newFormFields - return ResultAsync.fromPromise(originalForm.save(), (error) => { + + const savePromise = originalForm.save() as Promise + return ResultAsync.fromPromise(savePromise, (error) => { logger.error({ message: 'Error encountered while editing form fields', meta: { @@ -683,8 +683,12 @@ export const updateForm = ( // Updating some part of form, override original form with new updated form. assignIn(originalForm, scrubbedFormParams) + // Type instantiation is excessively deep and possibly infinite. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const savePromise = originalForm.save() as Promise - return ResultAsync.fromPromise(originalForm.save(), (error) => { + return ResultAsync.fromPromise(savePromise, (error) => { logger.error({ message: 'Error encountered while updating form', meta: { diff --git a/src/app/modules/form/public-form/__tests__/public-form.service.spec.ts b/src/app/modules/form/public-form/__tests__/public-form.service.spec.ts index cc50bfbd85..0b35ace90c 100644 --- a/src/app/modules/form/public-form/__tests__/public-form.service.spec.ts +++ b/src/app/modules/form/public-form/__tests__/public-form.service.spec.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { ObjectId } from 'bson-ext' -import mockingoose from 'mockingoose' import mongoose from 'mongoose' import { PartialDeep } from 'type-fest' @@ -79,8 +78,10 @@ describe('public-form.service', () => { it('should return DatabaseError when error occurs whilst inserting feedback', async () => { // Arrange // Mock failure - mockingoose(FormFeedbackModel).toReturn(new Error('some error'), 'save') - const insertSpy = jest.spyOn(FormFeedbackModel, 'create') + const insertSpy = jest + .spyOn(FormFeedbackModel, 'create') + // @ts-ignore + .mockRejectedValueOnce(new Error('some error')) // Act const actualResult = await PublicFormService.insertFormFeedback( @@ -151,8 +152,12 @@ describe('public-form.service', () => { } // Mock form return. - mockingoose(FormModel).toReturn(mockForm, 'findOne') - const findByIdSpy = jest.spyOn(FormModel, 'findById') + const findByIdSpy = jest + .spyOn(FormModel, 'findById') + // @ts-ignore + .mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(mockForm), + }) // Act const createResult = await PublicFormService.createMetatags( @@ -180,8 +185,12 @@ describe('public-form.service', () => { it('should return FormNotFoundError when form cannot be retrieved with given formId', async () => { // Arrange // Mock null form return. - mockingoose(FormModel).toReturn(null, 'findOne') - const findByIdSpy = jest.spyOn(FormModel, 'findById') + const findByIdSpy = jest + .spyOn(FormModel, 'findById') + // @ts-ignore + .mockReturnValueOnce({ + exec: jest.fn().mockResolvedValue(null), + }) // Act const createResult = await PublicFormService.createMetatags( @@ -199,8 +208,12 @@ describe('public-form.service', () => { it('should return DatabaseError when error occurs whilst querying database', async () => { // Arrange // Mock failure - mockingoose(FormModel).toReturn(new Error('some error'), 'findOne') - const findByIdSpy = jest.spyOn(FormModel, 'findById') + const findByIdSpy = jest + .spyOn(FormModel, 'findById') + // @ts-ignore + .mockReturnValueOnce({ + exec: jest.fn().mockRejectedValue(new Error('some error')), + }) // Act const createResult = await PublicFormService.createMetatags( diff --git a/src/app/modules/myinfo/__tests__/myinfo_hash.model.spec.ts b/src/app/modules/myinfo/__tests__/myinfo_hash.model.spec.ts index c09a9df56d..21f494a8dd 100644 --- a/src/app/modules/myinfo/__tests__/myinfo_hash.model.spec.ts +++ b/src/app/modules/myinfo/__tests__/myinfo_hash.model.spec.ts @@ -53,7 +53,7 @@ describe('MyInfo Hash Model', () => { expect(actual._id).toBeDefined() expect( pick(actual, ['uinFin', 'form', 'fields', 'created', 'expireAt']), - ).toEqual(DEFAULT_INPUT_PARAMS) + ).toMatchObject(DEFAULT_INPUT_PARAMS) }) it('should throw validation error on missing uinFin', async () => { @@ -134,10 +134,10 @@ describe('MyInfo Hash Model', () => { const found = await MyInfoHash.findOne({}) // Both the returned document and the found document should match // Note: we are checking that the document contains the HASHED uinFin - expect(pick(actual, ['uinFin', 'form', 'fields'])).toEqual( + expect(pick(actual, ['uinFin', 'form', 'fields'])).toMatchObject( pick(DEFAULT_SAVED_PARAMS, ['uinFin', 'form', 'fields']), ) - expect(pick(found, ['uinFin', 'form', 'fields'])).toEqual( + expect(pick(found, ['uinFin', 'form', 'fields'])).toMatchObject( pick(DEFAULT_SAVED_PARAMS, ['uinFin', 'form', 'fields']), ) }) diff --git a/src/app/modules/myinfo/myinfo_hash.model.ts b/src/app/modules/myinfo/myinfo_hash.model.ts index 28bbdb4d5d..9769d8b8dd 100644 --- a/src/app/modules/myinfo/myinfo_hash.model.ts +++ b/src/app/modules/myinfo/myinfo_hash.model.ts @@ -22,7 +22,7 @@ const MyInfoHashSchema = new Schema( required: true, }, fields: { - type: Object, + type: Schema.Types.Mixed, required: true, }, expireAt: { diff --git a/src/app/modules/spcp/__tests__/spcp.service.spec.ts b/src/app/modules/spcp/__tests__/spcp.service.spec.ts index 5ebac8b29a..5c66e96570 100644 --- a/src/app/modules/spcp/__tests__/spcp.service.spec.ts +++ b/src/app/modules/spcp/__tests__/spcp.service.spec.ts @@ -7,8 +7,6 @@ import { mocked } from 'ts-jest/utils' import { ISpcpMyInfo } from 'src/app/config/features/spcp-myinfo.config' import { MOCK_COOKIE_AGE } from 'src/app/modules/myinfo/__tests__/myinfo.test.constants' -import dbHandler from 'tests/unit/backend/helpers/jest-db' - import { FormAuthType } from '../../../../../shared/types' import { CreateRedirectUrlError, @@ -57,12 +55,9 @@ jest.mock('axios') const MockAxios = mocked(axios, true) describe('spcp.service', () => { - beforeAll(async () => await dbHandler.connect()) beforeEach(async () => { - await dbHandler.clearDatabase() jest.clearAllMocks() }) - afterAll(async () => await dbHandler.closeDatabase()) describe('class constructor', () => { it('should instantiate auth clients with the correct params', () => { const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) diff --git a/src/app/modules/submission/email-submission/__tests__/email-submission.service.spec.ts b/src/app/modules/submission/email-submission/__tests__/email-submission.service.spec.ts index fbf435de25..652067825f 100644 --- a/src/app/modules/submission/email-submission/__tests__/email-submission.service.spec.ts +++ b/src/app/modules/submission/email-submission/__tests__/email-submission.service.spec.ts @@ -72,7 +72,7 @@ describe('email-submission.service', () => { _id: result._id, }) - expect(result.form).toEqual(MOCK_FORM._id) + expect(result.form).toMatchObject(MOCK_FORM._id) expect(result.responseHash).toEqual(MOCK_RESPONSE_HASH) expect(result.responseSalt).toEqual(MOCK_RESPONSE_SALT) expect(foundInDatabase).toBeNull() 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 5b1b9cdf07..c1bbe6c8a9 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 @@ -71,7 +71,7 @@ describe('encrypt-submission.service', () => { }) expect(result.encryptedContent).toBe(MOCK_ENCRYPTED_CONTENT) - expect(result.form).toEqual(MOCK_FORM._id) + expect(result.form).toMatchObject(MOCK_FORM._id) expect(result.verifiedContent).toEqual(MOCK_VERIFIED_CONTENT) expect(Object.fromEntries(result.attachmentMetadata!)).toEqual( Object.fromEntries(MOCK_ATTACHMENT_METADATA), diff --git a/src/app/modules/verification/__tests__/verification.model.spec.ts b/src/app/modules/verification/__tests__/verification.model.spec.ts index 0d73b6fe73..f9979ab7b0 100644 --- a/src/app/modules/verification/__tests__/verification.model.spec.ts +++ b/src/app/modules/verification/__tests__/verification.model.spec.ts @@ -38,7 +38,7 @@ describe('Verification Model', () => { '__v', ]) const expectedSavedFields = merge({}, VFN_DEFAULTS, VFN_PARAMS) - expect(actualSavedFields).toEqual(expectedSavedFields) + expect(actualSavedFields).toMatchObject(expectedSavedFields) }) it('should save successfully when expireAt is specified', async () => { @@ -51,7 +51,7 @@ describe('Verification Model', () => { '__v', ]) const expectedSavedFields = merge({}, VFN_DEFAULTS, params) - expect(actualSavedFields).toEqual(expectedSavedFields) + expect(actualSavedFields).toMatchObject(expectedSavedFields) }) it('should save successfully with defaults when field keys are not specified', async () => { @@ -67,7 +67,7 @@ describe('Verification Model', () => { '__v', ]) const expectedSavedFields = merge({}, VFN_DEFAULTS, VFN_PARAMS, vfnParams) - expect(actualSavedFields).toEqual(expectedSavedFields) + expect(actualSavedFields).toMatchObject(expectedSavedFields) }) it('should save successfully when field keys are specified', async () => { @@ -88,7 +88,7 @@ describe('Verification Model', () => { '__v', ]) const expectedSavedFields = merge({}, VFN_DEFAULTS, VFN_PARAMS, vfnParams) - expect(actualSavedFields).toEqual(expectedSavedFields) + expect(actualSavedFields).toMatchObject(expectedSavedFields) }) it('should not save when field IDs are identical', async () => { @@ -127,7 +127,7 @@ describe('Verification Model', () => { const result = transaction.getField(field1._id)!.toJSON() - expect(omit(result, '_id')).toEqual(omit(field1, '_id')) + expect(omit(result, '_id')).toMatchObject(omit(field1, '_id')) expect(String(result._id)).toEqual(field1._id) }) @@ -156,7 +156,7 @@ describe('Verification Model', () => { verificationSaved._id, ) const expected = pick(verificationSaved, ['formId', 'expireAt', '_id']) - expect(actual).toEqual(expected) + expect(actual).toMatchObject(expected) }) it('should return null when transaction does not exist', async () => { diff --git a/src/app/modules/verification/__tests__/verification.service.spec.ts b/src/app/modules/verification/__tests__/verification.service.spec.ts index 3e19d3d01b..aa61956831 100644 --- a/src/app/modules/verification/__tests__/verification.service.spec.ts +++ b/src/app/modules/verification/__tests__/verification.service.spec.ts @@ -1,5 +1,4 @@ /* eslint-disable import/first */ -import { ObjectId } from 'bson' import { addHours, subHours, subMinutes, subSeconds } from 'date-fns' import mongoose from 'mongoose' import { errAsync, okAsync } from 'neverthrow' @@ -81,11 +80,11 @@ jest.mock('src/app/utils/hash') const MockHashUtils = mocked(HashUtils, true) describe('Verification service', () => { - const mockFieldIdObj = new ObjectId() + const mockFieldIdObj = new mongoose.Types.ObjectId() const mockFieldId = mockFieldIdObj.toHexString() const mockField = { ...generateFieldParams(), _id: mockFieldId } - const mockTransactionId = new ObjectId().toHexString() - const mockFormId = new ObjectId().toHexString() + const mockTransactionId = new mongoose.Types.ObjectId().toHexString() + const mockFormId = new mongoose.Types.ObjectId().toHexString() let mockTransaction: IVerificationSchema beforeAll(async () => await dbHandler.connect()) @@ -107,7 +106,7 @@ describe('Verification service', () => { describe('createTransaction', () => { const mockForm = { - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), title: 'mockForm', form_fields: [], } as unknown as IFormSchema @@ -171,7 +170,7 @@ describe('Verification service', () => { mockPublicView = { expireAt: mockTransaction.expireAt, formId: mockTransaction.formId, - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), } getPublicViewByIdSpy = jest .spyOn(VerificationModel, 'getPublicViewById') @@ -224,7 +223,7 @@ describe('Verification service', () => { it('should return TransactionNotFoundError when transaction ID does not exist', async () => { const result = await VerificationService.resetFieldForTransaction( // non-existent transaction ID - new ObjectId().toHexString(), + new mongoose.Types.ObjectId().toHexString(), mockFieldId, ) @@ -252,7 +251,7 @@ describe('Verification service', () => { const result = await VerificationService.resetFieldForTransaction( mockTransactionId, // ObjectId which does not exist in mockTransaction - new ObjectId().toHexString(), + new mongoose.Types.ObjectId().toHexString(), ) expect(resetFieldSpy).not.toHaveBeenCalled() @@ -293,7 +292,7 @@ describe('Verification service', () => { > const mockForm = { - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), title: 'mockForm', form_fields: [ generateDefaultField(BasicField.Mobile, { @@ -352,7 +351,7 @@ describe('Verification service', () => { it('should return TransactionNotFoundError when transaction ID does not exist', async () => { const result = await VerificationService.sendNewOtp({ // non-existent transaction ID - transactionId: new ObjectId().toHexString(), + transactionId: new mongoose.Types.ObjectId().toHexString(), fieldId: mockFieldId, hashedOtp: MOCK_HASHED_OTP, otp: MOCK_OTP, @@ -396,7 +395,7 @@ describe('Verification service', () => { const result = await VerificationService.sendNewOtp({ transactionId: mockTransactionId, // ObjectId which does not exist in mockTransaction - fieldId: new ObjectId().toHexString(), + fieldId: new mongoose.Types.ObjectId().toHexString(), hashedOtp: MOCK_HASHED_OTP, otp: MOCK_OTP, recipient: MOCK_RECIPIENT, @@ -498,7 +497,7 @@ describe('Verification service', () => { expect(MockSmsFactory.sendVerificationOtp).toHaveBeenCalledWith( MOCK_RECIPIENT, MOCK_OTP, - new ObjectId(mockFormId), + new mongoose.Types.ObjectId(mockFormId), ) expect( MockFormsgSdk.verification.generateSignature, @@ -524,12 +523,12 @@ describe('Verification service', () => { expect(MockSmsFactory.sendVerificationOtp).toHaveBeenCalledWith( MOCK_RECIPIENT, MOCK_OTP, - new ObjectId(mockFormId), + new mongoose.Types.ObjectId(mockFormId), ) expect(MockFormsgSdk.verification.generateSignature).toHaveBeenCalledWith( { transactionId: mockTransactionId, - formId: new ObjectId(mockFormId), + formId: new mongoose.Types.ObjectId(mockFormId), fieldId: mockFieldId, answer: MOCK_RECIPIENT, }, @@ -595,7 +594,7 @@ describe('Verification service', () => { it('should return TransactionNotFoundError when transaction ID does not exist', async () => { const result = await VerificationService.verifyOtp( - new ObjectId().toHexString(), + new mongoose.Types.ObjectId().toHexString(), mockFieldId, MOCK_OTP, ) @@ -624,7 +623,7 @@ describe('Verification service', () => { it('should return FieldNotFoundInTransactionError when field ID does not exist', async () => { const result = await VerificationService.verifyOtp( mockTransactionId, - new ObjectId().toHexString(), + new mongoose.Types.ObjectId().toHexString(), MOCK_OTP, ) @@ -729,9 +728,9 @@ describe('Verification service', () => { describe('disableVerifiedFieldsIfRequired', () => { const MOCK_FORM = { title: 'some mock form', - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), admin: { - _id: new ObjectId(), + _id: new mongoose.Types.ObjectId(), }, permissionList: [{ email: 'some@user.gov.sg' }], } as IPopulatedForm @@ -949,7 +948,7 @@ describe('Verification service', () => { describe('shouldGenerateMobileOtp', () => { it('should return true when the fieldId is valid and verifiable', async () => { // Arrange - const fieldId = new ObjectId().toHexString() + const fieldId = new mongoose.Types.ObjectId().toHexString() const mockForm = { form_fields: [ generateDefaultField(BasicField.Mobile, { @@ -971,7 +970,7 @@ describe('Verification service', () => { it('should return OtpRequestError when an OTP is requested on a field that is not verifiable', async () => { // Arrange - const fieldId = new ObjectId().toHexString() + const fieldId = new mongoose.Types.ObjectId().toHexString() const mockForm = { // Not enabled. form_fields: [ @@ -997,12 +996,12 @@ describe('Verification service', () => { const mockForm = { form_fields: [ generateDefaultField(BasicField.Mobile, { - _id: new ObjectId().toHexString(), + _id: new mongoose.Types.ObjectId().toHexString(), isVerifiable: true, }), ], } - const fieldIdOtherString = new ObjectId().toHexString() + const fieldIdOtherString = new mongoose.Types.ObjectId().toHexString() // Act const actual = await VerificationService.shouldGenerateMobileOtp( @@ -1019,7 +1018,7 @@ describe('Verification service', () => { const mockForm = { form_fields: [], } - const fieldId = new ObjectId().toHexString() + const fieldId = new mongoose.Types.ObjectId().toHexString() // Act const actual = await VerificationService.shouldGenerateMobileOtp( diff --git a/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.form.routes.spec.ts b/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.form.routes.spec.ts index 3a2461cace..7c8a231e52 100644 --- a/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.form.routes.spec.ts +++ b/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.form.routes.spec.ts @@ -25,7 +25,6 @@ import { setupApp } from 'tests/integration/helpers/express-setup' import { buildCelebrateError } from 'tests/unit/backend/helpers/celebrate' import { generateDefaultField } from 'tests/unit/backend/helpers/generate-form-data' import dbHandler from 'tests/unit/backend/helpers/jest-db' -import { jsonParseStringify } from 'tests/unit/backend/helpers/serialize-data' import { BasicField, @@ -55,7 +54,7 @@ const app = setupApp('/admin/forms', AdminFormsRouter, { setupWithAuth: true, }) -describe('admin-form.form.routes', () => { +describe('admin-forms.form.routes', () => { let request: Session let defaultUser: IUserSchema @@ -141,7 +140,7 @@ describe('admin-form.form.routes', () => { }, }) .lean() - expect(response.body).toEqual(jsonParseStringify(expected)) + expect(response.body).toMatchObject(expected) expect(response.status).toEqual(200) }) @@ -545,8 +544,8 @@ describe('admin-form.form.routes', () => { .lean() expect(response.status).toEqual(200) expect(response.body).not.toBeNull() - expect(response.body).toEqual({ - form: jsonParseStringify(expected), + expect(response.body).toMatchObject({ + form: expected, }) }) @@ -582,8 +581,8 @@ describe('admin-form.form.routes', () => { .lean() expect(response.status).toEqual(200) expect(response.body).not.toBeNull() - expect(response.body).toEqual({ - form: jsonParseStringify(expected), + expect(response.body).toMatchObject({ + form: expected, }) }) @@ -1470,7 +1469,7 @@ describe('admin-form.form.routes', () => { // Assert expect(response.status).toEqual(200) - expect(response.body).toEqual(jsonParseStringify(MOCK_FIELD)) + expect(response.body).toMatchObject(MOCK_FIELD) }) it('should return 403 when user does not have permissions to retrieve form field', async () => { @@ -1857,7 +1856,7 @@ describe('admin-form.form.routes', () => { // Assert expect(resp.status).toBe(200) - expect(resp.body).toEqual(jsonParseStringify(MOCK_UPDATED_START_PAGE)) + expect(resp.body).toMatchObject(MOCK_UPDATED_START_PAGE) }) it('should return 403 when the user does not have permission to update the start page', async () => { @@ -1884,7 +1883,7 @@ describe('admin-form.form.routes', () => { // Assert expect(resp.status).toBe(403) - expect(resp.body).toEqual(jsonParseStringify(expectedResponse)) + expect(resp.body).toMatchObject(expectedResponse) }) it('should return 404 when the form cannot be found', async () => { @@ -1896,7 +1895,7 @@ describe('admin-form.form.routes', () => { // Assert expect(resp.status).toBe(404) - expect(resp.body).toEqual(jsonParseStringify(expectedResponse)) + expect(resp.body).toMatchObject(expectedResponse) }) it('should return 410 when updating the start page for a form that has been archived', async () => { diff --git a/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.preview.routes.spec.ts b/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.preview.routes.spec.ts index b1c7ccf01b..9d50ed6c57 100644 --- a/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.preview.routes.spec.ts +++ b/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.preview.routes.spec.ts @@ -97,14 +97,12 @@ describe('admin-form.preview.routes', () => { // Assert const expectedForm = ( - await formToPreview - .populate({ - path: 'admin', - populate: { - path: 'agency', - }, - }) - .execPopulate() + await formToPreview.populate({ + path: 'admin', + populate: { + path: 'agency', + }, + }) ).getPublicView() expect(response.status).toEqual(200) expect(response.body).toEqual({ @@ -136,14 +134,12 @@ describe('admin-form.preview.routes', () => { // Assert const expectedForm = ( - await collabFormToPreview - .populate({ - path: 'admin', - populate: { - path: 'agency', - }, - }) - .execPopulate() + await collabFormToPreview.populate({ + path: 'admin', + populate: { + path: 'agency', + }, + }) ).getPublicView() expect(response.status).toEqual(200) expect(response.body).toEqual({ form: jsonParseStringify(expectedForm) }) diff --git a/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.submissions.routes.spec.ts b/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.submissions.routes.spec.ts index bed079d8a2..2dd84f21e9 100644 --- a/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.submissions.routes.spec.ts +++ b/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.submissions.routes.spec.ts @@ -2,6 +2,7 @@ import { ObjectId } from 'bson-ext' import { format, subDays } from 'date-fns' import { times } from 'lodash' +import MockDate from 'mockdate' import mongoose from 'mongoose' import supertest, { Session } from 'supertest-session' @@ -181,7 +182,6 @@ describe('admin-form.submissions.routes', () => { it('should return 200 with counts of submissions made between given start and end dates.', async () => { // Arrange - const expectedSubmissionCount = 3 const newForm = (await EmailFormModel.create({ title: 'new form', responseMode: FormResponseMode.Email, @@ -193,15 +193,15 @@ describe('admin-form.submissions.routes', () => { hash: 'some hash', salt: 'some salt', } - const results = await Promise.all( - times(expectedSubmissionCount, () => - saveSubmissionMetadata(newForm, mockSubmissionHash), - ), + // Inconsequential submissions + await Promise.all( + times(2, () => saveSubmissionMetadata(newForm, mockSubmissionHash)), ) + + // Add submission with specific date. const now = new Date() - const firstSubmission = results[0]._unsafeUnwrap() - firstSubmission.created = subDays(now, 5) - await firstSubmission.save() + MockDate.set(subDays(now, 5)) + await saveSubmissionMetadata(newForm, mockSubmissionHash) // Act const response = await request @@ -213,12 +213,14 @@ describe('admin-form.submissions.routes', () => { // Assert expect(response.status).toEqual(200) + // Should have a single submission that fits the date range expect(response.body).toEqual(1) + + MockDate.reset() }) it('should return 200 with counts of submissions made with same start and end dates', async () => { // Arrange - const expectedSubmissionCount = 3 const newForm = (await EmailFormModel.create({ title: 'new form', responseMode: FormResponseMode.Email, @@ -230,15 +232,15 @@ describe('admin-form.submissions.routes', () => { hash: 'some hash', salt: 'some salt', } - const results = await Promise.all( - times(expectedSubmissionCount, () => - saveSubmissionMetadata(newForm, mockSubmissionHash), - ), + + // Save two inconsequential submissions. + await Promise.all( + times(2, () => saveSubmissionMetadata(newForm, mockSubmissionHash)), ) + // Save a submission with a specific expected date. const expectedDate = '2021-04-04' - const firstSubmission = results[0]._unsafeUnwrap() - firstSubmission.created = new Date(expectedDate) - await firstSubmission.save() + MockDate.set(expectedDate) + await saveSubmissionMetadata(newForm, mockSubmissionHash) // Act const response = await request @@ -250,7 +252,10 @@ describe('admin-form.submissions.routes', () => { // Assert expect(response.status).toEqual(200) + // Should only have 1 submission from the specific date. expect(response.body).toEqual(1) + + MockDate.reset() }) it('should return 400 when query.startDate is missing when query.endDate is provided', async () => { @@ -533,6 +538,18 @@ describe('admin-form.submissions.routes', () => { })) as IFormDocument }) + const createEncryptSubmissionBody = (metaString: unknown) => { + return { + form: defaultForm, + encryptedContent: `any encrypted content ${metaString}`, + verifiedContent: `any verified content ${metaString}`, + attachmentMetadata: new Map([ + ['fieldId1', `some.attachment.url.${metaString}`], + ['fieldId2', `some.other.attachment.url.${metaString}`], + ]), + } + } + it('should return 200 with stream of encrypted responses without attachment URLs when query.downloadAttachments is false', async () => { // Arrange const submissions = await Promise.all( @@ -657,30 +674,21 @@ describe('admin-form.submissions.routes', () => { it('should return 200 with stream of encrypted responses when query.startDate is the same as query.endDate', async () => { // Arrange - const submissions = await Promise.all( - times(5, (count) => - createEncryptSubmission({ - form: defaultForm, - encryptedContent: `any encrypted content ${count}`, - verifiedContent: `any verified content ${count}`, - attachmentMetadata: new Map([ - ['fieldId1', `some.attachment.url.${count}`], - ['fieldId2', `some.other.attachment.url.${count}`], - ]), - }), + // Inconsequential submissions + await Promise.all( + times(3, (count) => + createEncryptSubmission(createEncryptSubmissionBody(count)), ), ) const expectedDate = '2020-02-03' + MockDate.set(expectedDate) // Set 2 submissions to be submitted with specific date - submissions[2].created = new Date(expectedDate) - submissions[4].created = new Date(expectedDate) - await submissions[2].save() - await submissions[4].save() - const expectedSubmissionIds = [ - String(submissions[2]._id), - String(submissions[4]._id), - ] + const specificSubmissions = await Promise.all( + times(2, (count) => + createEncryptSubmission(createEncryptSubmissionBody(count)), + ), + ) // Act const response = await request @@ -699,7 +707,7 @@ describe('admin-form.submissions.routes', () => { }) // Assert - const expectedSorted = submissions + const expectedSorted = specificSubmissions .map((s) => jsonParseStringify({ _id: s._id, @@ -712,7 +720,6 @@ describe('admin-form.submissions.routes', () => { version: s.version, }), ) - .filter((s) => expectedSubmissionIds.includes(s._id)) .sort((a, b) => String(a._id).localeCompare(String(b._id))) const actualSorted = (response.body as string) @@ -725,35 +732,30 @@ describe('admin-form.submissions.routes', () => { expect(response.status).toEqual(200) expect(actualSorted).toEqual(expectedSorted) + + MockDate.reset() }) it('should return 200 with stream of encrypted responses between given query.startDate and query.endDate', async () => { // Arrange - const submissions = await Promise.all( - times(5, (count) => - createEncryptSubmission({ - form: defaultForm, - encryptedContent: `any encrypted content ${count}`, - verifiedContent: `any verified content ${count}`, - attachmentMetadata: new Map([ - ['fieldId1', `some.attachment.url.${count}`], - ['fieldId2', `some.other.attachment.url.${count}`], - ]), - }), + // Inconsequential submissions + await Promise.all( + times(3, (count) => + createEncryptSubmission(createEncryptSubmissionBody(count)), ), ) const startDateStr = '2020-02-03' const endDateStr = '2020-02-04' + MockDate.set(startDateStr) // Set 2 submissions to be submitted with specific date - submissions[2].created = new Date(startDateStr) - submissions[4].created = new Date(endDateStr) - await submissions[2].save() - await submissions[4].save() - const expectedSubmissionIds = [ - String(submissions[2]._id), - String(submissions[4]._id), - ] + const specificSubmission1 = await createEncryptSubmission( + createEncryptSubmissionBody(startDateStr), + ) + MockDate.set(endDateStr) + const specificSubmission2 = await createEncryptSubmission( + createEncryptSubmissionBody(endDateStr), + ) // Act const response = await request @@ -772,7 +774,7 @@ describe('admin-form.submissions.routes', () => { }) // Assert - const expectedSorted = submissions + const expectedSorted = [specificSubmission1, specificSubmission2] .map((s) => jsonParseStringify({ _id: s._id, @@ -785,7 +787,6 @@ describe('admin-form.submissions.routes', () => { version: s.version, }), ) - .filter((s) => expectedSubmissionIds.includes(s._id)) .sort((a, b) => String(a._id).localeCompare(String(b._id))) const actualSorted = (response.body as string) @@ -798,6 +799,8 @@ describe('admin-form.submissions.routes', () => { expect(response.status).toEqual(200) expect(actualSorted).toEqual(expectedSorted) + + MockDate.reset() }) it('should return 400 when form of given formId is not an encrypt mode form', async () => { diff --git a/src/app/routes/api/v3/forms/__tests__/public-forms.auth.routes.spec.ts b/src/app/routes/api/v3/forms/__tests__/public-forms.auth.routes.spec.ts index 96a3254761..e4f4a2c7d1 100644 --- a/src/app/routes/api/v3/forms/__tests__/public-forms.auth.routes.spec.ts +++ b/src/app/routes/api/v3/forms/__tests__/public-forms.auth.routes.spec.ts @@ -47,7 +47,7 @@ describe('public-form.auth.routes', () => { formOptions: { authType: FormAuthType.SP, status: FormStatus.Public, - esrvcId: new ObjectId().toHexString(), + esrvcId: 'rubbish-esrvcId', }, }) @@ -72,7 +72,7 @@ describe('public-form.auth.routes', () => { formOptions: { authType: FormAuthType.CP, status: FormStatus.Public, - esrvcId: new ObjectId().toHexString(), + esrvcId: 'rubbish-esrvcId', }, }) @@ -97,7 +97,7 @@ describe('public-form.auth.routes', () => { formOptions: { authType: FormAuthType.MyInfo, status: FormStatus.Public, - esrvcId: new ObjectId().toHexString(), + esrvcId: 'rubbish-esrvcId', }, }) @@ -122,7 +122,7 @@ describe('public-form.auth.routes', () => { formOptions: { authType: FormAuthType.MyInfo, status: FormStatus.Public, - esrvcId: new ObjectId().toHexString(), + esrvcId: 'rubbish-esrvcId', }, }) const expectedResponse = buildCelebrateError({ @@ -147,7 +147,7 @@ describe('public-form.auth.routes', () => { const { form } = await dbHandler.insertEncryptForm({ formOptions: { status: FormStatus.Public, - esrvcId: new ObjectId().toHexString(), + esrvcId: 'rubbish-esrvcId', }, }) const expectedResponse = jsonParseStringify({ @@ -194,10 +194,11 @@ describe('public-form.auth.routes', () => { message: 'Could not find the form requested. Please refresh and try again.', }) + const mockFormId = new ObjectId().toHexString() // Act const response = await request - .get(`/forms/${new ObjectId().toHexString()}/auth/redirect`) + .get(`/forms/${mockFormId}/auth/redirect`) .query({ isPersistentLogin: false }) // Assert @@ -211,7 +212,7 @@ describe('public-form.auth.routes', () => { formOptions: { authType: FormAuthType.SP, status: FormStatus.Public, - esrvcId: new ObjectId().toHexString(), + esrvcId: 'rubbish-esrvcId', }, }) const expectedResponse = jsonParseStringify({ @@ -237,7 +238,7 @@ describe('public-form.auth.routes', () => { formOptions: { authType: FormAuthType.SP, status: FormStatus.Public, - esrvcId: new ObjectId().toHexString(), + esrvcId: 'rubbish-esrvcId', }, }) const expectedResponse = jsonParseStringify({ @@ -264,7 +265,7 @@ describe('public-form.auth.routes', () => { const { form } = await dbHandler.insertEmailForm({ formOptions: { authType: FormAuthType.SP, - esrvcId: new ObjectId().toHexString(), + esrvcId: 'rubbish-esrvcId', status: FormStatus.Public, }, }) @@ -284,7 +285,7 @@ describe('public-form.auth.routes', () => { const { form } = await dbHandler.insertEmailForm({ formOptions: { authType: FormAuthType.SP, - esrvcId: new ObjectId().toHexString(), + esrvcId: 'rubbish-esrvcId', status: FormStatus.Public, }, }) @@ -309,7 +310,7 @@ describe('public-form.auth.routes', () => { const { form } = await dbHandler.insertEmailForm({ formOptions: { authType: FormAuthType.NIL, - esrvcId: new ObjectId().toHexString(), + esrvcId: 'rubbish-esrvcId', status: FormStatus.Public, }, }) @@ -331,7 +332,7 @@ describe('public-form.auth.routes', () => { const { form } = await dbHandler.insertEmailForm({ formOptions: { authType: FormAuthType.CP, - esrvcId: new ObjectId().toHexString(), + esrvcId: 'rubbish-esrvcId', status: FormStatus.Public, }, }) @@ -375,11 +376,10 @@ describe('public-form.auth.routes', () => { message: 'Could not find the form requested. Please refresh and try again.', }) + const mockFormId = new ObjectId().toHexString() // Act - const response = await request.get( - `/forms/${new ObjectId().toHexString()}/auth/validate`, - ) + const response = await request.get(`/forms/${mockFormId}/auth/validate`) // Assert expect(response.status).toEqual(404) @@ -391,7 +391,7 @@ describe('public-form.auth.routes', () => { const { form } = await dbHandler.insertEmailForm({ formOptions: { authType: FormAuthType.SP, - esrvcId: new ObjectId().toHexString(), + esrvcId: 'rubbish-esrvcId', status: FormStatus.Public, }, }) @@ -415,7 +415,7 @@ describe('public-form.auth.routes', () => { const { form } = await dbHandler.insertEmailForm({ formOptions: { authType: FormAuthType.SP, - esrvcId: new ObjectId().toHexString(), + esrvcId: 'rubbish-esrvcId', status: FormStatus.Public, }, }) @@ -440,7 +440,7 @@ describe('public-form.auth.routes', () => { const { form } = await dbHandler.insertEmailForm({ formOptions: { authType: FormAuthType.SP, - esrvcId: new ObjectId().toHexString(), + esrvcId: 'rubbish-esrvcId', status: FormStatus.Public, }, }) diff --git a/src/app/routes/api/v3/forms/__tests__/public-forms.routes.spec.constants.ts b/src/app/routes/api/v3/forms/__tests__/public-forms.routes.spec.constants.ts index 2ad24dc931..5793a83794 100644 --- a/src/app/routes/api/v3/forms/__tests__/public-forms.routes.spec.constants.ts +++ b/src/app/routes/api/v3/forms/__tests__/public-forms.routes.spec.constants.ts @@ -1,6 +1,10 @@ import fs from 'fs' -import { IAttachmentFieldSchema, ICheckboxFieldSchema } from 'src/types' +import { + IAttachmentFieldSchema, + ICheckboxFieldSchema, + ITableFieldSchema, +} from 'src/types' import { generateAttachmentResponse, @@ -9,15 +13,45 @@ import { generateSingleAnswerResponse, } from 'tests/unit/backend/helpers/generate-form-data' -import { BasicField, FieldBase } from '../../../../../../../shared/types' +import { + BasicField, + Column, + FieldBase, +} from '../../../../../../../shared/types' export const MOCK_NO_RESPONSES_BODY = { responses: [], } +const MOCK_TABLE_COLUMNS = [ + { + title: 'Test Column Title 1', + required: true, + columnType: BasicField.ShortText, + }, + { + title: 'Test Column Title 2', + required: true, + columnType: BasicField.Dropdown, + fieldOptions: ['Option 1', 'Option 2', 'Option 3'], + }, +] as const export const MOCK_TEXT_FIELD = generateDefaultField(BasicField.ShortText) +export const MOCK_TABLE_FIELD = generateDefaultField(BasicField.Table, { + minimumRows: 2, + columns: MOCK_TABLE_COLUMNS as unknown as Column[], +}) as ITableFieldSchema export const MOCK_TEXTFIELD_RESPONSE = generateSingleAnswerResponse(MOCK_TEXT_FIELD) +export const MOCK_TABLE_RESPONSE = { + _id: MOCK_TABLE_FIELD._id, + fieldType: BasicField.Table, + question: MOCK_TABLE_FIELD.title, + answerArray: [ + ['Test', MOCK_TABLE_COLUMNS[1].fieldOptions[1]], + ['Test 2', MOCK_TABLE_COLUMNS[1].fieldOptions[2]], + ], +} export const MOCK_ATTACHMENT_FIELD = generateDefaultField(BasicField.Attachment) export const MOCK_ATTACHMENT_RESPONSE = generateAttachmentResponse( diff --git a/src/app/routes/api/v3/forms/__tests__/public-forms.submissions.routes.spec.ts b/src/app/routes/api/v3/forms/__tests__/public-forms.submissions.routes.spec.ts index b8e6db7c4a..fe2e5be398 100644 --- a/src/app/routes/api/v3/forms/__tests__/public-forms.submissions.routes.spec.ts +++ b/src/app/routes/api/v3/forms/__tests__/public-forms.submissions.routes.spec.ts @@ -27,6 +27,8 @@ import { MOCK_OPTIONAL_VERIFIED_RESPONSE, MOCK_SECTION_FIELD, MOCK_SECTION_RESPONSE, + MOCK_TABLE_FIELD, + MOCK_TABLE_RESPONSE, MOCK_TEXT_FIELD, MOCK_TEXTFIELD_RESPONSE, MOCK_UINFIN, @@ -87,7 +89,7 @@ describe('public-form.submissions.routes', () => { const mockCpClient = mocked(MockAuthClient.mock.instances[1], true) describe('Joi validation', () => { - it('should return 200 when submission is valid', async () => { + it('should return 200 when submission is valid for non-table field', async () => { // Arrange const { form } = await dbHandler.insertEmailForm({ formOptions: { @@ -117,6 +119,39 @@ describe('public-form.submissions.routes', () => { }) }) + /** + * This test tests to ensure that deep nested discriminator fields still continue to work. + */ + it('should return 200 when submission is valid for table field', async () => { + // Arrange + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + hasCaptcha: false, + status: FormStatus.Public, + form_fields: [MOCK_TABLE_FIELD], + }, + }) + + // Act + const response = await request + .post(`/forms/${form._id}/submissions/email`) + // MOCK_RESPONSE contains all required keys + .field( + 'body', + JSON.stringify({ + responses: [MOCK_TABLE_RESPONSE], + }), + ) + .query({ captchaResponse: 'null' }) + + // Assert + expect(response.status).toBe(200) + expect(response.body).toEqual({ + message: 'Form submission successful.', + submissionId: expect.any(String), + }) + }) + it('should return 200 when answer is empty string for optional field', async () => { // Arrange const { form } = await dbHandler.insertEmailForm({ diff --git a/src/app/services/mail/mail.types.ts b/src/app/services/mail/mail.types.ts index 2432b59a3e..a25f50d59b 100644 --- a/src/app/services/mail/mail.types.ts +++ b/src/app/services/mail/mail.types.ts @@ -91,6 +91,9 @@ export type BounceNotificationHtmlData = { } export type AdminSmsDisabledData = { + // Type instantiation is excessively deep and possibly infinite. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore forms: FormLinkView[] } & SmsVerificationTiers diff --git a/src/app/services/sms/__tests__/sms_count.server.model.spec.ts b/src/app/services/sms/__tests__/sms_count.server.model.spec.ts index 4399941b83..b86b4bfbd7 100644 --- a/src/app/services/sms/__tests__/sms_count.server.model.spec.ts +++ b/src/app/services/sms/__tests__/sms_count.server.model.spec.ts @@ -68,7 +68,7 @@ describe('SmsCount', () => { 'createdAt', '__v', ]) - expect(actualSavedObject).toEqual(MOCK_FORM_DEACTIVATED_PARAMS) + expect(actualSavedObject).toMatchObject(MOCK_FORM_DEACTIVATED_PARAMS) }) it('should reject if form is missing', async () => { @@ -157,7 +157,7 @@ describe('SmsCount', () => { 'createdAt', '__v', ]) - expect(actualSavedObject).toEqual(MOCK_BOUNCED_SUBMISSION_PARAMS) + expect(actualSavedObject).toMatchObject(MOCK_BOUNCED_SUBMISSION_PARAMS) }) it('should reject if form is missing', async () => { @@ -268,7 +268,7 @@ describe('SmsCount', () => { 'createdAt', '__v', ]) - expect(actualSavedObject).toEqual(expected) + expect(actualSavedObject).toMatchObject(expected) }) it('should save successfully, but not save fields that is not defined in the schema', async () => { @@ -300,7 +300,7 @@ describe('SmsCount', () => { 'createdAt', '__v', ]) - expect(actualSavedObject).toEqual(expected) + expect(actualSavedObject).toMatchObject(expected) }) it('should save successfully and set isOnboarded to true when the credentials are different from default', async () => { @@ -328,7 +328,7 @@ describe('SmsCount', () => { 'createdAt', '__v', ]) as IVerificationSmsCountSchema - expect(omit(actualSavedObject, 'isOnboardedAccount')).toEqual( + expect(omit(actualSavedObject, 'isOnboardedAccount')).toMatchObject( verificationParams, ) expect(actualSavedObject.isOnboardedAccount).toBe(true) @@ -451,7 +451,7 @@ describe('SmsCount', () => { 'createdAt', '__v', ]) - expect(actualSavedObject).toEqual( + expect(actualSavedObject).toMatchObject( merge({}, MOCK_BOUNCED_SUBMISSION_PARAMS, { logType: LogType.success, }), @@ -477,7 +477,7 @@ describe('SmsCount', () => { 'createdAt', '__v', ]) - expect(actualSavedObject).toEqual( + expect(actualSavedObject).toMatchObject( merge({}, MOCK_BOUNCED_SUBMISSION_PARAMS, { logType: LogType.failure, }), @@ -503,7 +503,7 @@ describe('SmsCount', () => { 'createdAt', '__v', ]) - expect(actualSavedObject).toEqual( + expect(actualSavedObject).toMatchObject( merge({}, MOCK_FORM_DEACTIVATED_PARAMS, { logType: LogType.success }), ) }) @@ -527,7 +527,7 @@ describe('SmsCount', () => { 'createdAt', '__v', ]) - expect(actualSavedObject).toEqual( + expect(actualSavedObject).toMatchObject( merge({}, MOCK_FORM_DEACTIVATED_PARAMS, { logType: LogType.failure }), ) }) @@ -555,7 +555,7 @@ describe('SmsCount', () => { expect(actualLog?._id).toBeDefined() // Retrieve object and compare to params, remove indeterministic keys const actualSavedObject = omit(actualLog, ['_id', 'createdAt', '__v']) - expect(actualSavedObject).toEqual(expectedLog) + expect(actualSavedObject).toMatchObject(expectedLog) }) it('should successfully log verification failures in the collection', async () => { @@ -581,7 +581,7 @@ describe('SmsCount', () => { expect(actualLog?._id).toBeDefined() // Retrieve object and compare to params, remove indeterministic keys const actualSavedObject = omit(actualLog, ['_id', 'createdAt', '__v']) - expect(actualSavedObject).toEqual(expectedLog) + expect(actualSavedObject).toMatchObject(expectedLog) }) it('should reject if smsType is invalid', async () => { diff --git a/src/app/services/sms/sms.types.ts b/src/app/services/sms/sms.types.ts index 0862c886d6..37a996969d 100644 --- a/src/app/services/sms/sms.types.ts +++ b/src/app/services/sms/sms.types.ts @@ -61,17 +61,21 @@ export interface IVerificationSmsCount extends ISmsCount { userId: IUserSchema['_id'] } isOnboardedAccount: boolean + smsType: SmsType.Verification } -export interface IVerificationSmsCountSchema extends ISmsCountSchema { - isOnboardedAccount: boolean -} +export interface IVerificationSmsCountSchema + extends IVerificationSmsCount, + Document {} export interface IAdminContactSmsCount extends ISmsCount { admin: IUserSchema['_id'] + smsType: SmsType.AdminContact } -export type IAdminContactSmsCountSchema = ISmsCountSchema +export interface IAdminContactSmsCountSchema + extends IAdminContactSmsCount, + Document {} export interface IFormDeactivatedSmsCount extends ISmsCount, diff --git a/src/app/utils/field-validation/validators/__tests__/attachment-validation.spec.ts b/src/app/utils/field-validation/validators/__tests__/attachment-validation.spec.ts index d43255c567..0806e06f7a 100644 --- a/src/app/utils/field-validation/validators/__tests__/attachment-validation.spec.ts +++ b/src/app/utils/field-validation/validators/__tests__/attachment-validation.spec.ts @@ -1,4 +1,4 @@ -import { ObjectId } from 'mongodb' +import { ObjectId } from 'bson-ext' import { ValidateFieldError } from 'src/app/modules/submission/submission.errors' import { validateField } from 'src/app/utils/field-validation/' diff --git a/src/app/utils/field-validation/validators/__tests__/checkbox-validation.spec.ts b/src/app/utils/field-validation/validators/__tests__/checkbox-validation.spec.ts index c1740b4ebd..f9886ff1a4 100644 --- a/src/app/utils/field-validation/validators/__tests__/checkbox-validation.spec.ts +++ b/src/app/utils/field-validation/validators/__tests__/checkbox-validation.spec.ts @@ -1,4 +1,4 @@ -import { ObjectId } from 'mongodb' +import { ObjectId } from 'bson-ext' import { ValidateFieldError } from 'src/app/modules/submission/submission.errors' import { validateField } from 'src/app/utils/field-validation' diff --git a/src/types/agency.ts b/src/types/agency.ts index 0bd310fa08..f3157e2a17 100644 --- a/src/types/agency.ts +++ b/src/types/agency.ts @@ -1,4 +1,4 @@ -import { EnforceDocument, Model } from 'mongoose' +import { HydratedDocument, Model } from 'mongoose' import { AgencyBase, PublicAgencyDto } from '../../shared/types' @@ -18,10 +18,12 @@ export interface IAgencyDocument extends IAgencySchema { } // Used to cast created documents whenever needed. -export type AgencyDocument = EnforceDocument< +export type AgencyDocument = HydratedDocument< IAgencyDocument, - AgencyInstanceMethods + AgencyInstanceMethods, + // eslint-disable-next-line @typescript-eslint/ban-types + {} > // eslint-disable-next-line @typescript-eslint/ban-types -export type IAgencyModel = Model +export type IAgencyModel = Model diff --git a/src/types/config.ts b/src/types/config.ts index da4de06ca4..79354ee5a3 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -1,7 +1,7 @@ import { PackageMode } from '@opengovsg/formsg-sdk/dist/types' import aws from 'aws-sdk' import { SessionOptions } from 'express-session' -import { ConnectionOptions } from 'mongoose' +import { ConnectOptions } from 'mongoose' import Mail from 'nodemailer/lib/mailer' // Enums @@ -23,7 +23,7 @@ export type AppConfig = { export type DbConfig = { uri: string - options: ConnectionOptions + options: ConnectOptions } export type AwsConfig = { diff --git a/src/types/form.ts b/src/types/form.ts index 2f1157024d..af76f80614 100644 --- a/src/types/form.ts +++ b/src/types/form.ts @@ -2,6 +2,7 @@ import { Document, LeanDocument, Model, + PopulatedDoc, ToObjectOptions, Types, UpdateWriteOpResult, @@ -67,7 +68,7 @@ export type IForm = Merge< SetOptional, { // Loosen types here to allow for IPopulatedForm extension - admin: any + admin: PopulatedDoc permission?: FormPermission[] form_fields?: FormFieldSchema[] form_logics?: FormLogicSchema[] diff --git a/src/types/form_logic.ts b/src/types/form_logic.ts index 1d884216a3..492459b5eb 100644 --- a/src/types/form_logic.ts +++ b/src/types/form_logic.ts @@ -10,7 +10,7 @@ import { ShowFieldLogic, } from '../../shared/types' -import { IFieldSchema } from './field' +import { FormFieldSchema, IFieldSchema } from './field' export interface ICondition extends FormCondition { field: IFieldSchema['_id'] @@ -27,6 +27,7 @@ export interface IShowFieldsLogicSchema extends ILogicSchema, ShowFieldLogic, Document { + show: FormFieldSchema['_id'][] logicType: LogicType.ShowFields conditions: IConditionSchema[] } diff --git a/src/types/form_statistics_total.ts b/src/types/form_statistics_total.ts index a3ff50e67a..9152cd2bf2 100644 --- a/src/types/form_statistics_total.ts +++ b/src/types/form_statistics_total.ts @@ -12,9 +12,7 @@ export interface IFormStatisticsTotalSchema extends IFormStatisticsTotal, Document {} -export type AggregateFormCountResult = - | [{ numActiveForms: number } | undefined] - | never[] +export type AggregateFormCountResult = [{ numActiveForms: number }] | never[] export interface IFormStatisticsTotalModel extends Model { diff --git a/src/types/user.ts b/src/types/user.ts index 04bb09fcb9..cc6dcbd385 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -1,5 +1,5 @@ import { ObjectId } from 'bson-ext' -import { Document, Model } from 'mongoose' +import { Document, Model, PopulatedDoc } from 'mongoose' import { SetOptional } from 'type-fest' import { PublicAgencyDto, UserBase } from '../../shared/types' @@ -17,7 +17,7 @@ export type AdminContactOtpData = { export interface IUser extends SetOptional { - agency: IAgencySchema['_id'] + agency: PopulatedDoc } export type UserContactView = Pick diff --git a/src/types/vendor/bson-ext.d.ts b/src/types/vendor/bson-ext.d.ts index cc9ca0836c..12f11091e9 100644 --- a/src/types/vendor/bson-ext.d.ts +++ b/src/types/vendor/bson-ext.d.ts @@ -1,509 +1,8 @@ -/* eslint-disable @typescript-eslint/no-empty-function */ -/* eslint-disable @typescript-eslint/ban-types */ -// Internal type definitions for bson-ext 2.0.3 -// !!! Typings are not verified !!! +// Internal type definitions for bson-ext basically re-exporting bson's own types. // Definitions retrieved from https://www.npmjs.com/package/@types/bson and // typed to follow bson-ext's exports. /// declare module 'bson-ext' { - interface CommonSerializeOptions { - /** {default:false}, the serializer will check if keys are valid. */ - checkKeys?: boolean - /** {default:false}, serialize the javascript functions. */ - serializeFunctions?: boolean - /** {default:true}, ignore undefined fields. */ - ignoreUndefined?: boolean - } - - export interface SerializeOptions extends CommonSerializeOptions { - /** {default:1024*1024*17}, minimum size of the internal temporary serialization buffer. */ - minInternalBufferSize?: number - } - - export interface SerializeWithBufferAndIndexOptions - extends CommonSerializeOptions { - /** {default:0}, the index in the buffer where we wish to start serializing into. */ - index?: number - } - - export interface DeserializeOptions { - /** {default:false}, evaluate functions in the BSON document scoped to the object deserialized. */ - evalFunctions?: boolean - /** {default:false}, cache evaluated functions for reuse. */ - cacheFunctions?: boolean - /** {default:false}, use a crc32 code for caching, otherwise use the string of the function. */ - cacheFunctionsCrc32?: boolean - /** {default:true}, when deserializing a Long will fit it into a Number if it's smaller than 53 bits. */ - promoteLongs?: boolean - /** {default:false}, deserialize Binary data directly into node.js Buffer object. */ - promoteBuffers?: boolean - /** {default:false}, when deserializing will promote BSON values to their Node.js closest equivalent types. */ - promoteValues?: boolean - /** {default:null}, allow to specify if there what fields we wish to return as unserialized raw buffer. */ - fieldsAsRaw?: { readonly [fieldName: string]: boolean } - /** {default:false}, return BSON regular expressions as BSONRegExp instances. */ - bsonRegExp?: boolean - /** {default:false}, allows the buffer to be larger than the parsed BSON object. */ - allowObjectSmallerThanBufferSize?: boolean - } - - export interface CalculateObjectSizeOptions { - /** {default:false}, serialize the javascript functions */ - serializeFunctions?: boolean - /** {default:true}, ignore undefined fields. */ - ignoreUndefined?: boolean - } - - /** - * Base class for Long and Timestamp. - * In original js-node@1.0.x code 'Timestamp' is a 100% copy-paste of 'Long' - * with 'Long' replaced by 'Timestamp' (changed to inheritance in js-node@2.0.0) - */ - class LongLike { - /** - * @param low The low (signed) 32 bits. - * @param high The high (signed) 32 bits. - */ - constructor(low: number, high: number) - - /** Returns the sum of `this` and the `other`. */ - add(other: T): T - /** Returns the bitwise-AND of `this` and the `other`. */ - and(other: T): T - /** - * Compares `this` with the given `other`. - * @returns 0 if they are the same, 1 if the this is greater, and -1 if the given one is greater. - */ - compare(other: T): number - /** Returns `this` divided by the given `other`. */ - div(other: T): T - /** Return whether `this` equals the `other` */ - equals(other: T): boolean - /** Return the high 32-bits value. */ - getHighBits(): number - /** Return the low 32-bits value. */ - getLowBits(): number - /** Return the low unsigned 32-bits value. */ - getLowBitsUnsigned(): number - /** Returns the number of bits needed to represent the absolute value of `this`. */ - getNumBitsAbs(): number - /** Return whether `this` is greater than the `other`. */ - greaterThan(other: T): boolean - /** Return whether `this` is greater than or equal to the `other`. */ - greaterThanOrEqual(other: T): boolean - /** Return whether `this` value is negative. */ - isNegative(): boolean - /** Return whether `this` value is odd. */ - isOdd(): boolean - /** Return whether `this` value is zero. */ - isZero(): boolean - /** Return whether `this` is less than the `other`. */ - lessThan(other: T): boolean - /** Return whether `this` is less than or equal to the `other`. */ - lessThanOrEqual(other: T): boolean - /** Returns `this` modulo the given `other`. */ - modulo(other: T): T - /** Returns the product of `this` and the given `other`. */ - multiply(other: T): T - /** The negation of this value. */ - negate(): T - /** The bitwise-NOT of this value. */ - not(): T - /** Return whether `this` does not equal to the `other`. */ - notEquals(other: T): boolean - /** Returns the bitwise-OR of `this` and the given `other`. */ - or(other: T): T - /** - * Returns `this` with bits shifted to the left by the given amount. - * @param numBits The number of bits by which to shift. - */ - shiftLeft(numBits: number): T - /** - * Returns `this` with bits shifted to the right by the given amount. - * @param numBits The number of bits by which to shift. - */ - shiftRight(numBits: number): T - /** - * Returns `this` with bits shifted to the right by the given amount, with the new top bits matching the current sign bit. - * @param numBits The number of bits by which to shift. - */ - shiftRightUnsigned(numBits: number): T - /** Returns the difference of `this` and the given `other`. */ - subtract(other: T): T - /** Return the int value (low 32 bits). */ - toInt(): number - /** Return the JSON value. */ - toJSON(): string - /** Returns closest floating-point representation to `this` value */ - toNumber(): number - /** - * Return as a string - * @param radix the radix in which the text should be written. {default:10} - */ - toString(radix?: number): string - /** Returns the bitwise-XOR of `this` and the given `other`. */ - xor(other: T): T - } - - /** A class representation of the BSON Binary type. */ - export class Binary { - static readonly SUBTYPE_DEFAULT: number - static readonly SUBTYPE_FUNCTION: number - static readonly SUBTYPE_BYTE_ARRAY: number - static readonly SUBTYPE_UUID_OLD: number - static readonly SUBTYPE_UUID: number - static readonly SUBTYPE_MD5: number - static readonly SUBTYPE_USER_DEFINED: number - - /** - * @param buffer A buffer object containing the binary data - * @param subType Binary data subtype - */ - constructor(buffer: Buffer, subType?: number) - - /** The underlying Buffer which stores the binary data. */ - readonly buffer: Buffer - /** Binary data subtype */ - readonly sub_type?: number - - /** The length of the binary. */ - length(): number - /** Updates this binary with byte_value */ - put(byte_value: number | string): void - /** Reads length bytes starting at position. */ - read(position: number, length: number): Buffer - /** Returns the value of this binary as a string. */ - value(): string - /** Writes a buffer or string to the binary */ - write(buffer: Buffer | string, offset: number): void - } - - /** A class representation of the BSON Code type. */ - export class Code { - /** - * @param code A string or function. - * @param scope An optional scope for the function. - */ - constructor(code: string | Function, scope?: any) - - readonly code: string | Function - readonly scope?: any - } - - /** - * A class representation of the BSON DBRef type. - */ - export class DBRef { - /** - * @param namespace The collection name. - * @param oid The reference ObjectId. - * @param db Optional db name, if omitted the reference is local to the current db - */ - constructor(namespace: string, oid: ObjectId, db?: string) - namespace: string - oid: ObjectId - db?: string - } - - /** A class representation of the BSON Double type. */ - export class Double { - /** - * @param value The number we want to represent as a double. - */ - constructor(value: number) - - /** - * https://github.com/mongodb/js-bson/blob/master/lib/double.js#L17 - */ - value: number - - valueOf(): number - } - - /** A class representation of the BSON Int32 type. */ - export class Int32 { - /** - * @param value The number we want to represent as an int32. - */ - constructor(value: number) - - valueOf(): number - } - - /** A class representation of the BSON Decimal128 type. */ - export class Decimal128 { - /** Create a Decimal128 instance from a string representation. */ - static fromString(s: string): Decimal128 - - /** - * @param bytes A buffer containing the raw Decimal128 bytes. - */ - constructor(bytes: Buffer) - - /** A buffer containing the raw Decimal128 bytes. */ - readonly bytes: Buffer - - toJSON(): string - toString(): string - } - - /** - * A class representation of the BSON Long type, a 64-bit two's-complement - * integer value, which faithfully simulates the behavior of a Java "Long". This - * implementation is derived from LongLib in GWT. - */ - export class Long extends LongLike { - static readonly MAX_VALUE: Long - static readonly MIN_VALUE: Long - static readonly NEG_ONE: Long - static readonly ONE: Long - static readonly ZERO: Long - - /** Returns a Long representing the given (32-bit) integer value. */ - static fromInt(i: number): Long - /** Returns a Long representing the given value, provided that it is a finite number. Otherwise, zero is returned. */ - static fromNumber(n: number): Long - /** - * Returns a Long representing the 64-bit integer that comes by concatenating the given high and low bits. Each is assumed to use 32 bits. - * @param lowBits The low 32-bits. - * @param highBits The high 32-bits. - */ - static fromBits(lowBits: number, highBits: number): Long - /** - * Returns a Long representation of the given string - * @param opt_radix The radix in which the text is written. {default:10} - */ - static fromString(s: string, opt_radix?: number): Long - } - - /** A class representation of the BSON MaxKey type. */ - export class MaxKey { - constructor() - } - - /** A class representation of the BSON MinKey type. */ - export class MinKey { - constructor() - } - - /** A class representation of the BSON ObjectId type. */ - export class ObjectId { - /** - * Create a new ObjectId instance - * @param {(string|number|ObjectId)} id Can be a 24 byte hex string, 12 byte binary string or a Number. - */ - constructor(id?: string | number | ObjectId) - /** The generation time of this ObjectId instance */ - generationTime: number - /** If true cache the hex string representation of ObjectId */ - static cacheHexString?: boolean - /** - * Creates an ObjectId from a hex string representation of an ObjectId. - * @param {string} hexString create a ObjectId from a passed in 24 byte hexstring. - * @return {ObjectId} return the created ObjectId - */ - static createFromHexString(hexString: string): ObjectId - /** - * Creates an ObjectId from a second based number, with the rest of the ObjectId zeroed out. Used for comparisons or sorting the ObjectId. - * @param {number} time an integer number representing a number of seconds. - * @return {ObjectId} return the created ObjectId - */ - static createFromTime(time: number): ObjectId - /** - * Checks if a value is a valid bson ObjectId - * - * @return {boolean} return true if the value is a valid bson ObjectId, return false otherwise. - */ - static isValid(id: string | number | ObjectId): boolean - /** - * Compares the equality of this ObjectId with `otherID`. - * @param {ObjectId|string} otherID ObjectId instance to compare against. - * @return {boolean} the result of comparing two ObjectId's - */ - equals(otherID: ObjectId | string): boolean - /** - * Generate a 12 byte id string used in ObjectId's - * @param {number} time optional parameter allowing to pass in a second based timestamp. - * @return {string} return the 12 byte id binary string. - */ - static generate(time?: number): Buffer - /** - * Returns the generation date (accurate up to the second) that this ID was generated. - * @return {Date} the generation date - */ - getTimestamp(): Date - /** - * Return the ObjectId id as a 24 byte hex string representation - * @return {string} return the 24 byte hex string representation. - */ - toHexString(): string - } - - /** A class representation of the BSON RegExp type. */ - export class BSONRegExp { - constructor(pattern: string, options: string) - - readonly pattern: string - readonly options: string - } - - /** - * A class representation of the BSON Symbol type. - * @deprecated - */ - export class Symbol { - constructor(value: string) - - /** Access the wrapped string value. */ - valueOf(): string - } - - /** A class representation of the BSON Timestamp type. */ - export class Timestamp extends LongLike { - static readonly MAX_VALUE: Timestamp - static readonly MIN_VALUE: Timestamp - static readonly NEG_ONE: Timestamp - static readonly ONE: Timestamp - static readonly ZERO: Timestamp - - /** Returns a Timestamp represented by the given (32-bit) integer value */ - static fromInt(value: number): Timestamp - /** Returns a Timestamp representing the given number value, provided that it is a finite number. */ - static fromNumber(value: number): Timestamp - /** - * Returns a Timestamp for the given high and low bits. Each is assumed to use 32 bits. - * @param lowBits The low 32-bits. - * @param highBits The high 32-bits. - */ - static fromBits(lowBits: number, highBits: number): Timestamp - /** - * Returns a Timestamp from the given string. - * @param opt_radix The radix in which the text is written. {default:10} - */ - static fromString(str: string, opt_radix?: number): Timestamp - } - - /** - * A class representation of the BSON Map type. - * @deprecated - */ - export class Map { - constructor() - } - - export default class BSON { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - constructor(types: any[]) {} - - // BSON MAX VALUES - static readonly BSON_INT32_MAX = 0x7fffffff - static readonly BSON_INT32_MIN = -0x80000000 - - static readonly BSON_INT64_MAX = Math.pow(2, 63) - 1 - static readonly BSON_INT64_MIN = -Math.pow(2, 63) - - // JS MAX PRECISE VALUES - static readonly JS_INT_MAX = 0x20000000000000 // Any integer up to 2^53 can be precisely represented by a double. - static readonly JS_INT_MIN = -0x20000000000000 // Any integer down to -2^53 can be precisely represented by a double. - - static Binary = Binary - static Code = Code - static DBRef = DBRef - static Decimal128 = Decimal128 - static Double = Double - static Int32 = Int32 - static Long = Long - /** @deprecated */ - static Map = Map - static MaxKey = MaxKey - static MinKey = MinKey - static ObjectId = ObjectId - // special case for deprecated names - /** @deprecated */ - static ObjectID = ObjectId - static BSONRegExp = BSONRegExp - /** @deprecated */ - static Symbol = Symbol - static Timestamp = Timestamp - - // Just add constants to the Native BSON parser - static readonly BSON_BINARY_SUBTYPE_DEFAULT = 0 - static readonly BSON_BINARY_SUBTYPE_FUNCTION = 1 - static readonly BSON_BINARY_SUBTYPE_BYTE_ARRAY = 2 - static readonly BSON_BINARY_SUBTYPE_UUID = 3 - static readonly BSON_BINARY_SUBTYPE_MD5 = 4 - static readonly BSON_BINARY_SUBTYPE_USER_DEFINED = 128 - - /** - * Calculate the bson size for a passed in Javascript object. - * - * @param {Object} object the Javascript object to calculate the BSON byte size for. - * @param {CalculateObjectSizeOptions} Options - * @return {Number} returns the number of bytes the BSON object will take up. - */ - calculateObjectSize( - object: any, - options?: CalculateObjectSizeOptions, - ): number - - /** - * Serialize a Javascript object. - * - * @param object The Javascript object to serialize. - * @param options Serialize options. - * @return The Buffer object containing the serialized object. - */ - serialize(object: any, options?: SerializeOptions): Buffer - - /** - * Serialize a Javascript object using a predefined Buffer and index into the buffer, useful when pre-allocating the space for serialization. - * - * @param object The Javascript object to serialize. - * @param buffer The Buffer you pre-allocated to store the serialized BSON object. - * @param options Serialize options. - * @returns The index pointing to the last written byte in the buffer - */ - serializeWithBufferAndIndex( - object: any, - buffer: Buffer, - options?: SerializeWithBufferAndIndexOptions, - ): number - - /** - * Deserialize data as BSON. - * - * @param buffer The buffer containing the serialized set of BSON documents. - * @param options Deserialize options. - * @returns The deserialized Javascript Object. - */ - deserialize(buffer: Buffer, options?: DeserializeOptions): any - - /** - * Deserialize stream data as BSON documents. - * - * @param data The buffer containing the serialized set of BSON documents. - * @param startIndex The start index in the data Buffer where the deserialization is to start. - * @param numberOfDocuments Number of documents to deserialize - * @param documents An array where to store the deserialized documents - * @param docStartIndex The index in the documents array from where to start inserting documents - * @param options Additional options used for the deserialization - * @returns The next index in the buffer after deserialization of the `numberOfDocuments` - */ - deserializeStream( - data: Buffer, - startIndex: number, - numberOfDocuments: number, - documents: Array, - docStartIndex: number, - options?: DeserializeOptions, - ): number - } - - /** - * ObjectID (with capital "D") is deprecated. Use ObjectId (lowercase "d") - * instead. - * @deprecated - */ - export { ObjectId as ObjectID } + export * from 'bson' } diff --git a/tests/database.js b/tests/database.js index bef5ff0b06..aece6b7cdb 100644 --- a/tests/database.js +++ b/tests/database.js @@ -1,26 +1,36 @@ const { MongoMemoryServer } = require('mongodb-memory-server-core') class MemoryDatabaseServer { - constructor() { - this.mongod = new MongoMemoryServer({ + constructor() {} + + async init() { + this.mongod = await MongoMemoryServer.create({ binary: { version: process.env.MONGO_BINARY_VERSION || '4.0.22', - checkMD5: true, + skipMD5: true, }, instance: {}, autoStart: false, }) } - start() { + async start() { + if (!this.mongod) { + await this.init() + } return this.mongod.start() } stop() { - return this.mongod.stop() + if (this.mongod) { + return this.mongod.stop() + } } - getConnectionString() { + async getConnectionString() { + if (!this.mongod) { + await this.init() + } return this.mongod.getUri(true) } } diff --git a/tests/unit/backend/helpers/jest-db.ts b/tests/unit/backend/helpers/jest-db.ts index 2fa724879b..23bf306315 100644 --- a/tests/unit/backend/helpers/jest-db.ts +++ b/tests/unit/backend/helpers/jest-db.ts @@ -26,14 +26,7 @@ import MemoryDatabaseServer from 'tests/database' */ const connect = async (): Promise => { const dbUrl = await MemoryDatabaseServer.getConnectionString() - - const conn = await mongoose.connect(dbUrl, { - useNewUrlParser: true, - useUnifiedTopology: true, - useCreateIndex: true, - useFindAndModify: false, - }) - return conn + return mongoose.connect(dbUrl) } /** diff --git a/tests/unit/backend/helpers/serialize-data.ts b/tests/unit/backend/helpers/serialize-data.ts index 06950f933d..7618e800a6 100644 --- a/tests/unit/backend/helpers/serialize-data.ts +++ b/tests/unit/backend/helpers/serialize-data.ts @@ -1,3 +1,3 @@ -export const jsonParseStringify = (obj: unknown) => { +export const jsonParseStringify = (obj: T): T => { return JSON.parse(JSON.stringify(obj)) }