diff --git a/package-lock.json b/package-lock.json index 1a8fb4645e..0a7c6ea81f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "method-override": "^3.0.0", "moment": "^2.29.4", "moment-recur": "^1.0.7", - "mongoose": "^7.8.3", + "mongoose": "^8.9.5", "morgan": "^1.10.0", "nconf": "^0.12.1", "node-gcm": "^1.0.5", @@ -3047,7 +3047,7 @@ "version": "1.1.9", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", - "optional": true, + "license": "MIT", "dependencies": { "sparse-bitfield": "^3.0.3" } @@ -3677,14 +3677,15 @@ "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" }, "node_modules/@types/whatwg-url": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", - "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", "dependencies": { - "@types/node": "*", "@types/webidl-conversions": "*" } }, @@ -6401,10 +6402,10 @@ } }, "node_modules/bson": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.3.0.tgz", - "integrity": "sha512-balJfqwwTBddxfnidJZagCBPP/f48zj9Sdp3OJswREOgsJzHiQSaOIAtApSgDQFYgHqAvFkp53AFSqjMDZoTFw==", - "dev": true, + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.2.tgz", + "integrity": "sha512-5afhLTjqDSA3akH56E+/2J6kTDuSIlBxyXPdQslj9hcIgOUE378xdOfZvC/9q3LifJNI6KR/juZ+d0NRNYBwXg==", + "license": "Apache-2.0", "engines": { "node": ">=16.20.1" } @@ -13360,28 +13361,6 @@ "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz", "integrity": "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==" }, - "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/ip-address/node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" - }, - "node_modules/ip-address/node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -14296,9 +14275,10 @@ } }, "node_modules/kareem": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", - "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", "engines": { "node": ">=12.0.0" } @@ -14307,7 +14287,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-1.1.7.tgz", "integrity": "sha512-1zXg4rARjsh/VMz2jjZeTfRHbJTVNR6f2DYHbLvtUSOW1satj33Fvc7vOJ0YVWB9+/9ITJWd1QKp4w217SsiFA==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "dependencies": { "bindings": "^1.5.0", @@ -14965,8 +14945,7 @@ "node_modules/memory-pager": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "optional": true + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" }, "node_modules/meow": { "version": "3.7.0", @@ -15475,43 +15454,47 @@ } }, "node_modules/mongodb-connection-string-url": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", - "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", "dependencies": { - "@types/whatwg-url": "^8.2.1", - "whatwg-url": "^11.0.0" + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" } }, "node_modules/mongodb-connection-string-url/node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "license": "MIT", "dependencies": { - "punycode": "^2.1.1" + "punycode": "^2.3.1" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/mongodb-connection-string-url/node_modules/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==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" } }, "node_modules/mongodb-connection-string-url/node_modules/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==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz", + "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==", + "license": "MIT", "dependencies": { - "tr46": "^3.0.0", + "tr46": "^5.0.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/mongodb-core": { @@ -15596,55 +15579,64 @@ } }, "node_modules/mongoose": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.8.3.tgz", - "integrity": "sha512-eFnbkKgyVrICoHB6tVJ4uLanS7d5AIo/xHkEbQeOv6g2sD7gh/1biRwvFifsmbtkIddQVNr3ROqHik6gkknN3g==", + "version": "8.9.7", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.7.tgz", + "integrity": "sha512-mvNXmU0V8qZzMR/qoK2mjT4Ti2ALdtfS0teK+twxhlGkwzOD76V02/zWajTu2MJ7QyEmZe9OWvnJsIY0iAuX3Q==", + "license": "MIT", "dependencies": { - "bson": "^5.5.0", - "kareem": "2.5.1", - "mongodb": "5.9.2", + "bson": "^6.10.1", + "kareem": "2.6.3", + "mongodb": "~6.12.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", - "sift": "16.0.1" + "sift": "17.1.3" }, "engines": { - "node": ">=14.20.1" + "node": ">=16.20.1" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mongoose" } }, - "node_modules/mongoose/node_modules/bson": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", - "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==", + "node_modules/mongoose/node_modules/kerberos": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-2.2.1.tgz", + "integrity": "sha512-Vlyv1tjAPb0y2VIJ03dKkUjsneGIBuTkH24uGRx6/DrKpFlVuGPmct3m5aEotljVUlw7PAGWABwR5aNeW7y8Zw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "node-addon-api": "^6.1.0", + "prebuild-install": "^7.1.2" + }, "engines": { - "node": ">=14.20.1" + "node": ">=12.9.0" } }, "node_modules/mongoose/node_modules/mongodb": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", - "integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz", + "integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==", + "license": "Apache-2.0", "dependencies": { - "bson": "^5.5.0", - "mongodb-connection-string-url": "^2.6.0", - "socks": "^2.7.1" + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.1", + "mongodb-connection-string-url": "^3.0.0" }, "engines": { - "node": ">=14.20.1" - }, - "optionalDependencies": { - "@mongodb-js/saslprep": "^1.1.0" + "node": ">=16.20.1" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.0.0", - "kerberos": "^1.0.0 || ^2.0.0", - "mongodb-client-encryption": ">=2.3.0 <3", - "snappy": "^7.2.2" + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" }, "peerDependenciesMeta": { "@aws-sdk/credential-providers": { @@ -15653,6 +15645,9 @@ "@mongodb-js/zstd": { "optional": true }, + "gcp-metadata": { + "optional": true + }, "kerberos": { "optional": true }, @@ -15661,6 +15656,9 @@ }, "snappy": { "optional": true + }, + "socks": { + "optional": true } } }, @@ -15669,6 +15667,105 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/mongoose/node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/mongoose/node_modules/node-abi": { + "version": "3.74.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", + "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mongoose/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/mongoose/node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mongoose/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mongoose/node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/monk": { "version": "7.3.4", "resolved": "https://registry.npmjs.org/monk/-/monk-7.3.4.tgz", @@ -16082,7 +16179,7 @@ "version": "2.30.1", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", - "devOptional": true, + "dev": true, "dependencies": { "semver": "^5.4.1" } @@ -16091,7 +16188,7 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "devOptional": true, + "dev": true, "bin": { "semver": "bin/semver" } @@ -16281,7 +16378,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", "integrity": "sha512-6kM8CLXvuW5crTxsAtva2YLrRrDaiTIkIePWs9moLHqbFWT94WpNFjwS/5dfLfECg5i/lkmw3aoqVidxt23TEQ==", - "devOptional": true + "dev": true }, "node_modules/nopt": { "version": "1.0.10", @@ -17896,7 +17993,7 @@ "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==", - "devOptional": true, + "dev": true, "dependencies": { "detect-libc": "^1.0.3", "expand-template": "^2.0.3", @@ -17924,7 +18021,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "devOptional": true, + "dev": true, "engines": { "node": ">=0.10.0" } @@ -17933,13 +18030,13 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "devOptional": true + "dev": true }, "node_modules/prebuild-install/node_modules/are-we-there-yet": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", - "devOptional": true, + "dev": true, "dependencies": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -17949,7 +18046,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "devOptional": true, + "dev": true, "bin": { "detect-libc": "bin/detect-libc.js" }, @@ -17961,7 +18058,7 @@ "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", - "devOptional": true, + "dev": true, "dependencies": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -17977,7 +18074,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", - "devOptional": true, + "dev": true, "dependencies": { "number-is-nan": "^1.0.0" }, @@ -17989,13 +18086,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "devOptional": true + "dev": true }, "node_modules/prebuild-install/node_modules/npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "devOptional": true, + "dev": true, "dependencies": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -18007,7 +18104,7 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "devOptional": true, + "dev": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -18022,7 +18119,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "devOptional": true, + "dev": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -18031,7 +18128,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", - "devOptional": true, + "dev": true, "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -18045,7 +18142,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "devOptional": true, + "dev": true, "dependencies": { "ansi-regex": "^2.0.0" }, @@ -19576,9 +19673,10 @@ } }, "node_modules/sift": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", - "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" }, "node_modules/signal-exit": { "version": "3.0.7", @@ -19608,7 +19706,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", - "devOptional": true, + "dev": true, "dependencies": { "decompress-response": "^4.2.0", "once": "^1.3.1", @@ -19619,7 +19717,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "devOptional": true, + "dev": true, "dependencies": { "mimic-response": "^2.0.0" }, @@ -19631,7 +19729,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "devOptional": true, + "dev": true, "engines": { "node": ">=8" }, @@ -19772,15 +19870,6 @@ "node": ">=4" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, "node_modules/snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -19900,19 +19989,6 @@ "node": ">=0.10.0" } }, - "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", - "dependencies": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, "node_modules/sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", @@ -19990,7 +20066,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "optional": true, "dependencies": { "memory-pager": "^1.0.2" } diff --git a/package.json b/package.json index c99cdfd5ff..e2c46ad21b 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "method-override": "^3.0.0", "moment": "^2.29.4", "moment-recur": "^1.0.7", - "mongoose": "^7.8.3", + "mongoose": "^8.9.5", "morgan": "^1.10.0", "nconf": "^0.12.1", "node-gcm": "^1.0.5", diff --git a/test/api/unit/libs/cron.test.js b/test/api/unit/libs/cron.test.js index 2863269ff2..31d3b6488c 100644 --- a/test/api/unit/libs/cron.test.js +++ b/test/api/unit/libs/cron.test.js @@ -2,13 +2,22 @@ import moment from 'moment'; import nconf from 'nconf'; import requireAgain from 'require-again'; -import { recoverCron, cron } from '../../../../website/server/libs/cron'; +import { v4 as generateUUID } from 'uuid'; +import { + generateRes, + generateReq, + generateTodo, + generateDaily, +} from '../../../helpers/api-unit.helper'; +import { cron, cronWrapper } from '../../../../website/server/libs/cron'; import { model as User } from '../../../../website/server/models/user'; import * as Tasks from '../../../../website/server/models/task'; import common from '../../../../website/common'; import * as analytics from '../../../../website/server/libs/analyticsService'; +import { model as Group } from '../../../../website/server/models/group'; -// const scoreTask = common.ops.scoreTask; +const CRON_TIMEOUT_WAIT = new Date(5 * 60 * 1000).getTime(); +const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime(); const pathToCronLib = '../../../../website/server/libs/cron'; @@ -1200,7 +1209,7 @@ describe('cron', async () => { it('increments perfect day achievement if all (at least 1) due dailies were completed', async () => { daysMissed = 1; tasksByType.dailys[0].completed = true; - tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 }); + tasksByType.dailys[0].isDue = true; await cron({ user, tasksByType, daysMissed, analytics, @@ -1212,7 +1221,7 @@ describe('cron', async () => { it('does not increment perfect day achievement if no due dailies', async () => { daysMissed = 1; tasksByType.dailys[0].completed = true; - tasksByType.dailys[0].startDate = moment(new Date()).add({ days: 1 }); + tasksByType.dailys[0].isDue = false; await cron({ user, tasksByType, daysMissed, analytics, @@ -1224,7 +1233,7 @@ describe('cron', async () => { it('gives perfect day buff if all (at least 1) due dailies were completed', async () => { daysMissed = 1; tasksByType.dailys[0].completed = true; - tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 }); + tasksByType.dailys[0].isDue = true; const previousBuffs = user.stats.buffs.toObject(); @@ -1242,7 +1251,7 @@ describe('cron', async () => { user.preferences.sleep = true; daysMissed = 1; tasksByType.dailys[0].completed = true; - tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 }); + tasksByType.dailys[0].isDue = true; const previousBuffs = user.stats.buffs.toObject(); @@ -1259,7 +1268,7 @@ describe('cron', async () => { it('clears buffs if user does not have a perfect day (no due dailys)', async () => { daysMissed = 1; tasksByType.dailys[0].completed = true; - tasksByType.dailys[0].startDate = moment(new Date()).add({ days: 1 }); + tasksByType.dailys[0].isDue = false; user.stats.buffs = { str: 1, @@ -1488,78 +1497,6 @@ describe('cron', async () => { }); }); - describe('notifications', async () => { - it('adds a user notification', async () => { - const mpBefore = user.stats.mp; - tasksByType.dailys[0].completed = true; - - const statsComputedRes = common.statsComputed(user); - const stubbedStatsComputed = sinon.stub(common, 'statsComputed'); - stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 })); - - daysMissed = 1; - const hpBefore = user.stats.hp; - tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 }); - - await cron({ - user, tasksByType, daysMissed, analytics, - }); - - expect(user.notifications.length).to.be.greaterThan(0); - expect(user.notifications[1].type).to.equal('CRON'); - expect(user.notifications[1].data).to.eql({ - hp: user.stats.hp - hpBefore, - mp: user.stats.mp - mpBefore, - }); - - common.statsComputed.restore(); - }); - - it('condenses multiple notifications into one', async () => { - const mpBefore1 = user.stats.mp; - tasksByType.dailys[0].completed = true; - - const statsComputedRes = common.statsComputed(user); - const stubbedStatsComputed = sinon.stub(common, 'statsComputed'); - stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 })); - - daysMissed = 1; - const hpBefore1 = user.stats.hp; - tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 }); - - await cron({ - user, tasksByType, daysMissed, analytics, - }); - - expect(user.notifications.length).to.be.greaterThan(0); - expect(user.notifications[1].type).to.equal('CRON'); - expect(user.notifications[1].data).to.eql({ - hp: user.stats.hp - hpBefore1, - mp: user.stats.mp - mpBefore1, - }); - - const notifsBefore2 = user.notifications.length; - const hpBefore2 = user.stats.hp; - const mpBefore2 = user.stats.mp; - - user.lastCron = moment(new Date()).subtract({ days: 2 }); - - await cron({ - user, tasksByType, daysMissed, analytics, - }); - - expect(user.notifications.length - notifsBefore2).to.equal(0); - expect(user.notifications[0].type).to.not.equal('CRON'); - expect(user.notifications[1].type).to.equal('CRON'); - expect(user.notifications[1].data).to.eql({ - hp: user.stats.hp - hpBefore2 - (hpBefore2 - hpBefore1), - mp: user.stats.mp - mpBefore2 - (mpBefore2 - mpBefore1), - }); - expect(user.notifications[0].type).to.not.equal('CRON'); - common.statsComputed.restore(); - }); - }); - describe('private messages', async () => { let lastMessageId; @@ -1606,7 +1543,7 @@ describe('cron', async () => { await cron({ user, tasksByType, daysMissed, analytics, }); - expect(user.notifications.length).to.be.greaterThan(1); + expect(user.notifications.length).to.eql(1); expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE'); }); @@ -1820,64 +1757,258 @@ describe('cron', async () => { }); }); -describe('recoverCron', async () => { - let locals; let status; let - execStub; +describe('cron wrapper', () => { + let res; let + req; + let user; beforeEach(async () => { - execStub = sandbox.stub(); - sandbox.stub(User, 'findOne').returns({ exec: execStub }); - - status = { times: 0 }; - locals = { - user: new User({ - auth: { - local: { - username: 'username', - lowerCaseUsername: 'username', - email: 'email@example.com', - salt: 'salt', - hashed_password: 'hashed_password', // eslint-disable-line camelcase - }, - }, - }), - }; + res = generateRes(); + req = generateReq(); + user = await res.locals.user.save(); + res.analytics = analytics; }); - afterEach(async () => { + afterEach(() => { sandbox.restore(); }); - it('throws an error if user cannot be found', async () => { - execStub.returns(Promise.resolve(null)); + it('calls next when user is not attached', async () => { + res.locals.user = null; + await cronWrapper(req, res); + }); + + it('calls next when days have not been missed', async () => { + await cronWrapper(req, res); + }); + + it('should clear todos older than 30 days for free users', async () => { + user.lastCron = moment(new Date()).subtract({ days: 2 }); + const task = generateTodo(user); + task.dateCompleted = moment(new Date()).subtract({ days: 31 }); + task.completed = true; + await task.save(); + await user.save(); + + await cronWrapper(req, res); + const taskRes = await Tasks.Task.findOne({ _id: task._id }); + expect(taskRes).to.not.exist; + }); + + it('should not clear todos older than 30 days for subscribed users', async () => { + user.purchased.plan.customerId = 'subscribedId'; + user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY'); + user.lastCron = moment(new Date()).subtract({ days: 2 }); + const task = generateTodo(user); + task.dateCompleted = moment(new Date()).subtract({ days: 31 }); + task.completed = true; + await Promise.all([task.save(), user.save()]); + + await cronWrapper(req, res); + const taskRes = await Tasks.Task.findOne({ _id: task._id }); + expect(taskRes).to.exist; + }); + + it('should clear todos older than 90 days for subscribed users', async () => { + user.purchased.plan.customerId = 'subscribedId'; + user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY'); + user.lastCron = moment(new Date()).subtract({ days: 2 }); + + const task = generateTodo(user); + task.dateCompleted = moment(new Date()).subtract({ days: 91 }); + task.completed = true; + await task.save(); + await user.save(); + + await cronWrapper(req, res); + const taskRes = await Tasks.Task.findOne({ _id: task._id }); + expect(taskRes).to.not.exist; + }); + + it('should call next if user was not modified after cron', async () => { + const hpBefore = user.stats.hp; + user.lastCron = moment(new Date()).subtract({ days: 2 }); + await user.save(); + + await cronWrapper(req, res); + expect(hpBefore).to.equal(user.stats.hp); + }); + + it('runs cron if previous cron was incomplete', async () => { + user.lastCron = moment(new Date()).subtract({ days: 1 }); + user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 }); + const now = new Date(); + await user.save(); + + await cronWrapper(req, res); + expect(moment(now).isSame(user.lastCron, 'day')); + expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day')); + }); + + it('updates user.auth.timestamps.loggedin and lastCron', async () => { + user.lastCron = moment(new Date()).subtract({ days: 2 }); + const now = new Date(); + await user.save(); + + await cronWrapper(req, res); + expect(moment(now).isSame(user.lastCron, 'day')); + expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day')); + }); + + it('does damage for missing dailies', async () => { + const hpBefore = user.stats.hp; + user.lastCron = moment(new Date()).subtract({ days: 2 }); + const daily = generateDaily(user); + daily.startDate = moment(new Date()).subtract({ days: 2 }); + await daily.save(); + await user.save(); + + await cronWrapper(req, res); + const updatedUser = await User.findOne({ _id: user._id }); + expect(updatedUser.stats.hp).to.be.lessThan(hpBefore); + }); + + it('updates tasks', async () => { + user.lastCron = moment(new Date()).subtract({ days: 2 }); + const todo = generateTodo(user); + const todoValueBefore = todo.value; + await Promise.all([todo.save(), user.save()]); + + await cronWrapper(req, res); + const todoFound = await Tasks.Task.findOne({ _id: todo._id }); + expect(todoFound.value).to.be.lessThan(todoValueBefore); + }); + + it('updates large number of tasks', async () => { + user.lastCron = moment(new Date()).subtract({ days: 2 }); + const todo = generateTodo(user); + const todoValueBefore = todo.value; + const start = new Date(); + const saves = [todo.save(), user.save()]; + for (let i = 0; i < 200; i += 1) { + const newTodo = generateTodo(user); + newTodo.value = i; + saves.push(newTodo.save()); + } + await Promise.all(saves); + + await cronWrapper(req, res); + const duration = new Date() - start; + expect(duration).to.be.lessThan(1000); + const todoFound = await Tasks.Task.findOne({ _id: todo._id }); + expect(moment(start).isSame(user.lastCron, 'day')); + expect(moment(start).isSame(user.auth.timestamps.loggedin, 'day')); + expect(todoFound.value).to.be.lessThan(todoValueBefore); + }); + + it('fails entire cron if one task is failing', async () => { + const lastCron = moment(new Date()).subtract({ days: 2 }); + user.lastCron = lastCron; + const todo = generateTodo(user); + const todoValueBefore = todo.value; + const badTodo = generateTodo(user); + badTodo.text = 'bad todo'; + badTodo.attribute = 'bad'; + await Promise.all([badTodo.save({ validateBeforeSave: false }), todo.save(), user.save()]); try { - await recoverCron(status, locals); - throw new Error('no exception when user cannot be found'); + await cronWrapper(req, res); } catch (err) { - expect(err.message).to.eql(`User ${locals.user._id} not found while recovering.`); + expect(err).to.exist; + } + const todoFound = await Tasks.Task.findOne({ _id: todo._id }); + expect(moment(lastCron).isSame(user.lastCron, 'day')); + expect(todoFound.value).to.be.equal(todoValueBefore); + }); + + it('applies quest progress', async () => { + const hpBefore = user.stats.hp; + user.lastCron = moment(new Date()).subtract({ days: 2 }); + const daily = generateDaily(user); + daily.startDate = moment(new Date()).subtract({ days: 2 }); + await daily.save(); + + const questKey = 'dilatory'; + user.party.quest.key = questKey; + + const party = new Group({ + type: 'party', + name: generateUUID(), + leader: user._id, + }); + party.quest.members[user._id] = true; + party.quest.key = questKey; + await party.save(); + + user.party._id = party._id; + await user.save(); + + party.startQuest(user); + + await cronWrapper(req, res); + const updatedUser = await User.findOne({ _id: user._id }); + expect(updatedUser.stats.hp).to.be.lessThan(hpBefore); + }); + + it('cronSignature less than 5 minutes ago should error', async () => { + user.lastCron = moment(new Date()).subtract({ days: 2 }); + const now = new Date(); + await User.updateOne({ + _id: user._id, + }, { + $set: { + _cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT, + }, + }).exec(); + await user.save(); + try { + await cronWrapper(req, res); + } catch (err) { + expect(err).to.exist; } }); - it('increases status.times count and reruns up to 4 times', async () => { - execStub.returns(Promise.resolve({ _cronSignature: 'RUNNING_CRON' })); - execStub.onCall(4).returns(Promise.resolve({ _cronSignature: 'NOT_RUNNING' })); + it('cronSignature longer than an hour ago should allow cron', async () => { + user.lastCron = moment(new Date()).subtract({ days: 2 }); + const now = new Date(); + await User.updateOne({ + _id: user._id, + }, { + $set: { + _cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT, + }, + }).exec(); + await user.save(); - await recoverCron(status, locals); - - expect(status.times).to.eql(4); - expect(locals.user).to.eql({ _cronSignature: 'NOT_RUNNING' }); + await cronWrapper(req, res); + expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day')); + expect(user._cronSignature).to.be.equal('NOT_RUNNING'); }); - it('throws an error if recoverCron runs 5 times', async () => { - execStub.returns(Promise.resolve({ _cronSignature: 'RUNNING_CRON' })); + it('cron should not run more than once', async () => { + user.lastCron = moment(new Date()).subtract({ days: 2 }); + await user.save(); - try { - await recoverCron(status, locals); - throw new Error('no exception when recoverCron runs 5 times'); - } catch (err) { - expect(status.times).to.eql(5); - expect(err.message).to.eql(`Impossible to recover from cron for user ${locals.user._id}.`); - } + const result = await Promise.allSettled([ + cronWrapper(req, res), + cronWrapper(req, res), + new Promise((resolve, reject) => { + setTimeout(async () => { + try { + const runResult = await cronWrapper(req, res); + if (runResult !== null) { + reject(new Error('cron ran more than once')); + } else { + resolve(); + } + } catch (err) { + reject(err); + } + }, 200); + }), + ]); + + expect(result.filter(r => r.status === 'fulfilled')).to.have.lengthOf(2); + expect(result.filter(r => r.status === 'rejected')).to.have.lengthOf(1); }); }); diff --git a/test/api/unit/libs/mongodb.js b/test/api/unit/libs/mongodb.js index 494dfb8a08..57a01b759a 100644 --- a/test/api/unit/libs/mongodb.js +++ b/test/api/unit/libs/mongodb.js @@ -1,5 +1,4 @@ import os from 'os'; -import nconf from 'nconf'; import requireAgain from 'require-again'; const pathToMongoLib = '../../../../website/server/libs/mongodb'; @@ -29,22 +28,4 @@ describe('mongodb', () => { expect(string).to.equal('mongodb://hostname:3030'); }); }); - - describe('getDefaultConnectionOptions', () => { - it('returns development config when IS_PROD is false', () => { - sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false); - const mongoLibOverride = requireAgain(pathToMongoLib); - - const options = mongoLibOverride.getDefaultConnectionOptions(); - expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']); - }); - - it('returns production config when IS_PROD is true', () => { - sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true); - const mongoLibOverride = requireAgain(pathToMongoLib); - - const options = mongoLibOverride.getDefaultConnectionOptions(); - expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']); - }); - }); }); diff --git a/test/api/unit/middlewares/cronMiddleware.js b/test/api/unit/middlewares/cronMiddleware.js deleted file mode 100644 index ccc6ef2e50..0000000000 --- a/test/api/unit/middlewares/cronMiddleware.js +++ /dev/null @@ -1,332 +0,0 @@ -import moment from 'moment'; -import { v4 as generateUUID } from 'uuid'; -import { - generateRes, - generateReq, - generateTodo, - generateDaily, -} from '../../../helpers/api-unit.helper'; -import cronMiddleware from '../../../../website/server/middlewares/cron'; -import { model as User } from '../../../../website/server/models/user'; -import { model as Group } from '../../../../website/server/models/group'; -import * as Tasks from '../../../../website/server/models/task'; -import * as analyticsService from '../../../../website/server/libs/analyticsService'; -import * as cronLib from '../../../../website/server/libs/cron'; - -const CRON_TIMEOUT_WAIT = new Date(60 * 60 * 1000).getTime(); -const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime(); - -describe('cron middleware', () => { - let res; let - req; - let user; - - beforeEach(async () => { - res = generateRes(); - req = generateReq(); - user = await res.locals.user.save(); - res.analytics = analyticsService; - }); - - afterEach(() => { - sandbox.restore(); - }); - - it('calls next when user is not attached', done => { - res.locals.user = null; - cronMiddleware(req, res, done); - }); - - it('calls next when days have not been missed', done => { - cronMiddleware(req, res, done); - }); - - it('should clear todos older than 30 days for free users', async () => { - user.lastCron = moment(new Date()).subtract({ days: 2 }); - const task = generateTodo(user); - task.dateCompleted = moment(new Date()).subtract({ days: 31 }); - task.completed = true; - await task.save(); - await user.save(); - - await new Promise((resolve, reject) => { - cronMiddleware(req, res, err => { - if (err) return reject(err); - - Tasks.Task.findOne({ _id: task }).then(foundTask => { - expect(foundTask).to.not.exist; - resolve(); - }); - - return null; - }); - }); - }); - - it('should not clear todos older than 30 days for subscribed users', async () => { - user.purchased.plan.customerId = 'subscribedId'; - user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY'); - user.lastCron = moment(new Date()).subtract({ days: 2 }); - const task = generateTodo(user); - task.dateCompleted = moment(new Date()).subtract({ days: 31 }); - task.completed = true; - await task.save(); - await user.save(); - - await new Promise((resolve, reject) => { - cronMiddleware(req, res, err => { - if (err) return reject(err); - Tasks.Task.findOne({ _id: task }).then(foundTask => { - expect(foundTask).to.exist; - return resolve(); - }); - return null; - }); - }); - }); - - it('should clear todos older than 90 days for subscribed users', async () => { - user.purchased.plan.customerId = 'subscribedId'; - user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY'); - user.lastCron = moment(new Date()).subtract({ days: 2 }); - - const task = generateTodo(user); - task.dateCompleted = moment(new Date()).subtract({ days: 91 }); - task.completed = true; - await task.save(); - await user.save(); - - await new Promise((resolve, reject) => { - cronMiddleware(req, res, err => { - if (err) return reject(err); - Tasks.Task.findOne({ _id: task }).then(foundTask => { - expect(foundTask).to.not.exist; - return resolve(); - }); - return null; - }); - }); - }); - - it('should call next if user was not modified after cron', async () => { - const hpBefore = user.stats.hp; - user.lastCron = moment(new Date()).subtract({ days: 2 }); - await user.save(); - - await new Promise((resolve, reject) => { - cronMiddleware(req, res, err => { - if (err) return reject(err); - expect(hpBefore).to.equal(user.stats.hp); - return resolve(); - }); - }); - }); - - it('runs cron if previous cron was incomplete', async () => { - user.lastCron = moment(new Date()).subtract({ days: 1 }); - user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 }); - const now = new Date(); - await user.save(); - - await new Promise((resolve, reject) => { - cronMiddleware(req, res, err => { - if (err) return reject(err); - expect(moment(now).isSame(user.lastCron, 'day')); - expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day')); - return resolve(); - }); - }); - }); - - it('updates user.auth.timestamps.loggedin and lastCron', async () => { - user.lastCron = moment(new Date()).subtract({ days: 2 }); - const now = new Date(); - await user.save(); - - await new Promise((resolve, reject) => { - cronMiddleware(req, res, err => { - if (err) return reject(err); - expect(moment(now).isSame(user.lastCron, 'day')); - expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day')); - return resolve(); - }); - }); - }); - - it('does damage for missing dailies', async () => { - const hpBefore = user.stats.hp; - user.lastCron = moment(new Date()).subtract({ days: 2 }); - const daily = generateDaily(user); - daily.startDate = moment(new Date()).subtract({ days: 2 }); - await daily.save(); - await user.save(); - - await new Promise((resolve, reject) => { - cronMiddleware(req, res, err => { - if (err) return reject(err); - return User.findOne({ _id: user._id }).then(updatedUser => { - expect(updatedUser.stats.hp).to.be.lessThan(hpBefore); - return resolve(); - }); - }); - }); - }); - - it('updates tasks', async () => { - user.lastCron = moment(new Date()).subtract({ days: 2 }); - const todo = generateTodo(user); - const todoValueBefore = todo.value; - await Promise.all([todo.save(), user.save()]); - - await new Promise((resolve, reject) => { - cronMiddleware(req, res, err => { - if (err) return reject(err); - return Tasks.Task.findOne({ _id: todo._id }).then(todoFound => { - expect(todoFound.value).to.be.lessThan(todoValueBefore); - return resolve(); - }); - }); - }); - }); - - it('applies quest progress', async () => { - const hpBefore = user.stats.hp; - user.lastCron = moment(new Date()).subtract({ days: 2 }); - const daily = generateDaily(user); - daily.startDate = moment(new Date()).subtract({ days: 2 }); - await daily.save(); - - const questKey = 'dilatory'; - user.party.quest.key = questKey; - - const party = new Group({ - type: 'party', - name: generateUUID(), - leader: user._id, - }); - party.quest.members[user._id] = true; - party.quest.key = questKey; - await party.save(); - - user.party._id = party._id; - await user.save(); - - party.startQuest(user); - - await new Promise((resolve, reject) => { - cronMiddleware(req, res, err => { - if (err) return reject(err); - return User.findOne({ _id: user._id }).then(updatedUser => { - expect(updatedUser.stats.hp).to.be.lessThan(hpBefore); - return resolve(); - }); - }); - }); - }); - - it('recovers from failed cron and does not error when user is already cronning', async () => { - user.lastCron = moment(new Date()).subtract({ days: 2 }); - await user.save(); - - const updatedUser = user.toObject(); - updatedUser.matchedCount = 0; - - sandbox.spy(cronLib, 'recoverCron'); - - sandbox.stub(User, 'updateOne') - .withArgs({ - _id: user._id, - $or: [ - { _cronSignature: 'NOT_RUNNING' }, - { _cronSignature: { $lt: sinon.match.number } }, - ], - }) - .returns({ - exec () { - return Promise.resolve(updatedUser); - }, - }); - - await new Promise((resolve, reject) => { - cronMiddleware(req, res, err => { - if (err) return reject(err); - expect(cronLib.recoverCron).to.be.calledOnce; - - return resolve(); - }); - }); - }); - - it('cronSignature less than an hour ago should error', async () => { - user.lastCron = moment(new Date()).subtract({ days: 2 }); - const now = new Date(); - await User.updateOne({ - _id: user._id, - }, { - $set: { - _cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT, - }, - }).exec(); - await user.save(); - const expectedErrMessage = `Impossible to recover from cron for user ${user._id}.`; - - await new Promise((resolve, reject) => { - cronMiddleware(req, res, err => { - if (!err) return reject(new Error('Cron should have failed.')); - expect(err.message).to.be.equal(expectedErrMessage); - return resolve(); - }); - }); - }); - - it('cronSignature longer than an hour ago should allow cron', async () => { - user.lastCron = moment(new Date()).subtract({ days: 2 }); - const now = new Date(); - await User.updateOne({ - _id: user._id, - }, { - $set: { - _cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT, - }, - }).exec(); - await user.save(); - - await new Promise((resolve, reject) => { - cronMiddleware(req, res, err => { - if (err) return reject(err); - expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day')); - expect(user._cronSignature).to.be.equal('NOT_RUNNING'); - return resolve(); - }); - }); - }); - - it('cron should not run more than once', async () => { - user.lastCron = moment(new Date()).subtract({ days: 2 }); - await user.save(); - - sandbox.spy(cronLib, 'cron'); - - await Promise.all([new Promise((resolve, reject) => { - cronMiddleware(req, res, err => { - if (err) return reject(err); - return resolve(); - }); - }), new Promise((resolve, reject) => { - cronMiddleware(req, res, err => { - if (err) return reject(err); - return resolve(); - }); - }), new Promise((resolve, reject) => { - setTimeout(() => { - cronMiddleware(req, res, err => { - if (err) return reject(err); - return resolve(); - }); - }, 400); - }), - ]); - - expect(cronLib.cron).to.be.calledOnce; - }); -}); diff --git a/test/helpers/mongo.js b/test/helpers/mongo.js index c67a00cb57..f0224d7477 100644 --- a/test/helpers/mongo.js +++ b/test/helpers/mongo.js @@ -74,15 +74,10 @@ export async function getDocument (collectionName, doc) { } before(done => { - mongoose.connection.on('open', err => { - if (err) return done(err); - return resetHabiticaDB() - .then(() => { - done(); - }) - .catch(error => { - throw error; - }); + mongoose.connection.once('open', async err => { + if (err) throw err; + await resetHabiticaDB(); + done(); }); }); diff --git a/website/client/src/components/appFooter.vue b/website/client/src/components/appFooter.vue index f7efd3be92..ae91f5d97c 100644 --- a/website/client/src/components/appFooter.vue +++ b/website/client/src/components/appFooter.vue @@ -944,24 +944,28 @@ export default { }, async jumpTime (amount) { const response = await axios.post('/api/v4/debug/jump-time', { offsetDays: amount }); - if (amount > 0) { - Vue.config.clock.jump(amount * 24 * 60 * 60 * 1000); - } else { - Vue.config.clock.setSystemTime(moment().add(amount, 'days').toDate()); - } - this.lastTimeJump = response.data.data.time; - this.triggerGetWorldState(true); + setTimeout(() => { + if (amount > 0) { + Vue.config.clock.jump(amount * 24 * 60 * 60 * 1000); + } else { + Vue.config.clock.setSystemTime(moment().add(amount, 'days').toDate()); + } + this.lastTimeJump = response.data.data.time; + this.triggerGetWorldState(true); + }, 1000); }, async resetTime () { const response = await axios.post('/api/v4/debug/jump-time', { reset: true }); const time = new Date(response.data.data.time); - Vue.config.clock.restore(); - Vue.config.clock = sinon.useFakeTimers({ - now: time, - shouldAdvanceTime: true, - }); - this.lastTimeJump = response.data.data.time; - this.triggerGetWorldState(true); + setTimeout(() => { + Vue.config.clock.restore(); + Vue.config.clock = sinon.useFakeTimers({ + now: time, + shouldAdvanceTime: true, + }); + this.lastTimeJump = response.data.data.time; + this.triggerGetWorldState(true); + }, 1000); }, addExp () { // @TODO: Name these variables better diff --git a/website/client/src/components/avatarModal/customize-options.vue b/website/client/src/components/avatarModal/customize-options.vue index 77153f945b..2a76bd505d 100644 --- a/website/client/src/components/avatarModal/customize-options.vue +++ b/website/client/src/components/avatarModal/customize-options.vue @@ -5,8 +5,8 @@ >
-