diff --git a/.github/workflows/backend-cicd.yml b/.github/workflows/backend-cicd.yml index 22c0a10..c9dcfd8 100644 --- a/.github/workflows/backend-cicd.yml +++ b/.github/workflows/backend-cicd.yml @@ -37,7 +37,11 @@ jobs: - name: Install Dependencies run: npm ci --loglevel=error --no-audit --no-fund - # Step 4: Run unit tests and generate a coverage report + # Step 4: Run ESLint to ensure code quality + - name: Run Lint + run: npm run lint + + # Step 5: Run unit tests and generate a coverage report - name: Run Tests with Coverage run: npm run test:coverage env: diff --git a/eslint.config.js b/eslint.config.js index 30942f7..d56f67d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,7 +3,9 @@ import globals from "globals"; import { defineConfig } from "eslint/config"; export default defineConfig([ - { files: ["**/*.{js,mjs,cjs}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: { ...globals.browser, ...globals.node } } }, + // Global ignores for auto-generated and third-party code + { ignores: ["node_modules/**", "coverage/**"] }, + { files: ["**/*.{js,mjs,cjs}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: { ...globals.node } } }, { // Tests need Jest/Mocha globals and Node globals (process, __dirname, etc.) files: ["tests/**/*.{js,mjs,cjs}", "**/*.test.{js,mjs,cjs}"], diff --git a/package-lock.json b/package-lock.json index 98c6003..ae760a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "jest": "^29.7.0", "mongodb-memory-server": "^10.3.0", "nodemon": "^3.0.1", - "supertest": "^6.3.3" + "supertest": "^7.1.0" }, "engines": { "node": ">=18" @@ -91,41 +91,6 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/core/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/generator": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", @@ -160,16 +125,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -555,31 +510,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/traverse/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/@babel/types": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", @@ -658,31 +588,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/config-array/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@eslint/config-array/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/@eslint/config-helpers": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", @@ -733,24 +638,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -764,13 +651,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/@eslint/js": { "version": "9.39.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", @@ -887,6 +767,20 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", @@ -901,6 +795,58 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -1690,16 +1636,6 @@ "node": ">=8" } }, - "node_modules/babel-plugin-istanbul/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/babel-plugin-jest-hoist": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", @@ -1783,9 +1719,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.8.32", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz", - "integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==", + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.5.tgz", + "integrity": "sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1812,29 +1748,44 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -1860,9 +1811,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", - "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -1880,11 +1831,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.25", - "caniuse-lite": "^1.0.30001754", - "electron-to-chromium": "^1.5.249", + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", - "update-browserslist-db": "^1.1.4" + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -1994,9 +1945,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001757", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", - "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "version": "1.0.30001759", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", + "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", "dev": true, "funding": [ { @@ -2031,29 +1982,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -2089,6 +2017,19 @@ "fsevents": "~2.3.2" } }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -2231,18 +2172,18 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, "node_modules/cookiejar": { @@ -2322,12 +2263,20 @@ } }, "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { - "ms": "2.0.0" + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/dedent": { @@ -2464,9 +2413,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.262", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz", - "integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==", + "version": "1.5.266", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz", + "integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==", "dev": true, "license": "ISC" }, @@ -2572,14 +2521,17 @@ "license": "MIT" }, "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" - } + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/eslint": { "version": "9.39.1", @@ -2671,106 +2623,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -2919,39 +2771,39 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -2980,18 +2832,33 @@ } }, "node_modules/express-validator": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.0.tgz", - "integrity": "sha512-ujK2BX5JUun5NR4JuBo83YSXoDDIpoGz3QxgHTzQcHFevkKnwV1in4K7YNuuXQ1W3a2ObXB/P4OTnTZpUyGWiw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz", + "integrity": "sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==", "license": "MIT", "dependencies": { "lodash": "^4.17.21", - "validator": "~13.15.15" + "validator": "~13.15.23" }, "engines": { "node": ">= 8.0.0" } }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3064,23 +2931,38 @@ } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "statuses": "2.0.1", + "statuses": "~2.0.2", "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -3115,28 +2997,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/find-cache-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", + "locate-path": "^6.0.0", "path-exists": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/flat-cache": { @@ -3199,16 +3074,18 @@ } }, "node_modules/formidable": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", - "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", "dev": true, "license": "MIT", "dependencies": { "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", - "once": "^1.4.0", - "qs": "^6.11.0" + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" }, "funding": { "url": "https://ko-fi.com/tunnckoCore/commissions" @@ -3366,16 +3243,16 @@ } }, "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" } }, "node_modules/globals": { @@ -3411,13 +3288,13 @@ "license": "ISC" }, "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/has-symbols": { @@ -3477,19 +3354,23 @@ "license": "MIT" }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/https-proxy-agent": { @@ -3506,31 +3387,6 @@ "node": ">= 14" } }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -3587,16 +3443,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -3790,6 +3636,19 @@ "node": ">=10" } }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -3805,29 +3664,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-report/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/istanbul-lib-source-maps": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", @@ -3843,31 +3679,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-source-maps/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -4364,6 +4175,19 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -4449,16 +4273,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -4549,12 +4363,12 @@ } }, "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", "license": "MIT", "dependencies": { - "jws": "^3.2.2", + "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", @@ -4570,16 +4384,22 @@ "npm": ">=6" } }, - "node_modules/jsonwebtoken/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, "node_modules/jwa": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", - "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "license": "MIT", "dependencies": { "buffer-equal-constant-time": "^1.0.1", @@ -4588,12 +4408,12 @@ } }, "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -4658,16 +4478,19 @@ "license": "MIT" }, "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^4.1.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash": { @@ -4751,6 +4574,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -4881,9 +4717,10 @@ } }, "node_modules/mongodb": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", - "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", + "integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@mongodb-js/saslprep": "^1.3.0", @@ -4937,14 +4774,14 @@ } }, "node_modules/mongodb-memory-server": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-10.3.0.tgz", - "integrity": "sha512-dRNr2uEhMgjEe6kgqS+ITBKBbl2cz0DNBjNZ12BGUckvEOAHbhd3R7q/lFPSZrZ6AMKa2EOUJdAmFF1WlqSbsA==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-10.4.1.tgz", + "integrity": "sha512-XpCyV1e7QQ1lW28rgtXP4ZlX8ZfD/8z1ZGNxz2y3JrosLgDrNnYWvPjlgFj3JjboYUtlh1jF2Ez/rwsQA6cl0w==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { - "mongodb-memory-server-core": "10.3.0", + "mongodb-memory-server-core": "10.4.1", "tslib": "^2.8.1" }, "engines": { @@ -4952,9 +4789,9 @@ } }, "node_modules/mongodb-memory-server-core": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-10.3.0.tgz", - "integrity": "sha512-tp+ZfTBAPqHXjROhAFg6HcVVzXaEhh/iHcbY7QPOIiLwr94OkBFAw4pixyGSfP5wI2SZeEA13lXyRmBAhugWgA==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-10.4.1.tgz", + "integrity": "sha512-YJdrEyF9hk64nfeoVDMP6IfTzK+gLZhrQqYyP6JJMsqo2LK5eF7JRZ4YPQDmt1re/JhItpiU+ypiZbIG1OsW5Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4988,35 +4825,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mongodb-memory-server-core/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/mongodb-memory-server-core/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=10" } }, - "node_modules/mongodb-memory-server-core/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/mongoose": { - "version": "8.19.3", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.19.3.tgz", - "integrity": "sha512-fTAGaIohkk8wCggMuBuqTVD4YrM1/J8cBr1ekqzFqtz65qkLjtX2dcy3NH1e+2rk2365dyrrsPAnt4YTxBhEiQ==", + "version": "8.20.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.20.2.tgz", + "integrity": "sha512-U0TPupnqBOAI3p9H9qdShX8/nJUBylliRcHFKuhbewEkM7Y0qc9BbrQR9h4q6+1easoZqej7cq2Ee36AZ0gMzQ==", "license": "MIT", "dependencies": { "bson": "^6.10.4", @@ -5035,11 +4860,51 @@ "url": "https://opencollective.com/mongoose" } }, - "node_modules/mongoose/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "node_modules/mongoose/node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@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.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } }, "node_modules/mpath": { "version": "0.9.0", @@ -5062,35 +4927,12 @@ "node": ">=14.0.0" } }, - "node_modules/mquery/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mquery/node_modules/ms": { + "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5120,31 +4962,6 @@ "node": ">=12.22.0" } }, - "node_modules/new-find-package-json/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/new-find-package-json/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -5188,30 +5005,41 @@ "url": "https://opencollective.com/nodemon" } }, - "node_modules/nodemon/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, "engines": { - "node": ">=6.0" + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "engines": { + "node": ">=10" } }, - "node_modules/nodemon/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -5330,29 +5158,16 @@ } }, "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5502,6 +5317,62 @@ "node": ">=8" } }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5601,12 +5472,12 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -5625,15 +5496,15 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" @@ -5703,7 +5574,7 @@ "node": ">=8" } }, - "node_modules/resolve-from": { + "node_modules/resolve-cwd/node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", @@ -5713,6 +5584,16 @@ "node": ">=8" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/resolve.exports": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", @@ -5750,27 +5631,25 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" } }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz", + "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==", "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", @@ -5785,20 +5664,45 @@ "node": ">= 0.8.0" } }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, "engines": { "node": ">= 0.8" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, "node_modules/serve-static": { "version": "1.16.2", @@ -5815,6 +5719,79 @@ "node": ">= 0.8.0" } }, + "node_modules/serve-static/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-static/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -5942,6 +5919,19 @@ "node": ">=10" } }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -6009,10 +5999,20 @@ "node": ">=10" } }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -6106,44 +6106,24 @@ } }, "node_modules/superagent": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", - "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", - "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", + "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", "dev": true, "license": "MIT", "dependencies": { - "component-emitter": "^1.3.0", + "component-emitter": "^1.3.1", "cookiejar": "^2.1.4", - "debug": "^4.3.4", + "debug": "^4.3.7", "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", - "formidable": "^2.1.2", + "form-data": "^4.0.4", + "formidable": "^3.5.4", "methods": "^1.1.2", "mime": "2.6.0", - "qs": "^6.11.0", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=6.4.0 <13 || >=14" - } - }, - "node_modules/superagent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" + "qs": "^6.11.2" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=14.18.0" } }, "node_modules/superagent/node_modules/mime": { @@ -6159,39 +6139,31 @@ "node": ">=4.0.0" } }, - "node_modules/superagent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/supertest": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz", - "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", - "deprecated": "Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", + "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", "dev": true, "license": "MIT", "dependencies": { "methods": "^1.1.2", - "superagent": "^8.1.2" + "superagent": "^10.2.3" }, "engines": { - "node": ">=6.4.0" + "node": ">=14.18.0" } }, "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -6399,9 +6371,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index a77c168..846e6b5 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "start": "node server.js", "dev": "nodemon server.js", - "lint": "eslint .", + "lint": "eslint . --max-warnings 0", "test": "cross-env NODE_ENV=test node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand", "test:watch": "cross-env NODE_ENV=test node --experimental-vm-modules node_modules/jest/bin/jest.js --watch", "test:coverage": "cross-env NODE_ENV=test node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --coverage" @@ -52,6 +52,6 @@ "jest": "^29.7.0", "mongodb-memory-server": "^10.3.0", "nodemon": "^3.0.1", - "supertest": "^6.3.3" + "supertest": "^7.1.0" } } \ No newline at end of file diff --git a/tests/integration/admin.test.js b/tests/integration/admin.test.js index 7db8bf3..433aeeb 100644 --- a/tests/integration/admin.test.js +++ b/tests/integration/admin.test.js @@ -15,9 +15,9 @@ describe('Admin Controller - Integration Tests', () => { beforeEach(async () => { // Create Admin User with role: 'admin' - const adminAuth = await createAuthenticatedUser({ - userId: 9001, - username: 'admin', + const adminAuth = await createAuthenticatedUser({ + userId: 9001, + username: 'admin', email: 'admin@test.com', role: 'admin' }); @@ -25,9 +25,9 @@ describe('Admin Controller - Integration Tests', () => { adminToken = adminAuth.token; // Create Regular User with role: 'user' - const regularAuth = await createAuthenticatedUser({ - userId: 9002, - username: 'regular', + const regularAuth = await createAuthenticatedUser({ + userId: 9002, + username: 'regular', email: 'regular@test.com', role: 'user' }); @@ -46,14 +46,14 @@ describe('Admin Controller - Integration Tests', () => { rating: 4.5 }); - const report1 = await db.addReport({ + await db.addReport({ placeId: place.placeId, userId: regularUser.userId, reason: 'INAPPROPRIATE_CONTENT', description: 'This place has inappropriate content' }); - const report2 = await db.addReport({ + await db.addReport({ placeId: place.placeId, userId: regularUser.userId, reason: 'INCORRECT_INFORMATION', @@ -702,7 +702,7 @@ describe('Admin Controller - Integration Tests', () => { const originalEnv = process.env.NODE_ENV; const originalUser = process.env.ADMIN_DEV_USER; const originalPass = process.env.ADMIN_DEV_PASS; - + process.env.NODE_ENV = 'development'; delete process.env.ADMIN_DEV_USER; delete process.env.ADMIN_DEV_PASS; diff --git a/tests/integration/database.mongo.test.js b/tests/integration/database.mongo.test.js index 1c23dcf..ad11ce4 100644 --- a/tests/integration/database.mongo.test.js +++ b/tests/integration/database.mongo.test.js @@ -30,7 +30,7 @@ describe('Database Integration Tests', () => { // Ensure clean state before each test try { await db.clearAllData(); - } catch (error) { + } catch { // DB might not be connected yet, ignore } }); @@ -52,7 +52,7 @@ describe('Database Integration Tests', () => { const counterName = 'testCounter'; const first = await db.getNextId(counterName); const second = await db.getNextId(counterName); - + expect(second).toBe(first + 1); expect(typeof first).toBe('number'); expect(typeof second).toBe('number'); @@ -83,19 +83,19 @@ describe('Database Integration Tests', () => { test('should clear all data from database', async () => { // Create test data - await models.User.create({ - userId: 9999, - name: 'Tester', - email: 'tester@example.com', - password: 'pass' + await models.User.create({ + userId: 9999, + name: 'Tester', + email: 'tester@example.com', + password: 'pass' }); - + let count = await models.User.countDocuments(); expect(count).toBeGreaterThan(0); // Clear all data await db.clearAllData(); - + count = await models.User.countDocuments(); expect(count).toBe(0); }); @@ -107,12 +107,12 @@ describe('Database Integration Tests', () => { await db.disconnectDB(); await db.clearAllData(); process.env.DISABLE_SEEDING = 'false'; - + await db.connectDB(); const userCount = await models.User.countDocuments(); expect(userCount).toBe(seedData.users.length); - + // Verify data integrity const users = await models.User.find({}); expect(users).toHaveLength(seedData.users.length); @@ -123,7 +123,7 @@ describe('Database Integration Tests', () => { await db.disconnectDB(); await db.clearAllData(); process.env.DISABLE_SEEDING = 'true'; - + await db.connectDB(); const userCount = await models.User.countDocuments(); diff --git a/tests/integration/favourite.test.js b/tests/integration/favourite.test.js index 89643fe..894de03 100644 --- a/tests/integration/favourite.test.js +++ b/tests/integration/favourite.test.js @@ -25,7 +25,7 @@ import { api, createAuthenticatedUser, generateTestJWT, authRequest } from '../h import db from '../../config/db.js'; describe('Favourite Controller - Integration Tests', () => { - + describe('GET /users/:userId/favourite-places', () => { describe('Happy Path - Successful Retrieval of Favourites', () => { it('should return 200 and empty array when there are no favourites', async () => { @@ -75,7 +75,7 @@ describe('Favourite Controller - Integration Tests', () => { it('should return multiple favourite places', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + const place1 = await db.createPlace({ name: 'Museum 1', category: 'museum', @@ -128,7 +128,7 @@ describe('Favourite Controller - Integration Tests', () => { it('should return 403 when user tries to view another user\'s favourites', async () => { // Arrange - Two users - const { user: user1, token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); + const { token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); const { user: user2 } = await createAuthenticatedUser({ email: 'user2@example.com' }); // Act - User1 tries to view User2's favourites @@ -317,7 +317,7 @@ describe('Favourite Controller - Integration Tests', () => { it('should return 403 when user tries to add to another user\'s favourites', async () => { // Arrange - const { user: user1, token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); + const { token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); const { user: user2 } = await createAuthenticatedUser({ email: 'user2@example.com' }); const testPlace = await db.createPlace({ name: 'Test Place', @@ -404,7 +404,7 @@ describe('Favourite Controller - Integration Tests', () => { // Arrange - Two users const { user: user1, token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); const { user: user2 } = await createAuthenticatedUser({ email: 'user2@example.com' }); - + const testPlace = await db.createPlace({ name: 'Test Place', category: 'restaurant', @@ -439,9 +439,9 @@ describe('Favourite Controller - Integration Tests', () => { it('should return 403 when user tries to remove from another user\'s favourites', async () => { // Arrange - const { user: user1, token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); + const { token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); const { user: user2 } = await createAuthenticatedUser({ email: 'user2@example.com' }); - + const testPlace = await db.createPlace({ name: 'Test Place', category: 'cafe', diff --git a/tests/integration/navigation.test.js b/tests/integration/navigation.test.js index ee0fa55..d609c0d 100644 --- a/tests/integration/navigation.test.js +++ b/tests/integration/navigation.test.js @@ -8,13 +8,11 @@ import { api, createAuthenticatedUser } from '../helpers/testUtils.js'; describe('Navigation Controller - Integration Tests', () => { describe('GET /navigation', () => { - let user; let token; beforeEach(async () => { // Create authenticated user for each test const auth = await createAuthenticatedUser(); - user = auth.user; token = auth.token; }); diff --git a/tests/integration/place.test.js b/tests/integration/place.test.js index bd50a21..3fe42d9 100644 --- a/tests/integration/place.test.js +++ b/tests/integration/place.test.js @@ -15,11 +15,11 @@ * - GET /places/search - Search places with keywords */ -import { api, createAuthenticatedUser, createTestUser, generateTestJWT, authRequest } from '../helpers/testUtils.js'; +import { api, createAuthenticatedUser, createTestUser, authRequest } from '../helpers/testUtils.js'; import db from '../../config/db.js'; describe('Place Controller - Integration Tests', () => { - + describe('GET /places/:placeId', () => { describe('Happy Path - Successful Retrieval Place', () => { it('should return 200 and the data of the place', async () => { @@ -243,7 +243,7 @@ describe('Place Controller - Integration Tests', () => { city: 'Athens' }); - const { user, token } = await createAuthenticatedUser(); + const { token } = await createAuthenticatedUser(); // Act - Only rating, without comment const response = await authRequest(token) @@ -648,7 +648,7 @@ describe('Place Controller - Integration Tests', () => { describe('Happy Path - Successής Searchήτηση', () => { it('should return places that match with the keyword', async () => { // Arrange - Create test places - const place1 = await db.createPlace({ + await db.createPlace({ name: 'Acropolis Museum', category: 'museum', description: 'Ancient Greek artifacts', @@ -656,7 +656,7 @@ describe('Place Controller - Integration Tests', () => { country: 'Greece' }); - const place2 = await db.createPlace({ + await db.createPlace({ name: 'National Archaeological Museum', category: 'museum', description: 'Archaeological treasures', @@ -664,7 +664,7 @@ describe('Place Controller - Integration Tests', () => { country: 'Greece' }); - const place3 = await db.createPlace({ + await db.createPlace({ name: 'Taverna Plaka', category: 'restaurant', description: 'Traditional Greek food', diff --git a/tests/integration/preference.test.js b/tests/integration/preference.test.js index 51a2576..60f5631 100644 --- a/tests/integration/preference.test.js +++ b/tests/integration/preference.test.js @@ -19,7 +19,7 @@ import { api, createAuthenticatedUser, generateTestJWT, authRequest } from '../h import db from '../../config/db.js'; describe('Preference Controller - Integration Tests', () => { - + describe('GET /users/:userId/preference-profiles', () => { describe('Happy Path - Successful Retrieval Profiles', () => { it('should return 200 and empty array when do not exist profiles', async () => { @@ -39,7 +39,7 @@ describe('Preference Controller - Integration Tests', () => { it('should return the preference profiles of the user', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + // Create profile await db.addPreferenceProfile({ userId: user.userId, @@ -63,7 +63,7 @@ describe('Preference Controller - Integration Tests', () => { it('should return πολλαπλά profiles', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + await db.addPreferenceProfile({ userId: user.userId, name: 'Profile 1', @@ -111,7 +111,7 @@ describe('Preference Controller - Integration Tests', () => { it('should return 403 when userς προσπαθεί να δει profiles another', async () => { // Arrange - Two χρήστες - const { user: user1, token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); + const { token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); const { user: user2 } = await createAuthenticatedUser({ email: 'user2@example.com' }); // Act - User1 προσπαθεί να δει profiles of the User2 @@ -352,7 +352,7 @@ describe('Preference Controller - Integration Tests', () => { it('should return 403 when userς προσπαθεί να δημιουργήσει to another user', async () => { // Arrange - const { user: user1, token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); + const { token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); const { user: user2 } = await createAuthenticatedUser({ email: 'user2@example.com' }); // Act - User1 προσπαθεί να δημιουργήσει profile for User2 @@ -373,7 +373,7 @@ describe('Preference Controller - Integration Tests', () => { it('should update the preference profile', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + // Create profile const profile = await db.addPreferenceProfile({ userId: user.userId, @@ -402,7 +402,7 @@ describe('Preference Controller - Integration Tests', () => { it('should allow partial update (only name)', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + const profile = await db.addPreferenceProfile({ userId: user.userId, name: 'Original', @@ -427,7 +427,7 @@ describe('Preference Controller - Integration Tests', () => { it('should allow partial update (only categories)', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + const profile = await db.addPreferenceProfile({ userId: user.userId, name: 'My Profile', @@ -452,7 +452,7 @@ describe('Preference Controller - Integration Tests', () => { it('should support legacy selectedPreferences field', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + const profile = await db.addPreferenceProfile({ userId: user.userId, name: 'Profile', @@ -474,7 +474,7 @@ describe('Preference Controller - Integration Tests', () => { it('should contain HATEOAS links', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + const profile = await db.addPreferenceProfile({ userId: user.userId, name: 'Profile', @@ -498,7 +498,7 @@ describe('Preference Controller - Integration Tests', () => { it('should return 400 when οι categories είναι empty array', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + const profile = await db.addPreferenceProfile({ userId: user.userId, name: 'Profile', @@ -520,7 +520,7 @@ describe('Preference Controller - Integration Tests', () => { it('should return 400 when οι categories είναι invalid', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + const profile = await db.addPreferenceProfile({ userId: user.userId, name: 'Profile', @@ -564,7 +564,7 @@ describe('Preference Controller - Integration Tests', () => { // Arrange - Two χρήστες const { user: user1, token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); const { user: user2 } = await createAuthenticatedUser({ email: 'user2@example.com' }); - + // User2 δημιουργεί profile const user2Profile = await db.addPreferenceProfile({ userId: user2.userId, @@ -603,9 +603,9 @@ describe('Preference Controller - Integration Tests', () => { it('should return 403 when userς προσπαθεί να ενημερώσει profile another', async () => { // Arrange - const { user: user1, token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); + const { token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); const { user: user2 } = await createAuthenticatedUser({ email: 'user2@example.com' }); - + const user2Profile = await db.addPreferenceProfile({ userId: user2.userId, name: 'User2 Profile', @@ -630,7 +630,7 @@ describe('Preference Controller - Integration Tests', () => { it('should delete the preference profile', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + const profile = await db.addPreferenceProfile({ userId: user.userId, name: 'To Delete', @@ -648,7 +648,7 @@ describe('Preference Controller - Integration Tests', () => { it('should not exist the profile μετά the διαγραφή', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + const profile = await db.addPreferenceProfile({ userId: user.userId, name: 'To Delete', @@ -688,7 +688,7 @@ describe('Preference Controller - Integration Tests', () => { // Arrange const { user: user1, token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); const { user: user2 } = await createAuthenticatedUser({ email: 'user2@example.com' }); - + const user2Profile = await db.addPreferenceProfile({ userId: user2.userId, name: 'User2 Profile', @@ -719,9 +719,9 @@ describe('Preference Controller - Integration Tests', () => { it('should return 403 when userς προσπαθεί να διαγράψει profile another', async () => { // Arrange - const { user: user1, token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); + const { token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); const { user: user2 } = await createAuthenticatedUser({ email: 'user2@example.com' }); - + const user2Profile = await db.addPreferenceProfile({ userId: user2.userId, name: 'User2 Profile', @@ -743,7 +743,7 @@ describe('Preference Controller - Integration Tests', () => { it('must ενεργοποιεί the preference profile', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + const profile = await db.addPreferenceProfile({ userId: user.userId, name: 'Active Profile', @@ -764,7 +764,7 @@ describe('Preference Controller - Integration Tests', () => { it('should update the activeProfile of the user', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + const profile = await db.addPreferenceProfile({ userId: user.userId, name: 'Profile', @@ -785,7 +785,7 @@ describe('Preference Controller - Integration Tests', () => { it('should contain HATEOAS links', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + const profile = await db.addPreferenceProfile({ userId: user.userId, name: 'Profile', @@ -852,9 +852,9 @@ describe('Preference Controller - Integration Tests', () => { it('should return 403 when userς προσπαθεί να ενεργοποιήσει profile another', async () => { // Arrange - const { user: user1, token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); + const { token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); const { user: user2 } = await createAuthenticatedUser({ email: 'user2@example.com' }); - + const user2Profile = await db.addPreferenceProfile({ userId: user2.userId, name: 'User2 Profile', diff --git a/tests/integration/recommendation.test.js b/tests/integration/recommendation.test.js index f8ad89c..48987f9 100644 --- a/tests/integration/recommendation.test.js +++ b/tests/integration/recommendation.test.js @@ -21,7 +21,7 @@ import { api, createAuthenticatedUser, generateTestJWT, authRequest } from '../h import db from '../../config/db.js'; describe('Recommendation Controller - Integration Tests', () => { - + describe('GET /users/:userId/recommendations', () => { describe('Happy Path - Successful Retrieval Recommendations', () => { it('should return 200 and empty array when does not exist preference profile', async () => { @@ -42,7 +42,7 @@ describe('Recommendation Controller - Integration Tests', () => { it('should return places that match with τις preferences of the user', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + // Create preference profile with MUSEUM await db.addPreferenceProfile({ userId: user.userId, @@ -74,26 +74,26 @@ describe('Recommendation Controller - Integration Tests', () => { // Assert expect(response.status).toBe(200); expect(response.body.success).toBe(true); - + // Verify only MUSEUM places επιστρέφονται (μπορεί να exist and from seed data) expect(response.body.data.recommendations.length).toBeGreaterThanOrEqual(1); const allMuseums = response.body.data.recommendations.every(r => r.category === 'MUSEUM'); expect(allMuseums).toBe(true); - + // Verify the δικό μας museum exists to results const placeIds = response.body.data.recommendations.map(r => r.placeId); expect(placeIds).toContain(museumPlace.placeId); - + // Verify the beach ΔΕΝ exists expect(placeIds).not.toContain(beachPlace.placeId); - + expect(response.body.message).toMatch(/generated successfully/i); }); it('should filter correctly multiple categories', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + // Preference profile with NIGHTLIFE and SHOPPING (πιθανόν do not exist to seed data) await db.addPreferenceProfile({ userId: user.userId, @@ -133,14 +133,14 @@ describe('Recommendation Controller - Integration Tests', () => { // Assert expect(response.status).toBe(200); expect(response.body.data.recommendations.length).toBeGreaterThanOrEqual(2); - + const placeIds = response.body.data.recommendations.map(r => r.placeId); expect(placeIds).toContain(nightclub.placeId); expect(placeIds).toContain(mall.placeId); - + // Verify SPORTS place is NOT included expect(placeIds).not.toContain(stadium.placeId); - + // Verify only NIGHTLIFE and SHOPPING categories const categories = response.body.data.recommendations.map(r => r.category); categories.forEach(cat => { @@ -151,7 +151,7 @@ describe('Recommendation Controller - Integration Tests', () => { it('should exclude disliked places from the recommendations', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + // Preference profile with PARK (does not exist to seed data) await db.addPreferenceProfile({ userId: user.userId, @@ -185,11 +185,11 @@ describe('Recommendation Controller - Integration Tests', () => { // Assert expect(response.status).toBe(200); expect(response.body.data.recommendations.length).toBeGreaterThanOrEqual(1); - + // Verify park2 is in recommendations const placeIds = response.body.data.recommendations.map(r => r.placeId); expect(placeIds).toContain(park2.placeId); - + // Verify disliked place (park1) is NOT in recommendations expect(placeIds).not.toContain(park1.placeId); }); @@ -197,7 +197,7 @@ describe('Recommendation Controller - Integration Tests', () => { it('should sort places by rating (without location)', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + await db.addPreferenceProfile({ userId: user.userId, name: 'Beach Lover', @@ -235,7 +235,7 @@ describe('Recommendation Controller - Integration Tests', () => { // Assert expect(response.status).toBe(200); expect(response.body.data.recommendations.length).toBeGreaterThanOrEqual(3); - + // Verify sorting by rating (highest first) - ελέγχουwith the 3 πρώτα const ratings = response.body.data.recommendations.map(r => r.rating); expect(ratings[0]).toBeGreaterThanOrEqual(ratings[1]); @@ -245,7 +245,7 @@ describe('Recommendation Controller - Integration Tests', () => { it('should return μέχρι 10 recommendations', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + await db.addPreferenceProfile({ userId: user.userId, name: 'Park Lover', @@ -274,7 +274,7 @@ describe('Recommendation Controller - Integration Tests', () => { it('should contain reviews for each place', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + await db.addPreferenceProfile({ userId: user.userId, name: 'Culture Profile', @@ -303,16 +303,16 @@ describe('Recommendation Controller - Integration Tests', () => { // Assert expect(response.status).toBe(200); expect(response.body.data.recommendations.length).toBeGreaterThanOrEqual(1); - + // Find the place with our review const placeWithReview = response.body.data.recommendations.find(p => p.placeId === place.placeId); expect(placeWithReview).toBeDefined(); expect(placeWithReview.reviews).toHaveLength(1); expect(placeWithReview.reviews[0].comment).toBe('Great place!'); - }); it('should contain HATEOAS links', async () => { + }); it('should contain HATEOAS links', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + await db.addPreferenceProfile({ userId: user.userId, name: 'Test Profile', @@ -339,7 +339,7 @@ describe('Recommendation Controller - Integration Tests', () => { it('should return the activeProfile name', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + await db.addPreferenceProfile({ userId: user.userId, name: 'My Active Profile', @@ -365,7 +365,7 @@ describe('Recommendation Controller - Integration Tests', () => { it('should use the more πρόσφαthe profile if does not exist active', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + // Create δύο profiles await db.addPreferenceProfile({ userId: user.userId, @@ -402,7 +402,7 @@ describe('Recommendation Controller - Integration Tests', () => { it('should sort places by distance when location is provided', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + await db.addPreferenceProfile({ userId: user.userId, name: 'Restaurant Profile', @@ -437,11 +437,11 @@ describe('Recommendation Controller - Integration Tests', () => { // Assert expect(response.status).toBe(200); expect(response.body.data.recommendations.length).toBeGreaterThanOrEqual(2); - + // Find indices of test places const nearIndex = response.body.data.recommendations.findIndex(p => p.name === 'Near Restaurant'); const farIndex = response.body.data.recommendations.findIndex(p => p.name === 'Far Restaurant'); - + // Verify the κοντινό εστιατόριο είναι πρώthe (παρά the χαμηλότερο rating) expect(nearIndex).toBeGreaterThanOrEqual(0); expect(farIndex).toBeGreaterThanOrEqual(0); @@ -452,7 +452,7 @@ describe('Recommendation Controller - Integration Tests', () => { it('should filter places βάσει maxDistance', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + await db.addPreferenceProfile({ userId: user.userId, name: 'Cafe Profile', @@ -486,11 +486,11 @@ describe('Recommendation Controller - Integration Tests', () => { // Assert expect(response.status).toBe(200); expect(response.body.data.recommendations.length).toBeGreaterThanOrEqual(1); - + // Verify nearby cafe is included const names = response.body.data.recommendations.map(p => p.name); expect(names).toContain('Nearby Cafe'); - + // Verify all places are within maxDistance response.body.data.recommendations.forEach(place => { if (place.distance !== undefined) { @@ -502,7 +502,7 @@ describe('Recommendation Controller - Integration Tests', () => { it('should place locations without coordinates at the end', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + await db.addPreferenceProfile({ userId: user.userId, name: 'Shopping Profile', @@ -535,11 +535,11 @@ describe('Recommendation Controller - Integration Tests', () => { // Assert expect(response.status).toBe(200); expect(response.body.data.recommendations.length).toBeGreaterThanOrEqual(2); - + // Find indices const withLocationIndex = response.body.data.recommendations.findIndex(p => p.name === 'Mall with Location'); const withoutLocationIndex = response.body.data.recommendations.findIndex(p => p.name === 'Mall without Location'); - + // Place with location έρχεται πρώthe from the place without location expect(withLocationIndex).toBeGreaterThanOrEqual(0); expect(withoutLocationIndex).toBeGreaterThanOrEqual(0); @@ -578,7 +578,7 @@ describe('Recommendation Controller - Integration Tests', () => { it('should return 403 when userς προσπαθεί να δει recommendations another', async () => { // Arrange - Two χρήστες - const { user: user1, token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); + const { token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); const { user: user2 } = await createAuthenticatedUser({ email: 'user2@example.com' }); // Act - User1 προσπαθεί να δει recommendations of the User2 @@ -593,7 +593,7 @@ describe('Recommendation Controller - Integration Tests', () => { it('should handle user with preferences αλλά without matching places', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + await db.addPreferenceProfile({ userId: user.userId, name: 'Rare Preferences', @@ -611,7 +611,7 @@ describe('Recommendation Controller - Integration Tests', () => { it('should handle user που έχει disliked all the matching places', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + await db.addPreferenceProfile({ userId: user.userId, name: 'Museum Profile', @@ -620,7 +620,7 @@ describe('Recommendation Controller - Integration Tests', () => { // Get all MUSEUM places const allMuseums = await db.getPlacesByCategories(['MUSEUM']); - + // Dislike ALL museums for (const museum of allMuseums) { await db.addDislikedPlace(user.userId, museum.placeId); @@ -637,7 +637,7 @@ describe('Recommendation Controller - Integration Tests', () => { it('should handle invalid location parameters', async () => { // Arrange const { user, token } = await createAuthenticatedUser(); - + await db.addPreferenceProfile({ userId: user.userId, name: 'Test Profile', diff --git a/tests/integration/security.test.js b/tests/integration/security.test.js index 0e46225..4bf1444 100644 --- a/tests/integration/security.test.js +++ b/tests/integration/security.test.js @@ -90,7 +90,7 @@ describe('Security Tests', () => { describe('User Resource Access', () => { it('should prevent user from accessing another user profile', async () => { // Arrange - Create two users - const { user: user1, token: token1 } = await createAuthenticatedUser({ + const { token: token1 } = await createAuthenticatedUser({ email: 'user1profile@test.com' }); const { user: user2 } = await createAuthenticatedUser({ @@ -108,7 +108,7 @@ describe('Security Tests', () => { it('should prevent user from modifying another user favourites', async () => { // Arrange - Create two users and a place - const { user: user1, token: token1 } = await createAuthenticatedUser({ + const { token: token1 } = await createAuthenticatedUser({ email: 'user1fav@test.com' }); const { user: user2 } = await createAuthenticatedUser({ @@ -135,7 +135,7 @@ describe('Security Tests', () => { it('should prevent user from modifying another user settings', async () => { // Arrange - const { user: user1, token: token1 } = await createAuthenticatedUser({ + const { token: token1 } = await createAuthenticatedUser({ email: 'user1settings@test.com' }); const { user: user2 } = await createAuthenticatedUser({ @@ -154,7 +154,7 @@ describe('Security Tests', () => { it('should prevent user from accessing another user preference profiles', async () => { // Arrange - const { user: user1, token: token1 } = await createAuthenticatedUser({ + const { token: token1 } = await createAuthenticatedUser({ email: 'user1prefs@test.com' }); const { user: user2 } = await createAuthenticatedUser({ @@ -226,7 +226,7 @@ describe('Security Tests', () => { describe('XSS Prevention', () => { it('should sanitize XSS attempts in review comments', async () => { // Arrange - const { user, token } = await createAuthenticatedUser({ + const { token } = await createAuthenticatedUser({ email: 'xsstest@test.com' }); @@ -256,7 +256,7 @@ describe('Security Tests', () => { it('should sanitize XSS in report descriptions', async () => { // Arrange - const { user, token } = await createAuthenticatedUser({ + const { token } = await createAuthenticatedUser({ email: 'xssreport@test.com' }); diff --git a/tests/integration/user.test.js b/tests/integration/user.test.js index 00d63fa..460a011 100644 --- a/tests/integration/user.test.js +++ b/tests/integration/user.test.js @@ -18,7 +18,7 @@ import { api, createAuthenticatedUser, createTestUser, generateTestJWT, authRequest } from '../helpers/testUtils.js'; describe('User Controller - Integration Tests', () => { - + describe('GET /users/:userId/profile', () => { describe('Happy Path - Successful Retrieval Profile', () => { it('should return 200 and the profile of the user when is authenticated', async () => { @@ -251,7 +251,7 @@ describe('User Controller - Integration Tests', () => { const { user: user1, token: token1 } = await createAuthenticatedUser({ email: 'user1@example.com' }); - const user2 = await createTestUser({ email: 'user2@example.com' }); + await createTestUser({ email: 'user2@example.com' }); // Act - User1 προσπαθεί να αλλάξει email to αυτό of the User2 const response = await authRequest(token1) diff --git a/tests/unit/authService.test.js b/tests/unit/authService.test.js index 3e4dbf0..1631c54 100644 --- a/tests/unit/authService.test.js +++ b/tests/unit/authService.test.js @@ -217,7 +217,7 @@ describe('Auth Service', () => { it('should throw AuthenticationError if user has no password field', async () => { // Create user without password - const userNoPass = await db.createUser({ + await db.createUser({ name: 'No Password User', email: 'nopass@example.com', role: 'user' @@ -264,6 +264,19 @@ describe('Auth Service', () => { expect(decoded.email).toBe('auth@example.com'); expect(decoded.role).toBe('user'); }); + + it('should throw AuthenticationError with expired message for expired token', async () => { + // Create an expired token using jwt.sign directly + const jwt = await import('jsonwebtoken'); + const expiredToken = jwt.default.sign( + { userId: testUser.userId, email: 'auth@example.com', role: 'user' }, + process.env.JWT_SECRET, + { expiresIn: '-1s' } // Already expired + ); + + expect(() => authService.verifyToken(expiredToken)).toThrow(AuthenticationError); + expect(() => authService.verifyToken(expiredToken)).toThrow(/expired/); + }); }); describe('changePassword', () => { diff --git a/tests/unit/favouriteController.test.js b/tests/unit/favouriteController.test.js new file mode 100644 index 0000000..6be535d --- /dev/null +++ b/tests/unit/favouriteController.test.js @@ -0,0 +1,171 @@ +/** + * Unit Tests for Favourite Controller + * + * Tests controller error handling by mocking the db module + * to throw errors, which triggers the next(error) paths. + */ + +import { jest } from '@jest/globals'; + +// Mock the db module +const mockFindUserById = jest.fn(); +const mockFindPlaceById = jest.fn(); +const mockGetFavouritePlaces = jest.fn(); +const mockAddFavouritePlace = jest.fn(); +const mockRemoveFavouritePlace = jest.fn(); +const mockGetReviewsForPlace = jest.fn(); +const mockGetDislikedPlaces = jest.fn(); +const mockAddDislikedPlace = jest.fn(); +const mockRemoveDislikedPlace = jest.fn(); + +jest.unstable_mockModule('../../config/db.js', () => ({ + default: { + findUserById: mockFindUserById, + findPlaceById: mockFindPlaceById, + getFavouritePlaces: mockGetFavouritePlaces, + addFavouritePlace: mockAddFavouritePlace, + removeFavouritePlace: mockRemoveFavouritePlace, + getReviewsForPlace: mockGetReviewsForPlace, + getDislikedPlaces: mockGetDislikedPlaces, + addDislikedPlace: mockAddDislikedPlace, + removeDislikedPlace: mockRemoveDislikedPlace + } +})); + +// Import controller after mocking +const favouriteController = (await import('../../controllers/favouriteController.js')).default; + +describe('Favourite Controller - Error Handling', () => { + let mockReq; + let mockRes; + let mockNext; + + beforeEach(() => { + jest.clearAllMocks(); + + mockReq = { + params: { userId: '1', favouriteId: '1', dislikedId: '1' }, + body: { placeId: 1 } + }; + + mockRes = { + json: jest.fn(), + status: jest.fn().mockReturnThis(), + send: jest.fn() + }; + + mockNext = jest.fn(); + }); + + describe('getFavouritePlaces', () => { + it('should call next with error when db.findUserById throws', async () => { + const testError = new Error('Database connection failed'); + mockFindUserById.mockRejectedValue(testError); + + await favouriteController.getFavouritePlaces(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + expect(mockRes.json).not.toHaveBeenCalled(); + }); + + it('should call next with error when db.getFavouritePlaces throws', async () => { + const testError = new Error('Query failed'); + mockFindUserById.mockResolvedValue({ userId: 1 }); + mockGetFavouritePlaces.mockRejectedValue(testError); + + await favouriteController.getFavouritePlaces(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + }); + + describe('addFavouritePlace', () => { + it('should call next with error when db operations throw', async () => { + const testError = new Error('Database error'); + mockFindUserById.mockResolvedValue({ userId: 1 }); + mockFindPlaceById.mockResolvedValue({ placeId: 1, toObject: () => ({ placeId: 1 }) }); + mockAddFavouritePlace.mockResolvedValue({ favouriteId: 1 }); + mockGetReviewsForPlace.mockRejectedValue(testError); + + await favouriteController.addFavouritePlace(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + }); + + describe('removeFavouritePlace', () => { + it('should call next with error when db.findUserById throws', async () => { + const testError = new Error('Database connection failed'); + mockFindUserById.mockRejectedValue(testError); + + await favouriteController.removeFavouritePlace(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + + it('should call next with error when db.removeFavouritePlace throws', async () => { + const testError = new Error('Delete failed'); + mockFindUserById.mockResolvedValue({ userId: 1 }); + mockRemoveFavouritePlace.mockRejectedValue(testError); + + await favouriteController.removeFavouritePlace(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + }); + + describe('getDislikedPlaces', () => { + it('should call next with error when db.findUserById throws', async () => { + const testError = new Error('Database connection failed'); + mockFindUserById.mockRejectedValue(testError); + + await favouriteController.getDislikedPlaces(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + + it('should call next with error when db.getDislikedPlaces throws', async () => { + const testError = new Error('Query failed'); + mockFindUserById.mockResolvedValue({ userId: 1 }); + mockGetDislikedPlaces.mockRejectedValue(testError); + + await favouriteController.getDislikedPlaces(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + }); + + describe('addDislikedPlace', () => { + it('should call next with error when db operations throw', async () => { + const testError = new Error('Database error'); + mockFindUserById.mockResolvedValue({ userId: 1 }); + mockFindPlaceById.mockResolvedValue({ placeId: 1 }); + mockAddDislikedPlace.mockRejectedValue(testError); + + await favouriteController.addDislikedPlace(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + }); + + describe('removeDislikedPlace', () => { + it('should call next with error when db.findUserById throws', async () => { + const testError = new Error('Database connection failed'); + mockFindUserById.mockRejectedValue(testError); + + await favouriteController.removeDislikedPlace(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + + it('should call next with error when db.removeDislikedPlace throws', async () => { + const testError = new Error('Delete failed'); + mockFindUserById.mockResolvedValue({ userId: 1 }); + mockRemoveDislikedPlace.mockRejectedValue(testError); + + await favouriteController.removeDislikedPlace(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + }); +}); diff --git a/tests/unit/hateoasBuilder.test.js b/tests/unit/hateoasBuilder.test.js index 380e682..499bb24 100644 --- a/tests/unit/hateoasBuilder.test.js +++ b/tests/unit/hateoasBuilder.test.js @@ -9,7 +9,7 @@ describe('HATEOAS Builder - Unit Tests', () => { describe('userProfile links', () => { it('should generate all user profile links', () => { const links = buildHateoasLinks.userProfile(1); - + expect(links).toHaveProperty('self'); expect(links.self.href).toBe('/users/1/profile'); expect(links).toHaveProperty('edit'); @@ -23,7 +23,7 @@ describe('HATEOAS Builder - Unit Tests', () => { describe('settings links', () => { it('should generate settings links', () => { const links = buildHateoasLinks.settings(1); - + expect(links.self.href).toBe('/users/1/settings'); expect(links.update.method).toBe('PUT'); expect(links['user-profile'].href).toBe('/users/1/profile'); @@ -33,7 +33,7 @@ describe('HATEOAS Builder - Unit Tests', () => { describe('preferenceProfile links', () => { it('should generate preference profile links', () => { const links = buildHateoasLinks.preferenceProfile(1, 5); - + expect(links.self.href).toBe('/users/1/preference-profiles/5'); expect(links.edit.href).toBe('/users/1/preference-profiles/5'); expect(links.delete.href).toBe('/users/1/preference-profiles/5'); @@ -44,7 +44,7 @@ describe('HATEOAS Builder - Unit Tests', () => { describe('place links', () => { it('should generate place links', () => { const links = buildHateoasLinks.place(10); - + expect(links.self.href).toBe('/places/10'); expect(links['see-reviews'].href).toBe('/places/10/reviews'); expect(links['add-review'].href).toBe('/places/10/reviews'); @@ -55,7 +55,7 @@ describe('HATEOAS Builder - Unit Tests', () => { describe('search links', () => { it('should generate search links', () => { const links = buildHateoasLinks.search(); - + expect(links['refine-search'].href).toBe('/places/search{?keywords}'); }); }); @@ -63,7 +63,7 @@ describe('HATEOAS Builder - Unit Tests', () => { describe('recommendations links', () => { it('should generate recommendations links with user ID', () => { const links = buildHateoasLinks.recommendations(1); - + expect(links.refresh.href).toContain('/users/1/recommendations'); expect(links['user-profile'].href).toBe('/users/1/profile'); }); @@ -72,7 +72,7 @@ describe('HATEOAS Builder - Unit Tests', () => { describe('favourites links', () => { it('should generate favourites collection links', () => { const links = buildHateoasLinks.favouritesCollection(1); - + expect(links.self.href).toBe('/users/1/favourite-places'); expect(links['add-favourite'].method).toBe('POST'); expect(links['user-profile'].href).toBe('/users/1/profile'); @@ -82,7 +82,7 @@ describe('HATEOAS Builder - Unit Tests', () => { describe('navigation links', () => { it('should generate navigation links', () => { const links = buildHateoasLinks.navigation(); - + expect(links['alternative-routes'].href).toContain('/navigation'); expect(links['alternative-routes'].method).toBe('GET'); }); @@ -91,7 +91,7 @@ describe('HATEOAS Builder - Unit Tests', () => { describe('admin links', () => { it('should generate admin report collection links', () => { const links = buildHateoasLinks.adminReportsCollection(1, 10); - + expect(links.self.href).toBe('/admin/1/places/10/reports'); }); }); @@ -99,7 +99,7 @@ describe('HATEOAS Builder - Unit Tests', () => { describe('review links', () => { it('should generate review links with place ID', () => { const links = buildHateoasLinks.review(10); - + expect(links.self.href).toBe('/places/10/reviews'); expect(links.place.href).toBe('/places/10'); }); @@ -108,7 +108,7 @@ describe('HATEOAS Builder - Unit Tests', () => { describe('disliked places links', () => { it('should generate disliked place links', () => { const links = buildHateoasLinks.dislikedPlace(1); - + expect(links.recommendations.href).toContain('/users/1/recommendations'); expect(links['user-profile'].href).toBe('/users/1/profile'); }); @@ -132,4 +132,205 @@ describe('HATEOAS Builder - Unit Tests', () => { expect(links.reports.href).toBe('/admin/1/places/2/reports'); }); }); + + describe('placeWithWebsite links', () => { + it('should generate place links with website when websiteUrl is provided', () => { + const links = buildHateoasLinks.placeWithWebsite(10, 'https://example.com'); + + expect(links.self.href).toBe('/places/10'); + expect(links.website).toBeDefined(); + expect(links.website.href).toBe('https://example.com'); + expect(links.website.method).toBe('GET'); + expect(links.website.type).toBe('text/html'); + }); + + it('should not include website link when websiteUrl is null', () => { + const links = buildHateoasLinks.placeWithWebsite(10, null); + + expect(links.self.href).toBe('/places/10'); + expect(links.website).toBeUndefined(); + }); + + it('should not include website link when websiteUrl is undefined', () => { + const links = buildHateoasLinks.placeWithWebsite(10, undefined); + + expect(links.website).toBeUndefined(); + }); + + it('should not include website link when websiteUrl is empty string', () => { + const links = buildHateoasLinks.placeWithWebsite(10, ''); + + expect(links.website).toBeUndefined(); + }); + + it('should include user links when userId is provided', () => { + const links = buildHateoasLinks.placeWithWebsite(10, 'https://example.com', 1); + + expect(links.website.href).toBe('https://example.com'); + expect(links['add-to-favourites']).toBeDefined(); + expect(links['add-to-favourites'].href).toBe('/users/1/favourite-places'); + expect(links['add-to-disliked']).toBeDefined(); + expect(links['user-profile'].href).toBe('/users/1/profile'); + }); + }); + + describe('place links with userId', () => { + it('should include user-specific links when userId is provided', () => { + const links = buildHateoasLinks.place(10, 1); + + expect(links.self.href).toBe('/places/10'); + expect(links['add-to-favourites']).toBeDefined(); + expect(links['add-to-favourites'].href).toBe('/users/1/favourite-places'); + expect(links['add-to-favourites'].method).toBe('POST'); + expect(links['add-to-disliked']).toBeDefined(); + expect(links['add-to-disliked'].href).toBe('/users/1/disliked-places'); + expect(links['user-profile']).toBeDefined(); + expect(links['user-profile'].href).toBe('/users/1/profile'); + }); + + it('should not include user links when userId is null', () => { + const links = buildHateoasLinks.place(10, null); + + expect(links['add-to-favourites']).toBeUndefined(); + expect(links['add-to-disliked']).toBeUndefined(); + expect(links['user-profile']).toBeUndefined(); + }); + }); + + describe('reviews links with userId', () => { + it('should include user-profile link when userId is provided', () => { + const links = buildHateoasLinks.reviews(10, 1); + + expect(links.self.href).toBe('/places/10/reviews'); + expect(links['user-profile']).toBeDefined(); + expect(links['user-profile'].href).toBe('/users/1/profile'); + }); + + it('should not include user-profile when userId is null', () => { + const links = buildHateoasLinks.reviews(10, null); + + expect(links['user-profile']).toBeUndefined(); + }); + }); + + describe('review links with userId', () => { + it('should include user-profile link when userId is provided', () => { + const links = buildHateoasLinks.review(10, 1); + + expect(links.self.href).toBe('/places/10/reviews'); + expect(links.place.href).toBe('/places/10'); + expect(links['user-profile']).toBeDefined(); + expect(links['user-profile'].href).toBe('/users/1/profile'); + }); + }); + + describe('report links', () => { + it('should generate report links with place ID', () => { + const links = buildHateoasLinks.report(10); + + expect(links.place.href).toBe('/places/10'); + }); + + it('should include user-profile link when userId is provided', () => { + const links = buildHateoasLinks.report(10, 1); + + expect(links.place.href).toBe('/places/10'); + expect(links['user-profile']).toBeDefined(); + expect(links['user-profile'].href).toBe('/users/1/profile'); + }); + }); + + describe('search links with userId', () => { + it('should include user-profile link when userId is provided', () => { + const links = buildHateoasLinks.search(1); + + expect(links['refine-search'].href).toBe('/places/search{?keywords}'); + expect(links['user-profile']).toBeDefined(); + expect(links['user-profile'].href).toBe('/users/1/profile'); + }); + }); + + describe('navigation links with parameters', () => { + it('should include destination link when placeId is provided', () => { + const links = buildHateoasLinks.navigation(10); + + expect(links.destination).toBeDefined(); + expect(links.destination.href).toBe('/places/10'); + expect(links.destination.method).toBe('GET'); + }); + + it('should include user-profile when userId is provided', () => { + const links = buildHateoasLinks.navigation(null, 1); + + expect(links.destination).toBeUndefined(); + expect(links['user-profile']).toBeDefined(); + expect(links['user-profile'].href).toBe('/users/1/profile'); + }); + + it('should include both destination and user-profile when both are provided', () => { + const links = buildHateoasLinks.navigation(10, 1); + + expect(links.destination.href).toBe('/places/10'); + expect(links['user-profile'].href).toBe('/users/1/profile'); + }); + }); + + describe('adminReport links', () => { + it('should generate admin report links', () => { + const links = buildHateoasLinks.adminReport(10, 1); + + expect(links.place.href).toBe('/places/10'); + expect(links['edit-place']).toBeDefined(); + expect(links['edit-place'].href).toBe('/admin/1/places/10'); + expect(links['edit-place'].method).toBe('PUT'); + }); + }); + + describe('favourite links', () => { + it('should generate favourite links with all parameters', () => { + const links = buildHateoasLinks.favourite(1, 5, 10); + + expect(links.self.href).toBe('/users/1/favourite-places'); + expect(links.remove).toBeDefined(); + expect(links.remove.href).toBe('/users/1/favourite-places/5'); + expect(links.remove.method).toBe('DELETE'); + expect(links.place.href).toBe('/places/10'); + expect(links['user-profile'].href).toBe('/users/1/profile'); + }); + }); + + describe('favouriteItem links', () => { + it('should generate favourite item links', () => { + const links = buildHateoasLinks.favouriteItem(1, 5, 10); + + expect(links.select).toBeDefined(); + expect(links.select.href).toBe('/places/10'); + expect(links.select.method).toBe('GET'); + expect(links['remove-favourite']).toBeDefined(); + expect(links['remove-favourite'].href).toBe('/users/1/favourite-places/5'); + expect(links['remove-favourite'].method).toBe('DELETE'); + }); + }); + + describe('selectLink', () => { + it('should generate select link for a place', () => { + const links = buildHateoasLinks.selectLink(10); + + expect(links.select).toBeDefined(); + expect(links.select.href).toBe('/places/10'); + expect(links.select.method).toBe('GET'); + }); + }); + + describe('preferenceProfilesCollection links', () => { + it('should generate all collection links', () => { + const links = buildHateoasLinks.preferenceProfilesCollection(1); + + expect(links.self.href).toBe('/users/1/preference-profiles'); + expect(links['create-profile']).toBeDefined(); + expect(links['create-profile'].href).toBe('/users/1/preference-profiles'); + expect(links['create-profile'].method).toBe('POST'); + expect(links['user-profile'].href).toBe('/users/1/profile'); + }); + }); }); diff --git a/tests/unit/infrastructure.test.js b/tests/unit/infrastructure.test.js index 6987c6c..3954f9e 100644 --- a/tests/unit/infrastructure.test.js +++ b/tests/unit/infrastructure.test.js @@ -10,18 +10,18 @@ import { jest } from '@jest/globals'; describe('Infrastructure - Database Connection', () => { - - let connectDB, seedInitialData, disconnectDB; + + let connectDB; let mockMongoose; let originalEnv; let consoleErrorSpy; - + beforeEach(async () => { // Save original env originalEnv = { ...process.env }; // Silence expected console.error output during tests - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); + // Mock mongoose mockMongoose = { connect: jest.fn(), @@ -31,12 +31,12 @@ describe('Infrastructure - Database Connection', () => { }, disconnect: jest.fn() }; - + // Mock the mongoose module jest.unstable_mockModule('mongoose', () => ({ default: mockMongoose })); - + // Mock models to avoid real DB operations jest.unstable_mockModule('../../models/index.js', () => ({ default: { @@ -44,7 +44,7 @@ describe('Infrastructure - Database Connection', () => { Place: { countDocuments: jest.fn().mockResolvedValue(0) } } })); - + // Mock seedData jest.unstable_mockModule('../../config/seedData.js', () => ({ default: { @@ -53,7 +53,7 @@ describe('Infrastructure - Database Connection', () => { } })); }); - + afterEach(() => { // Restore env process.env = originalEnv; @@ -62,75 +62,75 @@ describe('Infrastructure - Database Connection', () => { } jest.resetModules(); }); - + describe('connectDB()', () => { - + it('must συνδέεται στο MongoDB με success', async () => { process.env.MONGODB_URI = 'mongodb://localhost:27017/test'; process.env.DISABLE_SEEDING = 'true'; - + mockMongoose.connect.mockResolvedValue(undefined); - + const database = await import('../../config/database.js'); connectDB = database.connectDB || database.default?.connectDB; - + await connectDB(); - + expect(mockMongoose.connect).toHaveBeenCalledWith('mongodb://localhost:27017/test'); }); - + it('must throw error όταν MONGODB_URI λείπει', async () => { delete process.env.MONGODB_URI; process.env.DISABLE_SEEDING = 'true'; - + const database = await import('../../config/database.js'); connectDB = database.connectDB || database.default?.connectDB; - + await expect(connectDB()).rejects.toThrow('MONGODB_URI is not defined'); }); - + it('must handle connection errors', async () => { process.env.MONGODB_URI = 'mongodb://invalid:27017/test'; process.env.DISABLE_SEEDING = 'true'; - + const connectionError = new Error('Connection failed'); mockMongoose.connect.mockRejectedValue(connectionError); - + const database = await import('../../config/database.js'); connectDB = database.connectDB || database.default?.connectDB; - + await expect(connectDB()).rejects.toThrow('Connection failed'); }); - + it('must return early αν ήδη connected', async () => { process.env.MONGODB_URI = 'mongodb://localhost:27017/test'; process.env.DISABLE_SEEDING = 'true'; - + mockMongoose.connect.mockResolvedValue(undefined); - + const database = await import('../../config/database.js'); connectDB = database.connectDB || database.default?.connectDB; - + // First connection await connectDB(); expect(mockMongoose.connect).toHaveBeenCalledTimes(1); - + // Second connection should skip await connectDB(); expect(mockMongoose.connect).toHaveBeenCalledTimes(1); // Still 1 }); - + }); - + // disconnectDB is handled by mongoose.connection.close in server.js shutdown - + }); describe('Infrastructure - MongoDB API Wrapper', () => { - + let mongoDb; let mockModels; - + beforeEach(async () => { // Mock models mockModels = { @@ -152,136 +152,136 @@ describe('Infrastructure - MongoDB API Wrapper', () => { deleteOne: jest.fn() } }; - + jest.unstable_mockModule('../../models/index.js', () => ({ default: mockModels })); - + mongoDb = await import('../../config/mongoDb.js'); }); - + afterEach(() => { jest.resetModules(); }); - + describe('Place API', () => { - + it('findPlaceById must call Place.findOne', async () => { const mockPlace = { placeId: 1, name: 'Test Place' }; mockModels.Place.findOne.mockResolvedValue(mockPlace); - + const result = await mongoDb.findPlaceById(1); - + expect(mockModels.Place.findOne).toHaveBeenCalledWith({ placeId: 1 }); expect(result).toEqual(mockPlace); }); - + it('getReviewsForPlace must call Review.find', async () => { const mockReviews = [{ reviewId: 1 }, { reviewId: 2 }]; mockModels.Review.find.mockResolvedValue(mockReviews); - + const result = await mongoDb.getReviewsForPlace(1); - + expect(mockModels.Review.find).toHaveBeenCalledWith({ placeId: 1 }); expect(result).toEqual(mockReviews); }); - + }); - + describe('User API', () => { - + it('findUserById must call User.findOne', async () => { const mockUser = { userId: 1, name: 'Test User' }; mockModels.User.findOne.mockResolvedValue(mockUser); - + const result = await mongoDb.findUserById(1); - + expect(mockModels.User.findOne).toHaveBeenCalledWith({ userId: 1 }); expect(result).toEqual(mockUser); }); - + it('findUserByEmail must call User.findOne', async () => { const mockUser = { userId: 1, email: 'test@test.com' }; mockModels.User.findOne.mockResolvedValue(mockUser); - + const result = await mongoDb.findUserByEmail('test@test.com'); - + expect(mockModels.User.findOne).toHaveBeenCalledWith({ email: 'test@test.com' }); expect(result).toEqual(mockUser); }); - + }); - + describe('Preference Profile API', () => { - + it('getPreferenceProfiles must call PreferenceProfile.find', async () => { const mockProfiles = [{ profileId: 1 }, { profileId: 2 }]; mockModels.PreferenceProfile.find.mockResolvedValue(mockProfiles); - + const result = await mongoDb.getPreferenceProfiles(1); - + expect(mockModels.PreferenceProfile.find).toHaveBeenCalledWith({ userId: 1 }); expect(result).toEqual(mockProfiles); }); - + it('updatePreferenceProfile must call findOneAndUpdate', async () => { const mockProfile = { profileId: 1, userId: 1, categories: ['food'] }; mockModels.PreferenceProfile.findOneAndUpdate.mockResolvedValue(mockProfile); - + const result = await mongoDb.updatePreferenceProfile(1, 1, { categories: ['food'] }); - + expect(mockModels.PreferenceProfile.findOneAndUpdate).toHaveBeenCalled(); expect(result).toEqual(mockProfile); }); - + it('deletePreferenceProfile must call deleteOne', async () => { mockModels.PreferenceProfile.deleteOne.mockResolvedValue({ deletedCount: 1 }); - + const result = await mongoDb.deletePreferenceProfile(1, 1); - + expect(mockModels.PreferenceProfile.deleteOne).toHaveBeenCalledWith({ userId: 1, profileId: 1 }); expect(result).toBe(true); }); - + }); - + }); describe('Infrastructure - Module Exports', () => { - + describe('routes/index.js', () => { - + it('must export router object', async () => { const routes = await import('../../routes/index.js'); const router = routes.default; - + expect(router).toBeDefined(); expect(typeof router).toBe('function'); // Express Router is a function expect(router.stack).toBeDefined(); // Router has a stack property }); - + it('must έχει mounted routes', async () => { const routes = await import('../../routes/index.js'); const router = routes.default; - + // Verify router has middleware/routes registered expect(router.stack.length).toBeGreaterThan(0); }); - + }); - + describe('models/index.js', () => { - + it('must export όλα τα models', async () => { const models = await import('../../models/index.js'); const allModels = models.default; - + expect(allModels).toBeDefined(); expect(allModels.User).toBeDefined(); expect(allModels.Place).toBeDefined(); expect(allModels.PreferenceProfile).toBeDefined(); expect(allModels.Review).toBeDefined(); - + // Some models may be undefined if mongoose mocking affects imports // Just verify the main models are present if (allModels.Report) { @@ -300,180 +300,180 @@ describe('Infrastructure - Module Exports', () => { expect(allModels.Counter).toBeDefined(); } }); - + }); - + }); describe('Infrastructure - App Initialization', () => { - + describe('app.js', () => { - + it('must export Express app instance', async () => { const appModule = await import('../../app.js'); const app = appModule.default; - + expect(app).toBeDefined(); expect(typeof app).toBe('function'); // Express app is a function expect(app.listen).toBeDefined(); // Has listen method }); - + it('must έχει middleware registered', async () => { const appModule = await import('../../app.js'); const app = appModule.default; - + // Verify app has middleware stack expect(app._router).toBeDefined(); expect(app._router.stack).toBeDefined(); expect(app._router.stack.length).toBeGreaterThan(0); }); - + }); - + }); describe('Infrastructure - Config Files', () => { - + describe('config/db.js', () => { - + it('must import χωρίς errors', async () => { const dbModule = await import('../../config/db.js'); expect(dbModule).toBeDefined(); }); - + }); - + describe('config/constants.js', () => { - + it('must export API_VERSION', async () => { const constants = await import('../../config/constants.js'); - + expect(constants.API_VERSION).toBeDefined(); expect(typeof constants.API_VERSION).toBe('string'); }); - + it('must export HTTP_STATUS', async () => { const constants = await import('../../config/constants.js'); - + expect(constants.HTTP_STATUS).toBeDefined(); expect(typeof constants.HTTP_STATUS).toBe('object'); }); - + }); - + }); describe('Infrastructure - Error Classes', () => { - + describe('utils/errors.js', () => { - + it('must export ValidationError', async () => { const errors = await import('../../utils/errors.js'); - + expect(errors.ValidationError).toBeDefined(); - + const error = new errors.ValidationError('Test error'); expect(error.message).toBe('Test error'); expect(error.statusCode).toBe(400); expect(error instanceof Error).toBe(true); }); - + it('must export NotFoundError', async () => { const errors = await import('../../utils/errors.js'); - + expect(errors.NotFoundError).toBeDefined(); - + const error = new errors.NotFoundError('User', '123'); expect(error.message).toBe('User with ID 123 not found'); expect(error.statusCode).toBe(404); }); - + it('must export AuthenticationError', async () => { const errors = await import('../../utils/errors.js'); - + expect(errors.AuthenticationError).toBeDefined(); - + const error = new errors.AuthenticationError('Unauthorized'); expect(error.message).toBe('Unauthorized'); expect(error.statusCode).toBe(401); }); - + it('must export AuthorizationError', async () => { const errors = await import('../../utils/errors.js'); - + expect(errors.AuthorizationError).toBeDefined(); - + const error = new errors.AuthorizationError('Forbidden'); expect(error.message).toBe('Forbidden'); expect(error.statusCode).toBe(403); }); - + it('must export ConflictError', async () => { const errors = await import('../../utils/errors.js'); - + expect(errors.ConflictError).toBeDefined(); - + const error = new errors.ConflictError('Conflict'); expect(error.message).toBe('Conflict'); expect(error.statusCode).toBe(409); }); - + }); - + }); describe('Infrastructure - Response Helpers', () => { - + describe('utils/responses.js', () => { - + it('must export success helper', async () => { const responses = await import('../../utils/responses.js'); - + expect(responses.sendSuccess).toBeDefined(); expect(typeof responses.sendSuccess).toBe('function'); }); - + it('must export created helper', async () => { const responses = await import('../../utils/responses.js'); - + expect(responses.sendCreated).toBeDefined(); expect(typeof responses.sendCreated).toBe('function'); }); - + it('must export noContent helper', async () => { const responses = await import('../../utils/responses.js'); - + expect(responses.sendNoContent).toBeDefined(); expect(typeof responses.sendNoContent).toBe('function'); }); - + it('success helper must δημιουργεί σωστό response', async () => { const responses = await import('../../utils/responses.js'); - + const mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn() }; - + responses.sendSuccess(mockRes, { message: 'Success' }); - + expect(mockRes.status).toHaveBeenCalledWith(200); expect(mockRes.json).toHaveBeenCalledWith({ success: true, data: { message: 'Success' } }); }); - + it('created helper must δημιουργεί σωστό response', async () => { const responses = await import('../../utils/responses.js'); - + const mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn() }; - + responses.sendCreated(mockRes, { id: 1 }, 'Created successfully'); - + expect(mockRes.status).toHaveBeenCalledWith(201); expect(mockRes.json).toHaveBeenCalledWith({ success: true, @@ -481,27 +481,27 @@ describe('Infrastructure - Response Helpers', () => { data: { id: 1 } }); }); - + }); - + }); describe('Infrastructure - HATEOAS Builder', () => { - + describe('utils/hateoasBuilder.js', () => { - + it('must import χωρίς errors', async () => { const hateoas = await import('../../utils/hateoasBuilder.js'); expect(hateoas).toBeDefined(); }); - + it('must export buildHateoasLinks object', async () => { // HateoasBuilder exports buildHateoasLinks object with methods // Just verify the file loads successfully const hateoas = await import('../../utils/hateoasBuilder.js'); expect(hateoas.default).toBeDefined(); }); - + }); - + }); diff --git a/tests/unit/middleware.simple.test.js b/tests/unit/middleware.simple.test.js index 06b36f3..615d322 100644 --- a/tests/unit/middleware.simple.test.js +++ b/tests/unit/middleware.simple.test.js @@ -4,7 +4,7 @@ import errorHandler from '../../middleware/errorHandler.js'; import { validate } from '../../middleware/validation.js'; -import { APIError, ValidationError, NotFoundError } from '../../utils/errors.js'; +import { APIError, NotFoundError } from '../../utils/errors.js'; // Simple mock for Express objects const createMockReq = () => ({ @@ -19,17 +19,17 @@ const createMockRes = () => { const res = {}; res.statusCode = 200; res.jsonData = null; - - res.status = function(code) { + + res.status = function (code) { this.statusCode = code; return this; }; - - res.json = function(data) { + + res.json = function (data) { this.jsonData = data; return this; }; - + return res; }; @@ -43,290 +43,290 @@ const createMockNext = () => { }; describe('Error Handler Middleware - Simple Unit Tests', () => { - + // Save original console.error let originalConsoleError; - + beforeEach(() => { originalConsoleError = console.error; - console.error = () => {}; // Silence error logs during tests + console.error = () => { }; // Silence error logs during tests }); - + afterEach(() => { console.error = originalConsoleError; }); - + describe('APIError Handling', () => { - + it('should include field details', () => { const error = new APIError('VALIDATION_ERROR', 'Invalid field', 400); error.field = 'email'; error.value = 'invalid@'; - + const req = createMockReq(); const res = createMockRes(); const next = createMockNext(); - + errorHandler(error, req, res, next); - + expect(res.jsonData.details.field).toBe('email'); expect(res.jsonData.details.value).toBe('invalid@'); }); - + it('should include resource details', () => { const error = new NotFoundError('User'); - + const req = createMockReq(); const res = createMockRes(); const next = createMockNext(); - + errorHandler(error, req, res, next); - + expect(res.statusCode).toBe(404); expect(res.jsonData.details.resource).toBe('User'); }); - + }); - + describe('MongoDB Errors', () => { - + it('should handle duplicate key error (11000)', () => { const error = { code: 11000, keyPattern: { email: 1 } }; - + const req = createMockReq(); const res = createMockRes(); const next = createMockNext(); - + errorHandler(error, req, res, next); - + expect(res.statusCode).toBe(409); expect(res.jsonData.error).toBe('DUPLICATE_KEY'); expect(res.jsonData.message).toBe('A record with this information already exists'); expect(res.jsonData.details.field).toBe('email'); }); - + it('should handle duplicate key without keyPattern', () => { const error = { code: 11000 }; - + const req = createMockReq(); const res = createMockRes(); const next = createMockNext(); - + errorHandler(error, req, res, next); - + expect(res.statusCode).toBe(409); expect(res.jsonData.details.field).toBeUndefined(); }); - + }); - + describe('Mongoose Validation Errors', () => { - + it('should handle ValidationError', () => { const error = new Error('Validation failed'); error.name = 'ValidationError'; error.details = { field: 'age', message: 'Must be positive' }; - + const req = createMockReq(); const res = createMockRes(); const next = createMockNext(); - + errorHandler(error, req, res, next); - + expect(res.statusCode).toBe(400); expect(res.jsonData.error).toBe('VALIDATION_ERROR'); expect(res.jsonData.details).toEqual({ field: 'age', message: 'Must be positive' }); }); - + it('should handle ValidationError without details', () => { const error = new Error('Validation failed'); error.name = 'ValidationError'; - + const req = createMockReq(); const res = createMockRes(); const next = createMockNext(); - + errorHandler(error, req, res, next); - + expect(res.jsonData.details).toEqual({}); }); - + }); - + describe('JWT Errors', () => { - + it('should handle JsonWebTokenError', () => { const error = new Error('Invalid token'); error.name = 'JsonWebTokenError'; - + const req = createMockReq(); const res = createMockRes(); const next = createMockNext(); - + errorHandler(error, req, res, next); - + expect(res.statusCode).toBe(401); expect(res.jsonData.error).toBe('INVALID_TOKEN'); expect(res.jsonData.message).toBe('Authentication token is invalid'); }); - + it('should handle TokenExpiredError', () => { const error = new Error('Token expired'); error.name = 'TokenExpiredError'; - + const req = createMockReq(); const res = createMockRes(); const next = createMockNext(); - + errorHandler(error, req, res, next); - + expect(res.statusCode).toBe(401); expect(res.jsonData.error).toBe('TOKEN_EXPIRED'); expect(res.jsonData.message).toBe('Authentication token has expired'); }); - + }); - + describe('Custom Errors with statusCode', () => { - + it('should handle error with statusCode property', () => { const error = new Error('Custom error'); error.statusCode = 418; error.error = 'TEAPOT'; error.details = { info: 'I am a teapot' }; - + const req = createMockReq(); const res = createMockRes(); const next = createMockNext(); - + errorHandler(error, req, res, next); - + expect(res.statusCode).toBe(418); expect(res.jsonData.error).toBe('TEAPOT'); expect(res.jsonData.details).toEqual({ info: 'I am a teapot' }); }); - + it('should use default error code if missing', () => { const error = new Error('Error without code'); error.statusCode = 400; - + const req = createMockReq(); const res = createMockRes(); const next = createMockNext(); - + errorHandler(error, req, res, next); - + expect(res.jsonData.error).toBe('API_ERROR'); }); - + it('should use empty details if missing', () => { const error = new Error('Error'); error.statusCode = 400; - + const req = createMockReq(); const res = createMockRes(); const next = createMockNext(); - + errorHandler(error, req, res, next); - + expect(res.jsonData.details).toEqual({}); }); - + }); - + describe('Generic Errors (500)', () => { - + it('should return 500 for unknown errors', () => { const error = new Error('Something unexpected happened'); - + const req = createMockReq(); const res = createMockRes(); const next = createMockNext(); - + // Set NODE_ENV to production to avoid stack trace const originalEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'production'; - + errorHandler(error, req, res, next); - + expect(res.statusCode).toBe(500); expect(res.jsonData.error).toBe('INTERNAL_SERVER_ERROR'); expect(res.jsonData.message).toBe('An unexpected error occurred'); expect(res.jsonData.data).toBe(null); - + process.env.NODE_ENV = originalEnv; }); - + it('should include stack trace in development mode', () => { const error = new Error('Dev error'); - + const req = createMockReq(); const res = createMockRes(); const next = createMockNext(); - + // Set NODE_ENV to development const originalEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'development'; - + errorHandler(error, req, res, next); - + expect(res.jsonData.stack).toBeDefined(); expect(typeof res.jsonData.stack).toBe('string'); - + process.env.NODE_ENV = originalEnv; }); - + }); - + describe('Console Logging', () => { - + it('should log the error to console', () => { let errorLogged = false; let loggedMessage = null; - + // Temporarily capture console.error const originalError = console.error; - console.error = (msg, err) => { + console.error = (msg) => { errorLogged = true; loggedMessage = msg; }; - + const error = new Error('Test error'); const req = createMockReq(); const res = createMockRes(); const next = createMockNext(); - + errorHandler(error, req, res, next); - + expect(errorLogged).toBe(true); expect(loggedMessage).toBe('Error:'); - + console.error = originalError; }); - + }); - + }); describe('Validation Middleware - Simple Unit Tests', () => { - + describe('validate() function', () => { - + it('should be function', () => { expect(typeof validate).toBe('function'); }); - + it('should accept 3 parameters (req, res, next)', () => { expect(validate.length).toBe(3); }); - + // Note: Full testing of validate() would require mocking validationResult // from express-validator, which is more complex. Validate is called // indirectly through integration tests, so it has coverage there. - + }); - + }); diff --git a/tests/unit/preferenceController.test.js b/tests/unit/preferenceController.test.js new file mode 100644 index 0000000..94985d5 --- /dev/null +++ b/tests/unit/preferenceController.test.js @@ -0,0 +1,177 @@ +/** + * Unit Tests for Preference Controller + * + * Tests controller error handling by mocking the db module + * to throw errors, which triggers the next(error) paths. + */ + +import { jest } from '@jest/globals'; + +// Mock the db module +const mockFindUserById = jest.fn(); +const mockGetPreferenceProfiles = jest.fn(); +const mockGetPreferenceProfile = jest.fn(); +const mockAddPreferenceProfile = jest.fn(); +const mockUpdatePreferenceProfile = jest.fn(); +const mockDeletePreferenceProfile = jest.fn(); +const mockUpdateUserById = jest.fn(); + +jest.unstable_mockModule('../../config/db.js', () => ({ + default: { + findUserById: mockFindUserById, + getPreferenceProfiles: mockGetPreferenceProfiles, + getPreferenceProfile: mockGetPreferenceProfile, + addPreferenceProfile: mockAddPreferenceProfile, + updatePreferenceProfile: mockUpdatePreferenceProfile, + deletePreferenceProfile: mockDeletePreferenceProfile, + updateUserById: mockUpdateUserById + } +})); + +// Import controller after mocking +const preferenceController = (await import('../../controllers/preferenceController.js')).default; + +describe('Preference Controller - Error Handling', () => { + let mockReq; + let mockRes; + let mockNext; + + beforeEach(() => { + jest.clearAllMocks(); + + mockReq = { + params: { userId: '1', profileId: '1' }, + body: { categories: ['MUSEUM'] } + }; + + mockRes = { + json: jest.fn(), + status: jest.fn().mockReturnThis(), + send: jest.fn() + }; + + mockNext = jest.fn(); + }); + + describe('getPreferenceProfiles', () => { + it('should call next with error when db.findUserById throws', async () => { + const testError = new Error('Database connection failed'); + mockFindUserById.mockRejectedValue(testError); + + await preferenceController.getPreferenceProfiles(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + expect(mockRes.json).not.toHaveBeenCalled(); + }); + + it('should call next with error when db.getPreferenceProfiles throws', async () => { + const testError = new Error('Query failed'); + mockFindUserById.mockResolvedValue({ userId: 1 }); + mockGetPreferenceProfiles.mockRejectedValue(testError); + + await preferenceController.getPreferenceProfiles(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + }); + + describe('createPreferenceProfile', () => { + it('should call next with error when db.findUserById throws', async () => { + const testError = new Error('Database connection failed'); + mockFindUserById.mockRejectedValue(testError); + + await preferenceController.createPreferenceProfile(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + + it('should call next with error when db.addPreferenceProfile throws', async () => { + const testError = new Error('Insert failed'); + mockFindUserById.mockResolvedValue({ userId: 1 }); + mockGetPreferenceProfiles.mockResolvedValue([]); + mockAddPreferenceProfile.mockRejectedValue(testError); + + await preferenceController.createPreferenceProfile(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + }); + + describe('updatePreferenceProfile', () => { + it('should call next with error when db.findUserById throws', async () => { + const testError = new Error('Database connection failed'); + mockFindUserById.mockRejectedValue(testError); + mockReq.body = { name: 'Updated' }; + + await preferenceController.updatePreferenceProfile(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + + it('should call next with error when db.updatePreferenceProfile throws', async () => { + const testError = new Error('Update failed'); + mockFindUserById.mockResolvedValue({ userId: 1 }); + mockGetPreferenceProfile.mockResolvedValue({ profileId: 1, categories: ['MUSEUM'] }); + mockUpdatePreferenceProfile.mockRejectedValue(testError); + mockReq.body = { name: 'Updated' }; + + await preferenceController.updatePreferenceProfile(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + }); + + describe('deletePreferenceProfile', () => { + it('should call next with error when db.findUserById throws', async () => { + const testError = new Error('Database connection failed'); + mockFindUserById.mockRejectedValue(testError); + + await preferenceController.deletePreferenceProfile(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + + it('should call next with error when db.deletePreferenceProfile throws', async () => { + const testError = new Error('Delete failed'); + mockFindUserById.mockResolvedValue({ userId: 1 }); + mockDeletePreferenceProfile.mockRejectedValue(testError); + + await preferenceController.deletePreferenceProfile(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + }); + + describe('activatePreferenceProfile', () => { + it('should call next with error when db.findUserById throws', async () => { + const testError = new Error('Database connection failed'); + mockFindUserById.mockRejectedValue(testError); + + await preferenceController.activatePreferenceProfile(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + + it('should call next with error when db.getPreferenceProfiles throws', async () => { + const testError = new Error('Query failed'); + mockFindUserById.mockResolvedValue({ userId: 1 }); + mockGetPreferenceProfiles.mockRejectedValue(testError); + + await preferenceController.activatePreferenceProfile(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + + it('should call next with error when db.updateUserById throws', async () => { + const testError = new Error('Update failed'); + mockFindUserById.mockResolvedValue({ userId: 1 }); + // First call returns profile to find + mockGetPreferenceProfiles.mockResolvedValueOnce([{ profileId: 1, categories: ['MUSEUM'] }]); + mockUpdateUserById.mockRejectedValue(testError); + + await preferenceController.activatePreferenceProfile(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + }); + }); +}); diff --git a/tests/unit/rateLimiter.test.js b/tests/unit/rateLimiter.test.js new file mode 100644 index 0000000..0d215f4 --- /dev/null +++ b/tests/unit/rateLimiter.test.js @@ -0,0 +1,52 @@ +/** + * Unit Tests for Rate Limiter Middleware + * Tests authLimiter and apiLimiter configurations + */ + +import { authLimiter, apiLimiter } from '../../middleware/rateLimiter.js'; +import rateLimiterDefault from '../../middleware/rateLimiter.js'; + +describe('Rate Limiter Middleware - Unit Tests', () => { + describe('authLimiter', () => { + it('should be defined and be a function', () => { + expect(authLimiter).toBeDefined(); + expect(typeof authLimiter).toBe('function'); + }); + + it('should be exported as part of the default export', () => { + expect(rateLimiterDefault.authLimiter).toBe(authLimiter); + }); + }); + + describe('apiLimiter', () => { + it('should be defined and be a function', () => { + expect(apiLimiter).toBeDefined(); + expect(typeof apiLimiter).toBe('function'); + }); + + it('should be exported as part of the default export', () => { + expect(rateLimiterDefault.apiLimiter).toBe(apiLimiter); + }); + }); + + describe('default export', () => { + it('should contain both authLimiter and apiLimiter', () => { + expect(rateLimiterDefault).toHaveProperty('authLimiter'); + expect(rateLimiterDefault).toHaveProperty('apiLimiter'); + }); + }); + + describe('test environment behavior', () => { + // In test environment, rate limiting is disabled (max: 0) + // We test the configuration by examining the limiter properties + + it('authLimiter should have expected configuration structure', () => { + // Rate limiters are middleware functions + expect(authLimiter.length).toBeGreaterThanOrEqual(0); + }); + + it('apiLimiter should have expected configuration structure', () => { + expect(apiLimiter.length).toBeGreaterThanOrEqual(0); + }); + }); +}); diff --git a/tests/unit/responses.test.js b/tests/unit/responses.test.js index d3d3f7f..7a2db97 100644 --- a/tests/unit/responses.test.js +++ b/tests/unit/responses.test.js @@ -16,7 +16,7 @@ import { sendPaginatedResponse } from '../../utils/responses.js'; -import { HTTP_STATUS } from '../../config/constants.js'; +// HTTP_STATUS not used in tests, responses.js tested via function calls // Simple mock για Express Response const createMockRes = () => { @@ -24,283 +24,283 @@ const createMockRes = () => { res.statusCode = 200; res.jsonData = null; res.sendCalled = false; - - res.status = function(code) { + + res.status = function (code) { this.statusCode = code; return this; }; - - res.json = function(data) { + + res.json = function (data) { this.jsonData = data; return this; }; - - res.send = function() { + + res.send = function () { this.sendCalled = true; return this; }; - + return res; }; describe('Response Utilities - Unit Tests', () => { - + describe('sendSuccess()', () => { - + describe('Happy Path - Success Responses', () => { - + it('should return 200 με data', () => { const res = createMockRes(); const testData = { userId: 1, username: 'test' }; - + sendSuccess(res, testData); - + expect(res.statusCode).toBe(200); expect(res.jsonData.success).toBe(true); expect(res.jsonData.data).toEqual(testData); }); - + it('should accept custom status code', () => { const res = createMockRes(); - + sendSuccess(res, { message: 'ok' }, 202); - + expect(res.statusCode).toBe(202); expect(res.jsonData.success).toBe(true); }); - + it('should include meta when provided', () => { const res = createMockRes(); const meta = { page: 1, total: 100 }; - + sendSuccess(res, [], 200, meta); - + expect(res.jsonData.meta).toEqual(meta); }); - + it('should NOT include meta when null', () => { const res = createMockRes(); - + sendSuccess(res, { test: 'data' }); - + expect(res.jsonData.meta).toBeUndefined(); }); - + }); - + }); - + describe('sendCreated()', () => { - + it('should return 201 με success message', () => { const res = createMockRes(); const newResource = { id: 123, name: 'New Resource' }; - + sendCreated(res, newResource); - + expect(res.statusCode).toBe(201); expect(res.jsonData.success).toBe(true); expect(res.jsonData.data).toEqual(newResource); expect(res.jsonData.message).toBe('Resource created successfully'); }); - + it('should accept custom message', () => { const res = createMockRes(); - + sendCreated(res, { id: 1 }, 'User created!'); - + expect(res.jsonData.message).toBe('User created!'); }); - + }); - + describe('sendNoContent()', () => { - + it('should return 204 without body', () => { const res = createMockRes(); - + sendNoContent(res); - + expect(res.statusCode).toBe(204); expect(res.sendCalled).toBe(true); }); - + }); - + describe('sendError()', () => { - + describe('Happy Path - Error Responses', () => { - + it('should return error με status code', () => { const res = createMockRes(); - + sendError(res, 'TEST_ERROR', 'Something went wrong', 400); - + expect(res.statusCode).toBe(400); expect(res.jsonData.success).toBe(false); expect(res.jsonData.error).toBe('TEST_ERROR'); expect(res.jsonData.message).toBe('Something went wrong'); }); - + it('should use 500 as default status', () => { const res = createMockRes(); - + sendError(res, 'ERROR', 'Error message'); - + expect(res.statusCode).toBe(500); }); - + it('should include details when provided', () => { const res = createMockRes(); const details = { field: 'email', value: 'invalid' }; - + sendError(res, 'VALIDATION', 'Invalid email', 400, details); - + expect(res.jsonData.details).toEqual(details); }); - + it('should NOT include details when null', () => { const res = createMockRes(); - + sendError(res, 'ERROR', 'Message', 400); - + expect(res.jsonData.details).toBeUndefined(); }); - + }); - + }); - + describe('sendValidationError()', () => { - + it('should return 400 με VALIDATION_ERROR', () => { const res = createMockRes(); - + sendValidationError(res, 'Validation failed'); - + expect(res.statusCode).toBe(400); expect(res.jsonData.error).toBe('VALIDATION_ERROR'); expect(res.jsonData.message).toBe('Validation failed'); }); - + it('should include validation details', () => { const res = createMockRes(); const details = { errors: [{ field: 'email', message: 'Invalid' }] }; - + sendValidationError(res, 'Validation failed', details); - + expect(res.jsonData.details).toEqual(details); }); - + }); - + describe('sendAuthError()', () => { - + it('should return 401 με AUTHENTICATION_ERROR', () => { const res = createMockRes(); - + sendAuthError(res); - + expect(res.statusCode).toBe(401); expect(res.jsonData.error).toBe('AUTHENTICATION_ERROR'); expect(res.jsonData.message).toBe('Authentication required'); }); - + it('should accept custom message', () => { const res = createMockRes(); - + sendAuthError(res, 'Invalid credentials'); - + expect(res.jsonData.message).toBe('Invalid credentials'); }); - + }); - + describe('sendForbiddenError()', () => { - + it('should return 403 με AUTHORIZATION_ERROR', () => { const res = createMockRes(); - + sendForbiddenError(res); - + expect(res.statusCode).toBe(403); expect(res.jsonData.error).toBe('AUTHORIZATION_ERROR'); expect(res.jsonData.message).toBe('Access denied'); }); - + it('should accept custom message', () => { const res = createMockRes(); - + sendForbiddenError(res, 'Admin access required'); - + expect(res.jsonData.message).toBe('Admin access required'); }); - + }); - + describe('sendNotFoundError()', () => { - + it('should return 404 με NOT_FOUND', () => { const res = createMockRes(); - + sendNotFoundError(res, 'User'); - + expect(res.statusCode).toBe(404); expect(res.jsonData.error).toBe('NOT_FOUND'); expect(res.jsonData.message).toBe('User not found'); }); - + it('should include identifier in message', () => { const res = createMockRes(); - + sendNotFoundError(res, 'User', 123); - + expect(res.jsonData.message).toBe('User with ID 123 not found'); }); - + it('should handle null identifier', () => { const res = createMockRes(); - + sendNotFoundError(res, 'Resource', null); - + expect(res.jsonData.message).toBe('Resource not found'); }); - + }); - + describe('sendConflictError()', () => { - + it('should return 409 με CONFLICT', () => { const res = createMockRes(); - + sendConflictError(res, 'Email already exists'); - + expect(res.statusCode).toBe(409); expect(res.jsonData.error).toBe('CONFLICT'); expect(res.jsonData.message).toBe('Email already exists'); }); - + it('should include conflict details', () => { const res = createMockRes(); const details = { field: 'email', value: 'test@example.com' }; - + sendConflictError(res, 'Duplicate email', details); - + expect(res.jsonData.details).toEqual(details); }); - + }); - + describe('sendPaginatedResponse()', () => { - + describe('Happy Path - Paginated Data', () => { - + it('should return paginated data με meta', () => { const res = createMockRes(); const items = [{ id: 1 }, { id: 2 }, { id: 3 }]; - + sendPaginatedResponse(res, items, 1, 10, 30); - + expect(res.jsonData.success).toBe(true); expect(res.jsonData.data).toEqual(items); expect(res.jsonData.meta.page).toBe(1); @@ -308,63 +308,63 @@ describe('Response Utilities - Unit Tests', () => { expect(res.jsonData.meta.total).toBe(30); expect(res.jsonData.meta.totalPages).toBe(3); }); - + it('should calculate correctly the totalPages', () => { const res = createMockRes(); - + sendPaginatedResponse(res, [], 1, 10, 25); - + expect(res.jsonData.meta.totalPages).toBe(3); // Math.ceil(25/10) }); - + it('should show hasNextPage correctly', () => { const res1 = createMockRes(); const res2 = createMockRes(); - + sendPaginatedResponse(res1, [], 1, 10, 30); sendPaginatedResponse(res2, [], 3, 10, 30); - + expect(res1.jsonData.meta.hasNextPage).toBe(true); expect(res2.jsonData.meta.hasNextPage).toBe(false); }); - + it('should show hasPrevPage correctly', () => { const res1 = createMockRes(); const res2 = createMockRes(); - + sendPaginatedResponse(res1, [], 1, 10, 30); sendPaginatedResponse(res2, [], 2, 10, 30); - + expect(res1.jsonData.meta.hasPrevPage).toBe(false); expect(res2.jsonData.meta.hasPrevPage).toBe(true); }); - + }); - + describe('Edge Cases', () => { - + it('should handle empty results', () => { const res = createMockRes(); - + sendPaginatedResponse(res, [], 1, 10, 0); - + expect(res.jsonData.data).toEqual([]); expect(res.jsonData.meta.total).toBe(0); expect(res.jsonData.meta.totalPages).toBe(0); }); - + it('should handle single page', () => { const res = createMockRes(); - + sendPaginatedResponse(res, [1, 2, 3], 1, 10, 3); - + expect(res.jsonData.meta.totalPages).toBe(1); expect(res.jsonData.meta.hasNextPage).toBe(false); expect(res.jsonData.meta.hasPrevPage).toBe(false); }); - + }); - + }); - + }); diff --git a/tests/unit/userController.test.js b/tests/unit/userController.test.js new file mode 100644 index 0000000..279f011 --- /dev/null +++ b/tests/unit/userController.test.js @@ -0,0 +1,168 @@ +/** + * Unit Tests for User Controller + * + * Tests controller error handling by mocking the userService + * to throw errors, which triggers the next(error) paths. + */ + +import { jest } from '@jest/globals'; + +// Mock the userService module +const mockGetUserProfile = jest.fn(); +const mockUpdateUserProfile = jest.fn(); +const mockGetUserSettings = jest.fn(); +const mockUpdateUserSettings = jest.fn(); + +jest.unstable_mockModule('../../services/userService.js', () => ({ + getUserProfile: mockGetUserProfile, + updateUserProfile: mockUpdateUserProfile, + getUserSettings: mockGetUserSettings, + updateUserSettings: mockUpdateUserSettings, + default: { + getUserProfile: mockGetUserProfile, + updateUserProfile: mockUpdateUserProfile, + getUserSettings: mockGetUserSettings, + updateUserSettings: mockUpdateUserSettings + } +})); + +// Import controller after mocking +const userController = (await import('../../controllers/userController.js')).default; + +describe('User Controller - Error Handling', () => { + let mockReq; + let mockRes; + let mockNext; + + beforeEach(() => { + jest.clearAllMocks(); + + mockReq = { + params: { userId: '1' }, + body: {} + }; + + mockRes = { + json: jest.fn(), + status: jest.fn().mockReturnThis() + }; + + mockNext = jest.fn(); + }); + + describe('getUserProfile', () => { + it('should call next with error when userService throws', async () => { + const testError = new Error('Database connection failed'); + mockGetUserProfile.mockRejectedValue(testError); + + await userController.getUserProfile(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + expect(mockRes.json).not.toHaveBeenCalled(); + }); + + it('should return user profile on success', async () => { + const mockUser = { userId: 1, name: 'Test User', email: 'test@example.com' }; + mockGetUserProfile.mockResolvedValue(mockUser); + + await userController.getUserProfile(mockReq, mockRes, mockNext); + + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + success: true, + data: expect.objectContaining({ + user: mockUser + }) + }) + ); + expect(mockNext).not.toHaveBeenCalled(); + }); + }); + + describe('updateUserProfile', () => { + it('should call next with error when userService throws', async () => { + const testError = new Error('Update failed'); + mockUpdateUserProfile.mockRejectedValue(testError); + mockReq.body = { name: 'New Name' }; + + await userController.updateUserProfile(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + expect(mockRes.json).not.toHaveBeenCalled(); + }); + + it('should return updated user on success', async () => { + const mockUpdatedUser = { userId: 1, name: 'Updated Name' }; + mockUpdateUserProfile.mockResolvedValue(mockUpdatedUser); + mockReq.body = { name: 'Updated Name' }; + + await userController.updateUserProfile(mockReq, mockRes, mockNext); + + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + success: true, + message: 'User profile updated successfully' + }) + ); + expect(mockNext).not.toHaveBeenCalled(); + }); + }); + + describe('getSettings', () => { + it('should call next with error when userService throws', async () => { + const testError = new Error('Settings fetch failed'); + mockGetUserSettings.mockRejectedValue(testError); + + await userController.getSettings(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + expect(mockRes.json).not.toHaveBeenCalled(); + }); + + it('should return settings on success', async () => { + const mockSettings = { userId: 1, preferredLanguage: 'ENGLISH' }; + mockGetUserSettings.mockResolvedValue(mockSettings); + + await userController.getSettings(mockReq, mockRes, mockNext); + + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + success: true, + data: expect.objectContaining({ + settings: mockSettings + }) + }) + ); + expect(mockNext).not.toHaveBeenCalled(); + }); + }); + + describe('updateSettings', () => { + it('should call next with error when userService throws', async () => { + const testError = new Error('Settings update failed'); + mockUpdateUserSettings.mockRejectedValue(testError); + mockReq.body = { preferredLanguage: 'GREEK' }; + + await userController.updateSettings(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalledWith(testError); + expect(mockRes.json).not.toHaveBeenCalled(); + }); + + it('should return updated settings on success', async () => { + const mockUpdatedSettings = { userId: 1, preferredLanguage: 'GREEK' }; + mockUpdateUserSettings.mockResolvedValue(mockUpdatedSettings); + mockReq.body = { preferredLanguage: 'GREEK' }; + + await userController.updateSettings(mockReq, mockRes, mockNext); + + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + success: true, + message: 'User settings updated successfully' + }) + ); + expect(mockNext).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/tests/unit/userService.test.js b/tests/unit/userService.test.js index 5f2882a..e3e7ae1 100644 --- a/tests/unit/userService.test.js +++ b/tests/unit/userService.test.js @@ -76,8 +76,8 @@ describe('User Service', () => { }); it('should update multiple fields at once', async () => { - const updates = { - name: 'New Name', + const updates = { + name: 'New Name', email: 'another@example.com', phone: '1234567890' }; @@ -107,7 +107,7 @@ describe('User Service', () => { it('should throw ConflictError when email is already in use', async () => { // Create another user - const anotherUser = await db.createUser({ + await db.createUser({ name: 'Another User', email: 'another@example.com', password: 'password123' @@ -120,7 +120,7 @@ describe('User Service', () => { }); it('should not update disallowed fields like password or role', async () => { - const updates = { + const updates = { name: 'Valid Update', password: 'hacked', role: 'admin' @@ -173,7 +173,7 @@ describe('User Service', () => { }); it('should update notification preferences', async () => { - const updates = { + const updates = { emailNotifications: true, pushNotifications: false }; @@ -194,6 +194,61 @@ describe('User Service', () => { .rejects .toThrow(NotFoundError); }); + + it('should update accessibility settings', async () => { + const updates = { + accessibilitySettings: ['HIGH_CONTRAST', 'LARGE_TEXT'] + }; + const updated = await userService.updateUserSettings(testUser.userId, updates); + + expect(updated.accessibilitySettings).toEqual(['HIGH_CONTRAST', 'LARGE_TEXT']); + }); + + it('should update privacy settings', async () => { + const updates = { + privacySettings: ['HIDE_EMAIL', 'HIDE_LOCATION'] + }; + const updated = await userService.updateUserSettings(testUser.userId, updates); + + expect(updated.privacySettings).toEqual(['HIDE_EMAIL', 'HIDE_LOCATION']); + }); + + it('should update user agreement accepted flag', async () => { + const updates = { + userAgreementAccepted: true + }; + const updated = await userService.updateUserSettings(testUser.userId, updates); + + expect(updated.userAgreementAccepted).toBe(true); + }); + + it('should update language setting', async () => { + const updates = { + language: 'el' + }; + const updated = await userService.updateUserSettings(testUser.userId, updates); + + expect(updated.language).toBe('el'); + }); + + it('should update multiple settings at once', async () => { + const updates = { + preferredLanguage: 'GREEK', + emailNotifications: true, + pushNotifications: false, + accessibilitySettings: ['HIGH_CONTRAST'], + privacySettings: ['HIDE_LOCATION'], + userAgreementAccepted: true + }; + const updated = await userService.updateUserSettings(testUser.userId, updates); + + expect(updated.preferredLanguage).toBe('GREEK'); + expect(updated.emailNotifications).toBe(true); + expect(updated.pushNotifications).toBe(false); + expect(updated.accessibilitySettings).toEqual(['HIGH_CONTRAST']); + expect(updated.privacySettings).toEqual(['HIDE_LOCATION']); + expect(updated.userAgreementAccepted).toBe(true); + }); }); describe('deleteUser', () => { @@ -233,7 +288,7 @@ describe('User Service', () => { expect(Array.isArray(users)).toBe(true); expect(users.length).toBeGreaterThan(0); - + // Check that passwords are sanitized users.forEach(user => { expect(user).not.toHaveProperty('password'); diff --git a/tests/unit/validationMiddleware.test.js b/tests/unit/validationMiddleware.test.js index 1d67a3a..8361514 100644 --- a/tests/unit/validationMiddleware.test.js +++ b/tests/unit/validationMiddleware.test.js @@ -28,15 +28,15 @@ describe('Validation Middleware', () => { }); it('should return 400 validation error when validation fails', () => { - const req = {}; + // Note: req object not needed in this test - validation logic doesn't use it const res = { statusCode: 200, jsonData: null, - status: function(code) { + status: function (code) { this.statusCode = code; return this; }, - json: function(data) { + json: function (data) { this.jsonData = data; return this; } @@ -112,11 +112,11 @@ describe('Validation Middleware', () => { const res = { statusCode: 200, jsonData: null, - status: function(code) { + status: function (code) { this.statusCode = code; return this; }, - json: function(data) { + json: function (data) { this.jsonData = data; return this; }