diff --git a/package-lock.json b/package-lock.json index 0995b652..78c3c77d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,12 +14,12 @@ "@dnd-kit/sortable": "10.0.0", "@dnd-kit/utilities": "3.2.2", "@lunalytics/ui": "0.1.19", - "axios": "^1.12.2", + "axios": "1.12.2", "bcryptjs": "3.0.2", "better-sqlite3": "11.10.0", "classnames": "2.5.1", "compare-versions": "6.1.1", - "compression": "1.8.0", + "compression": "^1.8.1", "cookie-parser": "1.4.7", "cors": "2.8.5", "cron": "4.3.1", @@ -62,7 +62,8 @@ "@types/react-dom": "19.1.7", "@types/react-window": "1.8.8", "@vitejs/plugin-react-swc": "4.0.0", - "@vitest/coverage-v8": "3.2.4", + "@vitest/coverage-v8": "^4.0.4", + "@vitest/ui": "^4.0.4", "concurrently": "9.2.0", "cypress": "14.4.1", "eslint": "9.33.0", @@ -79,28 +80,14 @@ "sass": "1.89.2", "typescript": "5.9.2", "typescript-eslint": "8.39.1", - "vite": "7.1.11", + "vite": "^7.1.12", "vite-plugin-compression2": "2.2.0", - "vitest": "3.2.4" + "vitest": "^4.0.4" }, "engines": { "node": ">= 22.x" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -112,9 +99,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -122,13 +109,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", - "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -147,14 +134,14 @@ } }, "node_modules/@babel/types": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", - "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1382,134 +1369,6 @@ } } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1520,27 +1379,17 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -2036,16 +1885,12 @@ "node": ">=0.10" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } + "license": "MIT" }, "node_modules/@preact/signals-core": { "version": "1.11.0", @@ -2652,13 +2497,14 @@ } }, "node_modules/@types/chai": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", - "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "@types/deep-eql": "*" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, "node_modules/@types/d3-array": { @@ -3113,32 +2959,30 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", - "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.4.tgz", + "integrity": "sha512-YM7gDj2TX2AXyGLz0p/B7hvTsTfaQc+kSV/LU0nEnKlep/ZfbdCDppPND4YQiQC43OXyrhkG3y8ZSTqYb2CKqQ==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", - "ast-v8-to-istanbul": "^0.3.3", - "debug": "^4.4.1", + "@vitest/utils": "4.0.4", + "ast-v8-to-istanbul": "^0.3.5", + "debug": "^4.4.3", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.17", + "istanbul-reports": "^3.2.0", "magicast": "^0.3.5", "std-env": "^3.9.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^2.0.0" + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.2.4", - "vitest": "3.2.4" + "@vitest/browser": "4.0.4", + "vitest": "4.0.4" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -3147,39 +2991,40 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.4.tgz", + "integrity": "sha512-0ioMscWJtfpyH7+P82sGpAi3Si30OVV73jD+tEqXm5+rIx9LgnfdaOn45uaFkKOncABi/PHL00Yn0oW/wK4cXw==", "dev": true, "license": "MIT", "dependencies": { + "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@vitest/spy": "4.0.4", + "@vitest/utils": "4.0.4", + "chai": "^6.0.1", + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.4.tgz", + "integrity": "sha512-UTtKgpjWj+pvn3lUM55nSg34098obGhSHH+KlJcXesky8b5wCUgg7s60epxrS6yAG8slZ9W8T9jGWg4PisMf5Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.4", + "@vitest/spy": "4.0.4", "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "magic-string": "^0.30.19" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -3191,42 +3036,41 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.4.tgz", + "integrity": "sha512-lHI2rbyrLVSd1TiHGJYyEtbOBo2SDndIsN3qY4o4xe2pBxoJLD6IICghNCvD7P+BFin6jeyHXiUICXqgl6vEaQ==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.4.tgz", + "integrity": "sha512-99EDqiCkncCmvIZj3qJXBZbyoQ35ghOwVWNnQ5nj0Hnsv4Qm40HmrMJrceewjLVvsxV/JSU4qyx2CGcfMBmXJw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "@vitest/utils": "4.0.4", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.4.tgz", + "integrity": "sha512-XICqf5Gi4648FGoBIeRgnHWSNDp+7R5tpclGosFaUUFzY6SfcpsfHNMnC7oDu/iOLBxYfxVzaQpylEvpgii3zw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", + "@vitest/pretty-format": "4.0.4", + "magic-string": "^0.30.19", "pathe": "^2.0.3" }, "funding": { @@ -3234,28 +3078,46 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.4.tgz", + "integrity": "sha512-G9L13AFyYECo40QG7E07EdYnZZYCKMTSp83p9W8Vwed0IyCG1GnpDLxObkx8uOGPXfDpdeVf24P1Yka8/q1s9g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.4.tgz", + "integrity": "sha512-CmuFQLKw5SaLU/Flo8dLiQw2P2ONguhjfhBL9AYkTeDZPToE8laGvObXqRzS5G+4RD4SgWcI1USAmGxMVIqT0g==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^4.0.3" + "@vitest/utils": "4.0.4", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.4" } }, "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.4.tgz", + "integrity": "sha512-4bJLmSvZLyVbNsYFRpPYdJViG9jZyRvMZ35IF4ymXbRZoS+ycYghmwTGiscTXduUg2lgKK7POWIyXJNute1hjw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "@vitest/pretty-format": "4.0.4", + "tinyrainbow": "^3.0.3" }, "funding": { "url": "https://opencollective.com/vitest" @@ -3592,13 +3454,13 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz", - "integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz", + "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", + "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^9.0.1" } @@ -3918,16 +3780,6 @@ "node": ">= 0.8" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/cachedir": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", @@ -4004,20 +3856,13 @@ "license": "Apache-2.0" }, "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.0.tgz", + "integrity": "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/chalk": { @@ -4056,16 +3901,6 @@ "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", "license": "MIT" }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/check-more-types": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", @@ -4380,16 +4215,16 @@ } }, "node_modules/compression": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", - "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "license": "MIT", "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", - "on-headers": "~1.0.2", + "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" }, @@ -4870,9 +4705,9 @@ "license": "MIT" }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -4908,16 +4743,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -5064,13 +4889,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -5719,9 +5537,9 @@ } }, "node_modules/expect-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", - "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5928,6 +5746,13 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -6108,36 +5933,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -6400,27 +6195,6 @@ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6434,32 +6208,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/global-dirs": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", @@ -7530,9 +7278,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -7561,22 +7309,6 @@ "node": ">= 0.4" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -8446,20 +8178,6 @@ "integrity": "sha512-HusN80C0ohtT9kOHQH7EuUaqzRQsnekpa+2ot8OzvW0iC08dq/YtM/7uKwwajldQsCrHyC8q9fz3t3L+TmDltA==", "license": "MIT" }, - "node_modules/loupe": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", - "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/luxon": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", @@ -8470,13 +8188,13 @@ } }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/magicast": { @@ -8669,16 +8387,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -8729,6 +8437,16 @@ "node": "*" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9117,9 +8835,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -9295,13 +9013,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -9358,23 +9069,6 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -9388,16 +9082,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -10854,6 +10538,21 @@ "node": ">=10" } }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/slice-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", @@ -10995,22 +10694,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -11121,20 +10804,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -11158,19 +10827,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", - "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -11249,47 +10905,6 @@ "node": ">=8.0.0" } }, - "node_modules/test-exclude": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", - "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^9.0.4" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -11390,30 +11005,10 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", - "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", "dev": true, "license": "MIT", "engines": { @@ -11472,6 +11067,16 @@ "node": ">=0.6" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/touch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", @@ -11962,9 +11567,9 @@ } }, "node_modules/vite": { - "version": "7.1.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", - "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", + "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, "license": "MIT", "dependencies": { @@ -12036,29 +11641,6 @@ } } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vite-plugin-compression2": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/vite-plugin-compression2/-/vite-plugin-compression2-2.2.0.tgz", @@ -12102,41 +11684,38 @@ } }, "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.4.tgz", + "integrity": "sha512-hV31h0/bGbtmDQc0KqaxsTO1v4ZQeF8ojDFuy4sZhFadwAqqvJA0LDw68QUocctI5EDpFMql/jVWKuPYHIf2Ew==", "dev": true, "license": "MIT", "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", + "@vitest/expect": "4.0.4", + "@vitest/mocker": "4.0.4", + "@vitest/pretty-format": "4.0.4", + "@vitest/runner": "4.0.4", + "@vitest/snapshot": "4.0.4", + "@vitest/spy": "4.0.4", + "@vitest/utils": "4.0.4", + "debug": "^4.4.3", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.19", "pathe": "^2.0.3", - "picomatch": "^4.0.2", + "picomatch": "^4.0.3", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -12144,9 +11723,11 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.4", + "@vitest/browser-preview": "4.0.4", + "@vitest/browser-webdriverio": "4.0.4", + "@vitest/ui": "4.0.4", "happy-dom": "*", "jsdom": "*" }, @@ -12160,7 +11741,13 @@ "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { "optional": true }, "@vitest/ui": { @@ -12175,9 +11762,9 @@ } }, "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -12400,25 +11987,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 0b623d1d..b9892841 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "test:e2e": "npm run build && cypress open", "test:db": "node ./test/shared/setupDatabase.js", "test:server": "npm run test:db && cross-env NODE_ENV=test node server/index.js", + "test:ui": "vitest run --coverage --watch --ui", "migrate": "node ./scripts/migrate_manual.js", "dev:prepare": "husky", "preview": "vite preview", @@ -42,12 +43,12 @@ "@dnd-kit/sortable": "10.0.0", "@dnd-kit/utilities": "3.2.2", "@lunalytics/ui": "0.1.19", - "axios": "^1.12.2", + "axios": "1.12.2", "bcryptjs": "3.0.2", "better-sqlite3": "11.10.0", "classnames": "2.5.1", "compare-versions": "6.1.1", - "compression": "1.8.0", + "compression": "1.8.1", "cookie-parser": "1.4.7", "cors": "2.8.5", "cron": "4.3.1", @@ -90,7 +91,8 @@ "@types/react-dom": "19.1.7", "@types/react-window": "1.8.8", "@vitejs/plugin-react-swc": "4.0.0", - "@vitest/coverage-v8": "3.2.4", + "@vitest/coverage-v8": "4.0.4", + "@vitest/ui": "4.0.4", "concurrently": "9.2.0", "cypress": "14.4.1", "eslint": "9.33.0", @@ -107,8 +109,8 @@ "sass": "1.89.2", "typescript": "5.9.2", "typescript-eslint": "8.39.1", - "vite": "7.1.11", + "vite": "7.1.12", "vite-plugin-compression2": "2.2.0", - "vitest": "3.2.4" + "vitest": "4.0.4" } } diff --git a/server/middleware/notifications/delete.js b/server/middleware/notifications/delete.js index 26523e14..b5225cd1 100644 --- a/server/middleware/notifications/delete.js +++ b/server/middleware/notifications/delete.js @@ -3,13 +3,13 @@ import { UnprocessableError } from '../../../shared/utils/errors.js'; import { deleteNotification } from '../../database/queries/notification.js'; const NotificationDeleteMiddleware = async (request, response) => { - const { notificationId } = request.query; + try { + const { notificationId } = request.query; - if (!notificationId) { - throw new UnprocessableError('No notificationId provided'); - } + if (!notificationId) { + throw new UnprocessableError('No notificationId provided'); + } - try { await deleteNotification(notificationId); return response.status(200).send('Notification deleted'); } catch (error) { diff --git a/server/middleware/notifications/disable.js b/server/middleware/notifications/disable.js index 90611b58..0b93ffc8 100644 --- a/server/middleware/notifications/disable.js +++ b/server/middleware/notifications/disable.js @@ -5,15 +5,15 @@ import { toggleNotification } from '../../database/queries/notification.js'; const NotificationToggleMiddleware = async (request, response) => { const { notificationId, isEnabled } = request.query; - if (!notificationId) { - throw new UnprocessableError('No notificationId provided'); - } + try { + if (!notificationId) { + throw new UnprocessableError('No notificationId provided'); + } - if (isEnabled !== 'true' && isEnabled !== 'false') { - throw new UnprocessableError('isEnabled is not a boolean'); - } + if (isEnabled !== 'true' && isEnabled !== 'false') { + throw new UnprocessableError('isEnabled is not a boolean'); + } - try { await toggleNotification(notificationId, isEnabled === 'true'); return response.sendStatus(200); } catch (error) { diff --git a/server/middleware/status/defaultPage.js b/server/middleware/status/defaultPage.js index 3ec0f935..ee965d0d 100644 --- a/server/middleware/status/defaultPage.js +++ b/server/middleware/status/defaultPage.js @@ -31,7 +31,7 @@ const defaultPageMiddleware = async (request, response, next) => { } } - next(); + return next(); } catch { return response.redirect('/home'); } diff --git a/shared/utils/authenication.js b/shared/utils/authenication.js index c4a45f36..b790d34c 100644 --- a/shared/utils/authenication.js +++ b/shared/utils/authenication.js @@ -145,6 +145,7 @@ export const getAuthCallbackUrl = ( } if (provider === 'custom') { + return []; } return null; diff --git a/test/server/classes/certificate.test.js b/test/server/class/certificate.test.js similarity index 100% rename from test/server/classes/certificate.test.js rename to test/server/class/certificate.test.js diff --git a/test/server/class/incident.test.js b/test/server/class/incident.test.js new file mode 100644 index 00000000..1dc0b48b --- /dev/null +++ b/test/server/class/incident.test.js @@ -0,0 +1,83 @@ +import { cleanIncident } from '../../../server/class/incident.js'; + +describe('cleanIncident', () => { + it('should clean and parse monitorIds and messages if they are JSON strings', () => { + const incident = { + incidentId: 'abc123', + title: 'Test Incident', + monitorIds: '["monitor1","monitor2"]', + messages: '[{"msg":"down"},{"msg":"up"}]', + affect: 'partial', + status: 'investigating', + createdAt: '2025-10-25T10:00:00Z', + completedAt: null, + isClosed: '1', + }; + const result = cleanIncident(incident); + expect(result.incidentId).toBe('abc123'); + expect(result.title).toBe('Test Incident'); + expect(result.monitorIds).toEqual(['monitor1', 'monitor2']); + expect(result.messages).toEqual([{ msg: 'down' }, { msg: 'up' }]); + expect(result.affect).toBe('partial'); + expect(result.status).toBe('investigating'); + expect(result.createdAt).toBe('2025-10-25T10:00:00Z'); + expect(result.completedAt).toBeNull(); + expect(result.isClosed).toBe(true); + }); + + it('should not parse monitorIds/messages if already objects', () => { + const incident = { + incidentId: 'id2', + title: 'Already Parsed', + monitorIds: ['m1', 'm2'], + messages: [{ msg: 'ok' }], + affect: 'none', + status: 'resolved', + createdAt: '2025-10-25T11:00:00Z', + completedAt: '2025-10-25T12:00:00Z', + isClosed: '0', + }; + const result = cleanIncident(incident); + expect(result.monitorIds).toEqual(['m1', 'm2']); + expect(result.messages).toEqual([{ msg: 'ok' }]); + expect(result.isClosed).toBe(false); + }); + + it('should handle malformed JSON gracefully', () => { + const incident = { + incidentId: 'id3', + title: 'Malformed', + monitorIds: '[malformed', + messages: '{bad:json}', + affect: 'major', + status: 'error', + createdAt: '2025-10-25T13:00:00Z', + completedAt: null, + isClosed: '1', + }; + const result = cleanIncident(incident); + expect(result.monitorIds).toBe('[malformed'); + expect(result.messages).toBe('{bad:json}'); + expect(result.isClosed).toBe(true); + }); + + it('should handle missing optional fields', () => { + const incident = { + incidentId: 'id4', + title: 'Missing Fields', + monitorIds: '[]', + messages: '[]', + affect: undefined, + status: undefined, + createdAt: undefined, + completedAt: undefined, + isClosed: undefined, + }; + const result = cleanIncident(incident); + expect(result.affect).toBeUndefined(); + expect(result.status).toBeUndefined(); + expect(result.createdAt).toBeUndefined(); + expect(result.completedAt).toBeUndefined(); + expect(result.isClosed).toBe(false); + }); +}); diff --git a/test/server/classes/monitor.test.js b/test/server/class/monitor.test.js similarity index 100% rename from test/server/classes/monitor.test.js rename to test/server/class/monitor.test.js diff --git a/test/server/class/monitor/docker.test.js b/test/server/class/monitor/docker.test.js new file mode 100644 index 00000000..236d00e2 --- /dev/null +++ b/test/server/class/monitor/docker.test.js @@ -0,0 +1,75 @@ +import clean from '../../../../server/class/monitor/docker.js'; + +describe('docker clean', () => { + it('should parse and convert fields correctly', () => { + const monitor = { + monitorId: 'm1', + name: 'Docker Monitor', + url: 'http://localhost', + retry: '3', + interval: '60', + retryInterval: '10', + requestTimeout: '5000', + email: 'test@example.com', + type: 'docker', + notificationId: 'nid', + notificationType: 'email', + uptimePercentage: 99.9, + averageHeartbeatLatency: 100, + showFilters: true, + paused: '1', + ignoreTls: '1', + createdAt: '2025-10-25T10:00:00Z', + icon: '["icon1"]', + heartbeats: [{ status: 'ok' }], + }; + const result = clean(monitor); + expect(result.monitorId).toBe('m1'); + expect(result.retry).toBe(3); + expect(result.interval).toBe(60); + expect(result.retryInterval).toBe(10); + expect(result.requestTimeout).toBe(5000); + expect(result.paused).toBe(true); + expect(result.ignoreTls).toBe(true); + expect(result.icon).toEqual(['icon1']); + expect(result.heartbeats).toEqual([{ status: 'ok' }]); + }); + + it('should handle missing heartbeats and parse booleans', () => { + const monitor = { + monitorId: 'm2', + name: 'No Heartbeats', + url: 'http://localhost', + retry: '1', + interval: '30', + retryInterval: '5', + requestTimeout: '1000', + paused: '0', + ignoreTls: '0', + icon: '[]', + }; + const result = clean(monitor); + expect(result.paused).toBe(false); + expect(result.ignoreTls).toBe(false); + expect(result.heartbeats).toEqual([]); + expect(result.icon).toEqual([]); + }); + + it('should omit heartbeats if includeHeartbeats is false', () => { + const monitor = { + monitorId: 'm3', + name: 'No Heartbeats', + url: 'http://localhost', + retry: '1', + interval: '30', + retryInterval: '5', + requestTimeout: '1000', + paused: '0', + ignoreTls: '0', + icon: '[]', + heartbeats: [{ status: 'ok' }], + }; + const result = clean(monitor, false); + expect(result.heartbeats).toBeUndefined(); + }); +}); diff --git a/test/server/class/monitor/http.test.js b/test/server/class/monitor/http.test.js new file mode 100644 index 00000000..9c220b0e --- /dev/null +++ b/test/server/class/monitor/http.test.js @@ -0,0 +1,87 @@ +import clean from '../../../../server/class/monitor/http.js'; + +describe('http clean', () => { + it('should parse and convert fields correctly', () => { + const monitor = { + monitorId: 'h1', + name: 'HTTP Monitor', + url: 'http://localhost', + retry: '2', + interval: '30', + retryInterval: '5', + requestTimeout: '1000', + method: 'GET', + headers: '{"Accept":"*/*"}', + body: '{"data":1}', + valid_status_codes: '["200-299"]', + email: 'test@example.com', + type: 'http', + notificationId: 'nid', + notificationType: 'email', + uptimePercentage: 100, + averageHeartbeatLatency: 50, + showFilters: false, + paused: '1', + ignoreTls: '1', + createdAt: '2025-10-25T10:00:00Z', + icon: '["icon1"]', + heartbeats: [{ status: 'ok' }], + cert: { valid: true }, + }; + const result = clean(monitor); + expect(result.monitorId).toBe('h1'); + expect(result.retry).toBe(2); + expect(result.interval).toBe(30); + expect(result.retryInterval).toBe(5); + expect(result.requestTimeout).toBe(1000); + expect(result.method).toBe('GET'); + expect(result.headers).toEqual({ Accept: '*/*' }); + expect(result.body).toEqual({ data: 1 }); + expect(result.valid_status_codes).toEqual(['200-299']); + expect(result.paused).toBe(true); + expect(result.ignoreTls).toBe(true); + expect(result.icon).toEqual(['icon1']); + expect(result.heartbeats).toEqual([{ status: 'ok' }]); + }); + + it('should handle missing heartbeats and cert, and parse booleans', () => { + const monitor = { + monitorId: 'h2', + name: 'No Heartbeats', + url: 'http://localhost', + retry: '1', + interval: '10', + retryInterval: '2', + requestTimeout: '500', + paused: '0', + ignoreTls: '0', + icon: '[]', + }; + const result = clean(monitor); + expect(result.paused).toBe(false); + expect(result.ignoreTls).toBe(false); + expect(result.heartbeats).toEqual([]); + expect(result.icon).toEqual([]); + expect(result.cert).toEqual({ isValid: false }); + }); + + it('should omit heartbeats/cert if includeHeartbeats/includeCert is false', () => { + const monitor = { + monitorId: 'h3', + name: 'No Heartbeats', + url: 'http://localhost', + retry: '1', + interval: '10', + retryInterval: '2', + requestTimeout: '500', + paused: '0', + ignoreTls: '0', + icon: '[]', + heartbeats: [{ status: 'ok' }], + cert: { valid: true }, + }; + const result = clean(monitor, false, false); + expect(result.heartbeats).toBeUndefined(); + expect(result.cert).toBeUndefined(); + }); +}); diff --git a/test/server/class/monitor/json.test.js b/test/server/class/monitor/json.test.js new file mode 100644 index 00000000..ad0f705a --- /dev/null +++ b/test/server/class/monitor/json.test.js @@ -0,0 +1,90 @@ +import clean from '../../../../server/class/monitor/json.js'; + +describe('json clean', () => { + it('should parse and convert fields correctly', () => { + const monitor = { + monitorId: 'j1', + name: 'JSON Monitor', + url: 'http://localhost', + retry: '2', + interval: '30', + retryInterval: '5', + requestTimeout: '1000', + method: 'POST', + headers: '{"Content-Type":"application/json"}', + body: '{"data":2}', + email: 'test@example.com', + type: 'json', + notificationId: 'nid', + notificationType: 'email', + uptimePercentage: 100, + averageHeartbeatLatency: 50, + showFilters: false, + paused: '1', + ignoreTls: '1', + json_query: '["$.status"]', + createdAt: '2025-10-25T10:00:00Z', + icon: '["icon1"]', + heartbeats: [{ status: 'ok' }], + cert: { valid: true }, + }; + const result = clean(monitor); + expect(result.monitorId).toBe('j1'); + expect(result.retry).toBe(2); + expect(result.interval).toBe(30); + expect(result.retryInterval).toBe(5); + expect(result.requestTimeout).toBe(1000); + expect(result.method).toBe('POST'); + expect(result.headers).toEqual({ 'Content-Type': 'application/json' }); + expect(result.body).toEqual({ data: 2 }); + expect(result.json_query).toEqual(['$.status']); + expect(result.paused).toBe(true); + expect(result.ignoreTls).toBe(true); + expect(result.icon).toEqual(['icon1']); + expect(result.heartbeats).toEqual([{ status: 'ok' }]); + }); + + it('should handle missing heartbeats/cert and parse booleans', () => { + const monitor = { + monitorId: 'j2', + name: 'No Heartbeats', + url: 'http://localhost', + retry: '1', + interval: '10', + retryInterval: '2', + requestTimeout: '500', + paused: '0', + ignoreTls: '0', + json_query: '[]', + icon: '[]', + }; + const result = clean(monitor); + expect(result.paused).toBe(false); + expect(result.ignoreTls).toBe(false); + expect(result.heartbeats).toEqual([]); + expect(result.icon).toEqual([]); + expect(result.cert).toEqual({ isValid: false }); + expect(result.json_query).toEqual([]); + }); + + it('should omit heartbeats/cert if includeHeartbeats/includeCert is false', () => { + const monitor = { + monitorId: 'j3', + name: 'No Heartbeats', + url: 'http://localhost', + retry: '1', + interval: '10', + retryInterval: '2', + requestTimeout: '500', + paused: '0', + ignoreTls: '0', + json_query: '[]', + icon: '[]', + heartbeats: [{ status: 'ok' }], + cert: { valid: true }, + }; + const result = clean(monitor, false, false); + expect(result.heartbeats).toBeUndefined(); + expect(result.cert).toBeUndefined(); + }); +}); diff --git a/test/server/class/monitor/ping.test.js b/test/server/class/monitor/ping.test.js new file mode 100644 index 00000000..c5c46b4b --- /dev/null +++ b/test/server/class/monitor/ping.test.js @@ -0,0 +1,76 @@ +import clean from '../../../../server/class/monitor/ping.js'; + +describe('ping clean', () => { + it('should parse and convert fields correctly', () => { + const monitor = { + monitorId: 'p1', + name: 'Ping Monitor', + url: 'http://localhost', + retry: '2', + interval: '30', + retryInterval: '5', + requestTimeout: '1000', + method: 'GET', + headers: '{"Accept":"*/*"}', + body: '{"data":1}', + email: 'test@example.com', + type: 'ping', + notificationId: 'nid', + notificationType: 'email', + uptimePercentage: 100, + averageHeartbeatLatency: 50, + showFilters: false, + paused: '1', + createdAt: '2025-10-25T10:00:00Z', + icon: '["icon1"]', + heartbeats: [{ status: 'ok' }], + }; + const result = clean(monitor); + expect(result.monitorId).toBe('p1'); + expect(result.retry).toBe(2); + expect(result.interval).toBe(30); + expect(result.retryInterval).toBe(5); + expect(result.requestTimeout).toBe(1000); + expect(result.method).toBe('GET'); + expect(result.headers).toEqual({ Accept: '*/*' }); + expect(result.body).toEqual({ data: 1 }); + expect(result.paused).toBe(true); + expect(result.icon).toEqual(['icon1']); + expect(result.heartbeats).toEqual([{ status: 'ok' }]); + }); + + it('should handle missing heartbeats and parse booleans', () => { + const monitor = { + monitorId: 'p2', + name: 'No Heartbeats', + url: 'http://localhost', + retry: '1', + interval: '10', + retryInterval: '2', + requestTimeout: '500', + paused: '0', + icon: '[]', + }; + const result = clean(monitor); + expect(result.paused).toBe(false); + expect(result.heartbeats).toEqual([]); + expect(result.icon).toEqual([]); + }); + + it('should omit heartbeats if includeHeartbeats is false', () => { + const monitor = { + monitorId: 'p3', + name: 'No Heartbeats', + url: 'http://localhost', + retry: '1', + interval: '10', + retryInterval: '2', + requestTimeout: '500', + paused: '0', + icon: '[]', + heartbeats: [{ status: 'ok' }], + }; + const result = clean(monitor, false); + expect(result.heartbeats).toBeUndefined(); + }); +}); diff --git a/test/server/class/monitor/statusPage.test.js b/test/server/class/monitor/statusPage.test.js new file mode 100644 index 00000000..8890458d --- /dev/null +++ b/test/server/class/monitor/statusPage.test.js @@ -0,0 +1,35 @@ +import { cleanMonitorForStatusPage } from '../../../../server/class/monitor/statusPage.js'; + +describe('cleanMonitorForStatusPage', () => { + it('should parse and convert fields correctly', () => { + const monitor = { + monitorId: 's1', + name: 'StatusPage Monitor', + url: 'http://localhost', + createdAt: '2025-10-25T10:00:00Z', + paused: '1', + icon: '["icon1"]', + }; + const result = cleanMonitorForStatusPage(monitor); + expect(result.monitorId).toBe('s1'); + expect(result.name).toBe('StatusPage Monitor'); + expect(result.url).toBe('http://localhost'); + expect(result.createdAt).toBe('2025-10-25T10:00:00Z'); + expect(result.paused).toBe(true); + expect(result.icon).toEqual(['icon1']); + }); + + it('should handle paused false and empty icon', () => { + const monitor = { + monitorId: 's2', + name: 'Paused False', + url: 'http://localhost', + createdAt: '2025-10-25T11:00:00Z', + paused: '0', + icon: '[]', + }; + const result = cleanMonitorForStatusPage(monitor); + expect(result.paused).toBe(false); + expect(result.icon).toEqual([]); + }); +}); diff --git a/test/server/class/monitor/tcp.test.js b/test/server/class/monitor/tcp.test.js new file mode 100644 index 00000000..bf1fd25e --- /dev/null +++ b/test/server/class/monitor/tcp.test.js @@ -0,0 +1,72 @@ +import clean from '../../../../server/class/monitor/tcp.js'; + +describe('tcp clean', () => { + it('should parse and convert fields correctly', () => { + const monitor = { + monitorId: 't1', + name: 'TCP Monitor', + url: 'tcp://localhost', + retry: '2', + interval: '30', + retryInterval: '5', + requestTimeout: '1000', + email: 'test@example.com', + type: 'tcp', + port: 8080, + notificationId: 'nid', + notificationType: 'email', + uptimePercentage: 100, + averageHeartbeatLatency: 50, + showFilters: false, + paused: '1', + createdAt: '2025-10-25T10:00:00Z', + icon: '["icon1"]', + heartbeats: [{ status: 'ok' }], + }; + const result = clean(monitor); + expect(result.monitorId).toBe('t1'); + expect(result.retry).toBe(2); + expect(result.interval).toBe(30); + expect(result.retryInterval).toBe(5); + expect(result.requestTimeout).toBe(1000); + expect(result.paused).toBe(true); + expect(result.icon).toEqual(['icon1']); + expect(result.heartbeats).toEqual([{ status: 'ok' }]); + expect(result.port).toBe(8080); + }); + + it('should handle missing heartbeats and parse booleans', () => { + const monitor = { + monitorId: 't2', + name: 'No Heartbeats', + url: 'tcp://localhost', + retry: '1', + interval: '10', + retryInterval: '2', + requestTimeout: '500', + paused: '0', + icon: '[]', + }; + const result = clean(monitor); + expect(result.paused).toBe(false); + expect(result.heartbeats).toEqual([]); + expect(result.icon).toEqual([]); + }); + + it('should omit heartbeats if includeHeartbeats is false', () => { + const monitor = { + monitorId: 't3', + name: 'No Heartbeats', + url: 'tcp://localhost', + retry: '1', + interval: '10', + retryInterval: '2', + requestTimeout: '500', + paused: '0', + icon: '[]', + heartbeats: [{ status: 'ok' }], + }; + const result = clean(monitor, false); + expect(result.heartbeats).toBeUndefined(); + }); +}); diff --git a/test/server/class/status.test.js b/test/server/class/status.test.js new file mode 100644 index 00000000..80291924 --- /dev/null +++ b/test/server/class/status.test.js @@ -0,0 +1,95 @@ +import { + cleanStatusPage, + cleanStatusPageWithMonitors, + cleanStatusApiResponse, +} from '../../../server/class/status.js'; + +describe('cleanStatusPage', () => { + it('should merge defaultStatusValues with parsed settings and parse layout', () => { + const status = { + id: '1', + statusId: 'sid', + statusUrl: 'url', + settings: '{"theme":"dark"}', + layout: '["row1","row2"]', + email: 'test@example.com', + createdAt: '2025-10-25T10:00:00Z', + }; + const result = cleanStatusPage(status); + expect(result.id).toBe('1'); + expect(result.statusId).toBe('sid'); + expect(result.statusUrl).toBe('url'); + expect(result.settings.theme).toBe('dark'); + expect(Array.isArray(result.layout)).toBe(true); + expect(result.layout).toEqual(['row1', 'row2']); + expect(result.email).toBe('test@example.com'); + expect(result.createdAt).toBe('2025-10-25T10:00:00Z'); + }); + + it('should handle malformed settings/layout JSON gracefully', () => { + const status = { + id: '2', + statusId: 'sid2', + statusUrl: 'url2', + settings: '{bad:json}', + layout: '[malformed', + email: 'bad@example.com', + createdAt: '2025-10-25T11:00:00Z', + }; + const result = cleanStatusPage(status); + expect(typeof result.settings).toBe('object'); + expect(result.layout).toEqual([]); + }); + + it('should handle missing optional fields', () => { + const status = { + id: '3', + statusId: 'sid3', + statusUrl: 'url3', + settings: '{}', + layout: '[]', + }; + const result = cleanStatusPage(status); + expect(result.email).toBeUndefined(); + expect(result.createdAt).toBeUndefined(); + }); +}); + +describe('cleanStatusPageWithMonitors', () => { + it('should merge defaultStatusValues with parsed settings and parse layout', () => { + const status = { + settings: '{"lang":"en"}', + layout: '["rowA"]', + }; + const result = cleanStatusPageWithMonitors(status); + expect(result.settings.lang).toBe('en'); + expect(result.layout).toEqual(['rowA']); + }); + + it('should handle malformed JSON gracefully', () => { + const status = { + settings: '{bad:json}', + layout: '[malformed', + }; + const result = cleanStatusPageWithMonitors(status); + expect(typeof result.settings).toBe('object'); + expect(result.layout).toEqual([]); + }); +}); + +describe('cleanStatusApiResponse', () => { + it('should return all fields as-is', () => { + const data = { + id: 'id', + statusId: 'sid', + statusUrl: 'url', + settings: { theme: 'light' }, + layout: ['row'], + monitors: [1, 2], + incidents: [3], + heartbeats: [4], + }; + const result = cleanStatusApiResponse(data); + expect(result).toEqual(data); + }); +}); diff --git a/test/server/middleware/auth/callback/custom.test.js b/test/server/middleware/auth/callback/custom.test.js new file mode 100644 index 00000000..b1416404 --- /dev/null +++ b/test/server/middleware/auth/callback/custom.test.js @@ -0,0 +1,60 @@ +import axios from 'axios'; +import config from '../../../../../server/utils/config.js'; +import { fetchProvider } from '../../../../../server/database/queries/provider.js'; +import customCallback from '../../../../../server/middleware/auth/callback/custom.js'; +import { createRequest, createResponse } from 'node-mocks-http'; + +vi.mock('axios'); +vi.mock('../../../../../server/database/queries/provider.js'); +vi.mock('../../../../../server/utils/config.js'); + +describe('customCallback', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.query = { code: 'abc' }; + fakeResponse.redirect = vi.fn(); + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.send = vi.fn(); + fakeResponse.locals = {}; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if no code provided', async () => { + fakeRequest.query.code = undefined; + + await customCallback(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.send).toHaveBeenCalledWith('No code provided'); + }); + + it('should redirect if provider not found', async () => { + fetchProvider.mockResolvedValue(null); + + await customCallback(fakeRequest, fakeResponse); + + expect(fakeResponse.redirect).toHaveBeenCalledWith('/auth/error'); + }); + + it('should send userInfo if provider and code are valid', async () => { + fetchProvider.mockResolvedValue({ + provider: 'custom', + clientId: 'id', + clientSecret: 'secret', + }); + + config.get.mockReturnValue('https://site.com'); + axios.post.mockResolvedValue({ data: { access_token: 'token' } }); + axios.get.mockResolvedValue({ data: { id: 'i', email: 'e' } }); + + await customCallback(fakeRequest, fakeResponse); + + expect(fakeResponse.send).toHaveBeenCalledWith({ id: 'i', email: 'e' }); + }); +}); diff --git a/test/server/middleware/auth/callback/discord.test.js b/test/server/middleware/auth/callback/discord.test.js new file mode 100644 index 00000000..6eb70323 --- /dev/null +++ b/test/server/middleware/auth/callback/discord.test.js @@ -0,0 +1,104 @@ +import axios from 'axios'; +import config from '../../../../../server/utils/config.js'; +import { handleError } from '../../../../../server/utils/errors.js'; +import { fetchProvider } from '../../../../../server/database/queries/provider.js'; +import discordCallback from '../../../../../server/middleware/auth/callback/discord.js'; +import { createRequest, createResponse } from 'node-mocks-http'; + +vi.mock('axios'); +vi.mock('../../../../../server/database/queries/provider.js'); +vi.mock('../../../../../server/utils/config.js'); +vi.mock('../../../../../server/utils/errors.js'); + +describe('discordCallback', () => { + let fakeRequest, fakeResponse, fakeNext; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.query = { code: 'abc' }; + fakeResponse.redirect = vi.fn(); + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.send = vi.fn(); + fakeResponse.locals = {}; + + fakeNext = vi.fn(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should redirect if no code provided', async () => { + fakeRequest.query.code = undefined; + + await discordCallback(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith( + '/error?code=missing_code&provider=discord' + ); + }); + + it('should redirect if provider not found', async () => { + fetchProvider.mockResolvedValue(null); + + await discordCallback(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith( + '/error?code=provider_not_found&provider=discord' + ); + }); + + it('should redirect if user not verified', async () => { + fetchProvider.mockResolvedValue({ + provider: 'discord', + clientId: 'id', + clientSecret: 'secret', + }); + config.get.mockReturnValue('https://site.com'); + axios.post.mockResolvedValue({ data: { access_token: 'token' } }); + axios.get.mockResolvedValue({ + data: { verified: false, email: null }, + }); + + await discordCallback(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith( + '/error?code=unverified_user&provider=discord' + ); + }); + + it('should set authUser and call next if user verified', async () => { + fetchProvider.mockResolvedValue({ + provider: 'discord', + clientId: 'id', + clientSecret: 'secret', + }); + config.get.mockReturnValue('https://site.com'); + axios.post.mockResolvedValue({ data: { access_token: 'token' } }); + axios.get.mockResolvedValue({ + data: { avatar: 'a', id: 'i', username: 'u', email: 'e', verified: true }, + }); + + await discordCallback(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.locals.authUser).toEqual({ + id: 'i', + email: 'e', + avatar: 'a', + username: 'u', + provider: 'discord', + }); + expect(fakeNext).toHaveBeenCalled(); + }); + + it('should handle errors gracefully', async () => { + fetchProvider.mockImplementation(() => { + throw new Error('fail'); + }); + + await discordCallback(fakeRequest, fakeResponse, fakeNext); + + expect(handleError).toHaveBeenCalled(); + }); +}); diff --git a/test/server/middleware/auth/callback/github.test.js b/test/server/middleware/auth/callback/github.test.js new file mode 100644 index 00000000..8a5c1d8f --- /dev/null +++ b/test/server/middleware/auth/callback/github.test.js @@ -0,0 +1,128 @@ +import axios from 'axios'; +import { createRequest, createResponse } from 'node-mocks-http'; +import config from '../../../../../server/utils/config.js'; +import { handleError } from '../../../../../server/utils/errors.js'; +import { fetchProvider } from '../../../../../server/database/queries/provider.js'; +import githubCallback from '../../../../../server/middleware/auth/callback/github.js'; + +vi.mock('axios'); +vi.mock('../../../../../server/database/queries/provider.js'); +vi.mock('../../../../../server/utils/config.js'); +vi.mock('../../../../../server/utils/errors.js'); + +describe('githubCallback', () => { + let fakeRequest, fakeResponse, fakeNext; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.query = { code: 'abc' }; + fakeResponse.redirect = vi.fn(); + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.send = vi.fn(); + fakeResponse.locals = {}; + + fakeNext = vi.fn(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if no code provided', async () => { + fakeRequest.query.code = undefined; + + await githubCallback(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.send).toHaveBeenCalledWith('No code provided'); + }); + + it('should redirect if provider not found', async () => { + fetchProvider.mockResolvedValue(null); + + await githubCallback(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith('/auth/error'); + }); + + it('should redirect if not a user', async () => { + fetchProvider.mockResolvedValue({ + provider: 'github', + clientId: 'id', + clientSecret: 'secret', + }); + + config.get.mockReturnValue('https://site.com'); + axios.post.mockResolvedValue({ data: { access_token: 'token' } }); + axios.get.mockResolvedValueOnce({ data: { type: 'Bot' } }); + + await githubCallback(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith( + '/error?code=not_a_user&provider=github' + ); + }); + + it('should redirect if missing email', async () => { + fetchProvider.mockResolvedValue({ + provider: 'github', + clientId: 'id', + clientSecret: 'secret', + }); + + config.get.mockReturnValue('https://site.com'); + axios.post.mockResolvedValue({ data: { access_token: 'token' } }); + axios.get.mockResolvedValueOnce({ + data: { type: 'User', id: 'i', login: 'l', avatar_url: 'a' }, + }); + axios.get.mockResolvedValueOnce({ + data: [{ primary: false, verified: false }], + }); + + await githubCallback(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith( + '/error?code=missing_email&provider=github' + ); + }); + + it('should set authUser and call next if user and email found', async () => { + fetchProvider.mockResolvedValue({ + provider: 'github', + clientId: 'id', + clientSecret: 'secret', + }); + + config.get.mockReturnValue('https://site.com'); + axios.post.mockResolvedValue({ data: { access_token: 'token' } }); + axios.get.mockResolvedValueOnce({ + data: { type: 'User', id: 'i', login: 'l', avatar_url: 'a' }, + }); + axios.get.mockResolvedValueOnce({ + data: [{ primary: true, verified: true, email: 'e' }], + }); + + await githubCallback(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.locals.authUser).toEqual({ + id: 'i', + avatar: 'a', + username: 'l', + email: 'e', + provider: 'github', + }); + + expect(fakeNext).toHaveBeenCalled(); + }); + + it('should handle errors gracefully', async () => { + fetchProvider.mockImplementation(() => { + throw new Error('fail'); + }); + + await githubCallback(fakeRequest, fakeResponse, fakeNext); + + expect(handleError).toHaveBeenCalled(); + }); +}); diff --git a/test/server/middleware/auth/callback/google.test.js b/test/server/middleware/auth/callback/google.test.js new file mode 100644 index 00000000..10bd52fb --- /dev/null +++ b/test/server/middleware/auth/callback/google.test.js @@ -0,0 +1,103 @@ +import axios from 'axios'; +import { createRequest, createResponse } from 'node-mocks-http'; +import config from '../../../../../server/utils/config.js'; +import { handleError } from '../../../../../server/utils/errors.js'; +import { fetchProvider } from '../../../../../server/database/queries/provider.js'; +import googleCallback from '../../../../../server/middleware/auth/callback/google.js'; + +vi.mock('axios'); +vi.mock('../../../../../server/database/queries/provider.js'); +vi.mock('../../../../../server/utils/config.js'); +vi.mock('../../../../../server/utils/errors.js'); + +describe('googleCallback', () => { + let fakeRequest, fakeResponse, fakeNext; + + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.query = { code: 'abc' }; + fakeResponse.redirect = vi.fn(); + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.send = vi.fn(); + fakeResponse.locals = {}; + fakeNext = vi.fn(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if no code provided', async () => { + fakeRequest.query.code = undefined; + + await googleCallback(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.send).toHaveBeenCalledWith('No code provided'); + }); + + it('should redirect if provider not found', async () => { + fetchProvider.mockResolvedValue(null); + + await googleCallback(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith( + '/auth/error?code=provider_not_found&provider=google' + ); + }); + + it('should redirect if user not verified', async () => { + fetchProvider.mockResolvedValue({ + provider: 'google', + clientId: 'id', + clientSecret: 'secret', + }); + + config.get.mockReturnValue('https://site.com'); + axios.post.mockResolvedValue({ data: { access_token: 'token' } }); + axios.get.mockResolvedValue({ data: { verified_email: false } }); + + await googleCallback(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith( + '/error?code=unverified_user&provider=google' + ); + }); + + it('should set authUser and call next if user verified', async () => { + fetchProvider.mockResolvedValue({ + provider: 'google', + clientId: 'id', + clientSecret: 'secret', + }); + + config.get.mockReturnValue('https://site.com'); + axios.post.mockResolvedValue({ data: { access_token: 'token' } }); + axios.get.mockResolvedValue({ + data: { picture: 'p', id: 'i', email: 'e', verified_email: true }, + }); + + await googleCallback(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.locals.authUser).toEqual({ + id: 'i', + email: 'e', + avatar: 'p', + username: 'unknown', + provider: 'google', + }); + expect(fakeNext).toHaveBeenCalled(); + }); + + it('should handle errors gracefully', async () => { + fetchProvider.mockImplementation(() => { + throw new Error('fail'); + }); + + await googleCallback(fakeRequest, fakeResponse, fakeNext); + + expect(handleError).toHaveBeenCalled(); + }); +}); diff --git a/test/server/middleware/auth/callback/slack.test.js b/test/server/middleware/auth/callback/slack.test.js new file mode 100644 index 00000000..9400ebd2 --- /dev/null +++ b/test/server/middleware/auth/callback/slack.test.js @@ -0,0 +1,98 @@ +import axios from 'axios'; +import { createRequest, createResponse } from 'node-mocks-http'; +import config from '../../../../../server/utils/config.js'; +import { handleError } from '../../../../../server/utils/errors.js'; +import { fetchProvider } from '../../../../../server/database/queries/provider.js'; +import slackCallback from '../../../../../server/middleware/auth/callback/slack.js'; + +vi.mock('axios'); +vi.mock('../../../../../server/database/queries/provider.js'); +vi.mock('../../../../../server/utils/config.js'); +vi.mock('../../../../../server/utils/errors.js'); + +describe('slackCallback', () => { + let fakeRequest, fakeResponse, fakeNext; + + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.query = { code: 'abc' }; + fakeResponse.redirect = vi.fn(); + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.send = vi.fn(); + fakeResponse.locals = {}; + + fakeNext = vi.fn(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if no code provided', async () => { + fakeRequest.query.code = undefined; + await slackCallback(fakeRequest, fakeResponse, fakeNext); + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.send).toHaveBeenCalledWith('No code provided'); + }); + + it('should redirect if provider not found', async () => { + fetchProvider.mockResolvedValue(null); + await slackCallback(fakeRequest, fakeResponse, fakeNext); + expect(fakeResponse.redirect).toHaveBeenCalledWith('/auth/error'); + }); + + it('should redirect if user not found', async () => { + fetchProvider.mockResolvedValue({ + provider: 'slack', + clientId: 'id', + clientSecret: 'secret', + }); + + config.get.mockReturnValue('https://site.com'); + axios.post.mockResolvedValue({ data: { access_token: 'token' } }); + axios.get.mockResolvedValue({ data: {} }); + + await slackCallback(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith( + '/auth/error?code=unverified_user&provider=slack' + ); + }); + + it('should set authUser and call next if user found', async () => { + fetchProvider.mockResolvedValue({ + provider: 'slack', + clientId: 'id', + clientSecret: 'secret', + }); + + config.get.mockReturnValue('https://site.com'); + axios.post.mockResolvedValue({ data: { access_token: 'token' } }); + axios.get.mockResolvedValue({ + data: { email: 'e', sub: 'i', picture: 'p', name: 'n' }, + }); + + await slackCallback(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.locals.authUser).toEqual({ + id: 'i', + email: 'e', + avatar: 'p', + username: 'n', + provider: 'slack', + }); + expect(fakeNext).toHaveBeenCalled(); + }); + + it('should handle errors gracefully', async () => { + fetchProvider.mockImplementation(() => { + throw new Error('fail'); + }); + + await slackCallback(fakeRequest, fakeResponse, fakeNext); + + expect(handleError).toHaveBeenCalled(); + }); +}); diff --git a/test/server/middleware/auth/callback/twitch.test.js b/test/server/middleware/auth/callback/twitch.test.js new file mode 100644 index 00000000..8a7fa9fd --- /dev/null +++ b/test/server/middleware/auth/callback/twitch.test.js @@ -0,0 +1,104 @@ +import axios from 'axios'; +import { createRequest, createResponse } from 'node-mocks-http'; +import config from '../../../../../server/utils/config.js'; +import { handleError } from '../../../../../server/utils/errors.js'; +import { fetchProvider } from '../../../../../server/database/queries/provider.js'; +import twitchCallback from '../../../../../server/middleware/auth/callback/twitch.js'; + +vi.mock('axios'); +vi.mock('../../../../../server/database/queries/provider.js'); +vi.mock('../../../../../server/utils/config.js'); +vi.mock('../../../../../server/utils/errors.js'); + +describe('twitchCallback', () => { + let fakeRequest, fakeResponse, fakeNext; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.query = { code: 'abc' }; + fakeResponse.redirect = vi.fn(); + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.send = vi.fn(); + fakeResponse.locals = {}; + fakeNext = vi.fn(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should redirect if no code provided', async () => { + fakeRequest.query.code = undefined; + await twitchCallback(fakeRequest, fakeResponse, fakeNext); + expect(fakeResponse.redirect).toHaveBeenCalledWith( + '/error?code=missing_code&provider=twitch' + ); + }); + + it('should redirect if provider not found', async () => { + fetchProvider.mockResolvedValue(null); + await twitchCallback(fakeRequest, fakeResponse, fakeNext); + expect(fakeResponse.redirect).toHaveBeenCalledWith( + '/error?code=provider_not_found&provider=twitch' + ); + }); + + it('should redirect if user not found', async () => { + fetchProvider.mockResolvedValue({ + provider: 'twitch', + clientId: 'id', + clientSecret: 'secret', + }); + + config.get.mockReturnValue('https://site.com'); + axios.post.mockResolvedValue({ data: { access_token: 'token' } }); + axios.get.mockResolvedValue({ data: { data: [] } }); + + await twitchCallback(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith( + '/auth/error?code=unverified_user&provider=twitch' + ); + }); + + it('should set authUser and call next if user found', async () => { + fetchProvider.mockResolvedValue({ + provider: 'twitch', + clientId: 'id', + clientSecret: 'secret', + }); + + config.get.mockReturnValue('https://site.com'); + axios.post.mockResolvedValue({ data: { access_token: 'token' } }); + axios.get.mockResolvedValue({ + data: { + data: [ + { email: 'e', display_name: 'd', id: 'i', profile_image_url: 'p' }, + ], + }, + }); + + await twitchCallback(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.locals.authUser).toEqual({ + id: 'i', + email: 'e', + avatar: 'p', + username: 'd', + provider: 'twitch', + }); + + expect(fakeNext).toHaveBeenCalled(); + }); + + it('should handle errors gracefully', async () => { + fetchProvider.mockImplementation(() => { + throw new Error('fail'); + }); + + await twitchCallback(fakeRequest, fakeResponse, fakeNext); + + expect(handleError).toHaveBeenCalled(); + }); +}); diff --git a/test/server/middleware/auth/config/getConfig.test.js b/test/server/middleware/auth/config/getConfig.test.js new file mode 100644 index 00000000..1fc30846 --- /dev/null +++ b/test/server/middleware/auth/config/getConfig.test.js @@ -0,0 +1,63 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import config from '../../../../../server/utils/config.js'; +import { handleError } from '../../../../../server/utils/errors.js'; +import { fetchProviders } from '../../../../../server/database/queries/provider.js'; +import getConfigMiddleware from '../../../../../server/middleware/auth/config/getConfig.js'; + +vi.mock('../../../../../server/database/queries/provider.js'); +vi.mock('../../../../../server/utils/config.js'); +vi.mock('../../../../../server/utils/errors.js'); + +describe('getConfigMiddleware', () => { + let fakeRequest, fakeResponse; + + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + fakeResponse.json = vi.fn(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return nativeSignin true and sso false if no providers', async () => { + fetchProviders.mockResolvedValue([]); + + config.get.mockReturnValue(undefined); + + await getConfigMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.json).toHaveBeenCalledWith({ + nativeSignin: true, + register: true, + sso: false, + providers: [], + }); + }); + + it('should return sso true and nativeSignin from config if SSO enabled', async () => { + fetchProviders.mockResolvedValue([{ provider: 'google', enabled: true }]); + + config.get.mockImplementation((key) => + key === 'nativeSignin' ? false : true + ); + + await getConfigMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.json).toHaveBeenCalledWith({ + nativeSignin: false, + register: true, + sso: true, + providers: ['google'], + }); + }); + + it('should handle errors gracefully', async () => { + fetchProviders.mockRejectedValue(new Error('fail')); + + await getConfigMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalled(); + }); +}); diff --git a/test/server/middleware/auth/config/update.test.js b/test/server/middleware/auth/config/update.test.js new file mode 100644 index 00000000..4b1f7ba7 --- /dev/null +++ b/test/server/middleware/auth/config/update.test.js @@ -0,0 +1,70 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import config from '../../../../../server/utils/config.js'; +import { handleError } from '../../../../../server/utils/errors.js'; +import ConfigValidator from '../../../../../shared/validators/config.js'; +import { fetchProviders } from '../../../../../server/database/queries/provider.js'; +import updateConfigMiddleware from '../../../../../server/middleware/auth/config/update.js'; + +vi.mock('../../../../../shared/validators/config.js'); +vi.mock('../../../../../server/database/queries/provider.js'); +vi.mock('../../../../../server/utils/config.js'); +vi.mock('../../../../../server/utils/errors.js'); + +describe('updateConfigMiddleware', () => { + let fakeRequest, fakeResponse; + + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.body = { nativeSignin: true, register: true }; + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.json = vi.fn().mockReturnThis(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if ConfigValidator returns string error', async () => { + ConfigValidator.mockReturnValue('error'); + + await updateConfigMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.json).toHaveBeenCalledWith({ error: 'error' }); + }); + + it('should return 400 if nativeSignin is false and SSO not enabled', async () => { + ConfigValidator.mockReturnValue({ nativeSignin: false }); + fetchProviders.mockResolvedValue([{ enabled: false }]); + + await updateConfigMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.json).toHaveBeenCalledWith({ + error: + 'SSO is not enabled, please enable that before turning off native signin', + }); + }); + + it('should set config and return success if valid', async () => { + ConfigValidator.mockReturnValue({ nativeSignin: true, register: true }); + + await updateConfigMiddleware(fakeRequest, fakeResponse); + + expect(config.set).toHaveBeenCalledWith('nativeSignin', true); + expect(config.set).toHaveBeenCalledWith('register', true); + expect(fakeResponse.json).toHaveBeenCalledWith({ success: true }); + }); + + it('should handle errors gracefully', async () => { + ConfigValidator.mockImplementation(() => { + throw new Error('fail'); + }); + + await updateConfigMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalled(); + }); +}); diff --git a/test/server/middleware/auth/emailExists.test.js b/test/server/middleware/auth/emailExists.test.js new file mode 100644 index 00000000..ab65a28b --- /dev/null +++ b/test/server/middleware/auth/emailExists.test.js @@ -0,0 +1,42 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import emailExistsMiddleware from '../../../../server/middleware/auth/emailExists.js'; +import { getUserByEmail } from '../../../../server/database/queries/user.js'; + +vi.mock('../../../../server/database/queries/user.js'); + +describe('emailExistsMiddleware', () => { + let fakeRequest; + let fakeResponse; + + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.body = { email: 'test@example.com' }; + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.send = vi.fn().mockReturnThis(); + fakeResponse.sendStatus = vi.fn().mockReturnThis(); + }); + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return 400 if no email provided', async () => { + fakeRequest.body.email = undefined; + await emailExistsMiddleware(fakeRequest, fakeResponse); + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.send).toHaveBeenCalledWith('No email provided'); + }); + + it('should return 404 if user not found', async () => { + getUserByEmail.mockResolvedValue(null); + await emailExistsMiddleware(fakeRequest, fakeResponse); + expect(fakeResponse.sendStatus).toHaveBeenCalledWith(404); + }); + + it('should return 200 if user found', async () => { + getUserByEmail.mockResolvedValue({ id: 1 }); + await emailExistsMiddleware(fakeRequest, fakeResponse); + expect(fakeResponse.sendStatus).toHaveBeenCalledWith(200); + }); +}); diff --git a/test/server/middleware/auth/login.test.js b/test/server/middleware/auth/login.test.js index c15c8804..88b1ef5c 100644 --- a/test/server/middleware/auth/login.test.js +++ b/test/server/middleware/auth/login.test.js @@ -1,4 +1,3 @@ -import { beforeEach, describe, it, vi } from 'vitest'; import { createRequest, createResponse } from 'node-mocks-http'; import SQLite from '../../../../server/database/sqlite/setup'; import login from '../../../../server/middleware/auth/login'; diff --git a/test/server/middleware/auth/platform.test.js b/test/server/middleware/auth/platform.test.js new file mode 100644 index 00000000..6c960220 --- /dev/null +++ b/test/server/middleware/auth/platform.test.js @@ -0,0 +1,66 @@ +import config from '../../../../server/utils/config.js'; +import { createRequest, createResponse } from 'node-mocks-http'; +import { getAuthRedirectUrl } from '../../../../shared/utils/authenication.js'; +import { fetchProvider } from '../../../../server/database/queries/provider.js'; +import redirectUsingProviderMiddleware from '../../../../server/middleware/auth/platform.js'; + +vi.mock('../../../../server/utils/config.js'); +vi.mock('../../../../shared/utils/authenication.js'); +vi.mock('../../../../server/database/queries/provider.js'); + +describe('redirectUsingProviderMiddleware', () => { + let fakeRequest, fakeResponse; + + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.params = { provider: 'google' }; + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.send = vi.fn().mockReturnThis(); + fakeResponse.redirect = vi.fn().mockReturnThis(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return 400 if no provider provided', async () => { + fakeRequest.params.provider = undefined; + + await redirectUsingProviderMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.send).toHaveBeenCalledWith('No provider provided'); + }); + + it('should return 404 if provider not found', async () => { + fetchProvider.mockResolvedValue(null); + + await redirectUsingProviderMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(404); + expect(fakeResponse.send).toHaveBeenCalledWith( + 'Unable to find SSO for provider' + ); + }); + + it('should redirect to provider URL if provider found', async () => { + fetchProvider.mockResolvedValue({ + provider: 'google', + clientId: 'abc', + }); + + config.get.mockReturnValue('https://site.com'); + getAuthRedirectUrl.mockReturnValue('https://auth.url'); + + await redirectUsingProviderMiddleware(fakeRequest, fakeResponse); + + expect(getAuthRedirectUrl).toHaveBeenCalledWith( + 'google', + 'abc', + 'https://site.com/api/auth/callback/google' + ); + expect(fakeResponse.redirect).toHaveBeenCalledWith('https://auth.url'); + }); +}); diff --git a/test/server/middleware/auth/setup.test.js b/test/server/middleware/auth/setup.test.js new file mode 100644 index 00000000..4a955b8f --- /dev/null +++ b/test/server/middleware/auth/setup.test.js @@ -0,0 +1,60 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { ownerExists } from '../../../../server/database/queries/user.js'; +import setupMiddleware from '../../../../server/middleware/auth/setup.js'; +import config from '../../../../server/utils/config.js'; + +vi.mock('../../../../server/utils/config.js'); +vi.mock('../../../../server/database/queries/user.js'); + +describe('setupMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.body = { + type: 'basic', + email: 'test@example.com', + username: 'user', + password: 'pass', + databaseType: 'sqlite', + databaseName: 'db', + websiteUrl: 'http://localhost', + migrationType: 'none', + }; + + fakeRequest.headers = { 'user-agent': 'test-agent' }; + + fakeRequest.protocol = 'http'; + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.send = vi.fn().mockReturnThis(); + fakeResponse.sendStatus = vi.fn().mockReturnThis(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should return 400 for invalid setup type', async () => { + fakeRequest.body.type = 'invalid'; + + await setupMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.send).toHaveBeenCalledWith({ + general: 'Invalid setup type', + }); + }); + + it('should return 400 if database and owner exists', async () => { + config.get.mockReturnValue({ name: 'db' }); + ownerExists.mockImplementation(() => Promise.resolve(true)); + + await setupMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.send).toHaveBeenCalledWith( + expect.objectContaining({ errorType: 'ownerExists' }) + ); + }); +}); diff --git a/test/server/middleware/auth/signInOrRegisterUsingAuth.test.js b/test/server/middleware/auth/signInOrRegisterUsingAuth.test.js new file mode 100644 index 00000000..f46c6ae3 --- /dev/null +++ b/test/server/middleware/auth/signInOrRegisterUsingAuth.test.js @@ -0,0 +1,84 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { + getUserByEmail, + registerSsoUser, +} from '../../../../server/database/queries/user.js'; +import { handleError } from '../../../../server/utils/errors.js'; +import { parseUserAgent } from '../../../../server/utils/uaParser.js'; +import { createUserSession } from '../../../../server/database/queries/session.js'; +import { fetchConnectionByEmail } from '../../../../server/database/queries/connection.js'; +import signInOrRegisterUsingAuth from '../../../../server/middleware/auth/signInOrRegisterUsingAuth.js'; + +vi.mock('../../../../server/utils/errors.js'); +vi.mock('../../../../shared/utils/cookies.js'); +vi.mock('../../../../server/utils/uaParser.js'); +vi.mock('../../../../server/database/queries/user.js'); +vi.mock('../../../../server/database/queries/session.js'); +vi.mock('../../../../server/database/queries/connection.js'); + +describe('signInOrRegisterUsingAuth', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.headers = { 'user-agent': 'test-agent' }; + fakeRequest.protocol = 'http'; + + fakeResponse.locals = { + authUser: { + avatar: 'a', + id: 'id', + username: 'user', + email: 'test@example.com', + provider: 'google', + }, + }; + fakeResponse.redirect = vi.fn().mockReturnThis(); + fakeResponse.json = vi.fn().mockReturnThis(); + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.send = vi.fn().mockReturnThis(); + fakeResponse.sendStatus = vi.fn().mockReturnThis(); + + parseUserAgent.mockReturnValue({ device: 'dev', data: {} }); + createUserSession.mockResolvedValue('token'); + handleError.mockImplementation((error, res) => { + res.json({ error: error.message }); + }); + }); + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should call fetchConnectionByEmail with correct params', async () => { + fetchConnectionByEmail.mockResolvedValue(null); + getUserByEmail.mockResolvedValue(null); + registerSsoUser.mockResolvedValue(null); + + await signInOrRegisterUsingAuth(fakeRequest, fakeResponse); + + expect(fetchConnectionByEmail).toHaveBeenCalledWith('google', 'id'); + }); + + it('should redirect to /home if user is verified', async () => { + fetchConnectionByEmail.mockResolvedValue(true); + getUserByEmail.mockResolvedValue({ isVerified: true }); + await signInOrRegisterUsingAuth(fakeRequest, fakeResponse); + expect(fakeResponse.redirect).toHaveBeenCalledWith('/home'); + }); + + it('should redirect to /verify if user is not verified', async () => { + fetchConnectionByEmail.mockResolvedValue(true); + getUserByEmail.mockResolvedValue({ isVerified: false }); + await signInOrRegisterUsingAuth(fakeRequest, fakeResponse); + expect(fakeResponse.redirect).toHaveBeenCalledWith('/verify'); + }); + + it('should handle errors gracefully', async () => { + fetchConnectionByEmail.mockRejectedValue(new Error('fail')); + + await signInOrRegisterUsingAuth(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalled(); + }); +}); diff --git a/test/server/middleware/incident/addMessage.test.js b/test/server/middleware/incident/addMessage.test.js new file mode 100644 index 00000000..7aec927c --- /dev/null +++ b/test/server/middleware/incident/addMessage.test.js @@ -0,0 +1,87 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { + fetchIncident, + updateIncident, +} from '../../../../server/database/queries/incident.js'; +import statusCache from '../../../../server/cache/status.js'; +import { incidentMessageValidator } from '../../../../shared/validators/incident.js'; +import createIncidentMessageMiddleware from '../../../../server/middleware/incident/addMessage.js'; + +vi.mock('../../../../shared/validators/incident.js'); +vi.mock('../../../../server/database/queries/incident.js'); +vi.mock('../../../../server/cache/status.js'); + +describe('createIncidentMessageMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.body = { + message: 'msg', + status: 'ok', + monitorIds: ['m'], + incidentId: 'id', + }; + + fakeRequest.locals = { user: { email: 'e' } }; + fakeResponse.locals = { user: { email: 'e' } }; + + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.json = vi.fn(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if incidentId is missing', async () => { + fakeRequest.body.incidentId = undefined; + + await createIncidentMessageMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.json).toHaveBeenCalledWith({ + message: 'Incident id is required', + }); + }); + + it('should return 400 if message is invalid', async () => { + fakeRequest.body.incidentId = 'id'; + incidentMessageValidator.mockReturnValue('invalid'); + + await createIncidentMessageMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.json).toHaveBeenCalledWith({ message: 'invalid' }); + }); + + it('should return 404 if incident not found', async () => { + incidentMessageValidator.mockReturnValue(false); + fetchIncident.mockResolvedValue(null); + + await createIncidentMessageMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(404); + expect(fakeResponse.json).toHaveBeenCalledWith({ + message: 'Incident not found', + }); + }); + + it('should update incident and return data if valid', async () => { + incidentMessageValidator.mockReturnValue(false); + fetchIncident.mockResolvedValue({ + messages: [], + status: 'old', + monitorIds: ['m'], + incidentId: 'id', + }); + updateIncident.mockResolvedValue({ updated: true }); + + await createIncidentMessageMiddleware(fakeRequest, fakeResponse); + + expect(updateIncident).toHaveBeenCalled(); + expect(statusCache.addIncident).toHaveBeenCalled(); + expect(fakeResponse.json).toHaveBeenCalledWith({ updated: true }); + }); +}); diff --git a/test/server/middleware/incident/create.test.js b/test/server/middleware/incident/create.test.js new file mode 100644 index 00000000..57938914 --- /dev/null +++ b/test/server/middleware/incident/create.test.js @@ -0,0 +1,68 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import statusCache from '../../../../server/cache/status.js'; +import { handleError } from '../../../../server/utils/errors.js'; +import IncidentValidator from '../../../../shared/validators/incident.js'; +import { createIncident } from '../../../../server/database/queries/incident.js'; +import createIncidentMiddleware from '../../../../server/middleware/incident/create.js'; + +vi.mock('../../../../shared/validators/incident.js'); +vi.mock('../../../../server/database/queries/incident.js'); +vi.mock('../../../../server/cache/status.js'); +vi.mock('../../../../server/utils/errors.js'); + +describe('createIncidentMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.body = { + title: 't', + monitorIds: ['m'], + affect: 'a', + status: 's', + message: 'msg', + }; + + fakeRequest.locals = { user: { email: 'e' } }; + + fakeResponse.locals = { user: { email: 'e' } }; + + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.json = vi.fn(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if incident is invalid', async () => { + IncidentValidator.mockReturnValue('invalid'); + + await createIncidentMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.json).toHaveBeenCalledWith({ message: 'invalid' }); + }); + + it('should create incident and return data if valid', async () => { + IncidentValidator.mockReturnValue(false); + createIncident.mockResolvedValue({ created: true }); + + await createIncidentMiddleware(fakeRequest, fakeResponse); + + expect(createIncident).toHaveBeenCalled(); + expect(statusCache.addIncident).toHaveBeenCalled(); + expect(fakeResponse.json).toHaveBeenCalledWith({ created: true }); + }); + + it('should handle errors gracefully', async () => { + IncidentValidator.mockImplementation(() => { + throw new Error('fail'); + }); + + await createIncidentMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalled(); + }); +}); diff --git a/test/server/middleware/incident/delete.test.js b/test/server/middleware/incident/delete.test.js new file mode 100644 index 00000000..c569b738 --- /dev/null +++ b/test/server/middleware/incident/delete.test.js @@ -0,0 +1,58 @@ +import { afterEach } from 'vitest'; +import { createRequest, createResponse } from 'node-mocks-http'; +import statusCache from '../../../../server/cache/status.js'; +import { deleteIncident } from '../../../../server/database/queries/incident.js'; +import deleteIncidentMiddleware from '../../../../server/middleware/incident/delete.js'; + +vi.mock('../../../../server/database/queries/incident.js'); +vi.mock('../../../../server/cache/status.js'); + +describe('deleteIncidentMiddleware', () => { + let fakeRequest, fakeResponse; + + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.body = { incidentId: 'id' }; + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.json = vi.fn(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if incidentId is missing', async () => { + fakeRequest.body.incidentId = undefined; + + await deleteIncidentMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.json).toHaveBeenCalledWith({ + message: 'Incident id is required', + }); + }); + + it('should delete incident and return success', async () => { + await deleteIncidentMiddleware(fakeRequest, fakeResponse); + + expect(deleteIncident).toHaveBeenCalledWith('id'); + expect(statusCache.deleteIncident).toHaveBeenCalledWith('id'); + expect(fakeResponse.status).toHaveBeenCalledWith(200); + expect(fakeResponse.json).toHaveBeenCalledWith({ + message: 'Incident deleted successfully', + }); + }); + + it('should handle errors and return 400', async () => { + deleteIncident.mockImplementation(() => { + throw new Error('fail'); + }); + + await deleteIncidentMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.json).toHaveBeenCalledWith({ message: 'fail' }); + }); +}); diff --git a/test/server/middleware/incident/deleteMessage.test.js b/test/server/middleware/incident/deleteMessage.test.js new file mode 100644 index 00000000..05ee9f80 --- /dev/null +++ b/test/server/middleware/incident/deleteMessage.test.js @@ -0,0 +1,106 @@ +import { + fetchIncident, + updateIncident, +} from '../../../../server/database/queries/incident.js'; +import statusCache from '../../../../server/cache/status.js'; +import { handleError } from '../../../../server/utils/errors.js'; +import deleteIncidentMessageMiddleware from '../../../../server/middleware/incident/deleteMessage.js'; +import { createRequest, createResponse } from 'node-mocks-http'; + +vi.mock('../../../../server/database/queries/incident.js'); +vi.mock('../../../../server/cache/status.js'); +vi.mock('../../../../server/utils/errors.js'); + +describe('deleteIncidentMessageMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.body = { incidentId: 'id', position: 0 }; + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.json = vi.fn(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if incidentId is missing', async () => { + fakeRequest.body.incidentId = undefined; + + await deleteIncidentMessageMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.json).toHaveBeenCalledWith({ + message: 'Incident id is required', + }); + }); + + it('should return 404 if incident not found', async () => { + fetchIncident.mockResolvedValue(null); + + await deleteIncidentMessageMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(404); + expect(fakeResponse.json).toHaveBeenCalledWith({ + message: 'Incident not found', + }); + }); + + it('should return 404 if position is invalid', async () => { + fetchIncident.mockResolvedValue({ messages: [] }); + + await deleteIncidentMessageMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(404); + expect(fakeResponse.json).toHaveBeenCalledWith({ + message: 'Message not found', + }); + }); + + it('should return 404 if only one message', async () => { + fetchIncident.mockResolvedValue({ messages: [{}, {}] }); + + fakeRequest.body.position = 0; + fetchIncident.mockResolvedValue({ messages: [{}] }); + + await deleteIncidentMessageMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(404); + expect(fakeResponse.json).toHaveBeenCalledWith({ + message: 'Need to have at least one message', + }); + }); + + it('should update incident and return data if valid', async () => { + const responseQuery = { + messages: [{ status: 'old' }, { status: 'new' }], + status: 'unknown', + incidentId: 'id', + }; + + fetchIncident.mockResolvedValue(responseQuery); + + updateIncident.mockResolvedValue({ updated: true }); + + await deleteIncidentMessageMiddleware(fakeRequest, fakeResponse); + + expect(updateIncident).toHaveBeenCalled(); + expect(statusCache.addIncident).toHaveBeenCalled(); + expect(fakeResponse.json).toHaveBeenCalledWith({ + ...responseQuery, + status: 'new', + }); + }); + + it('should handle errors gracefully', async () => { + fetchIncident.mockImplementation(() => { + throw new Error('fail'); + }); + + await deleteIncidentMessageMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalled(); + }); +}); diff --git a/test/server/middleware/incident/getAll.test.js b/test/server/middleware/incident/getAll.test.js new file mode 100644 index 00000000..476e4f10 --- /dev/null +++ b/test/server/middleware/incident/getAll.test.js @@ -0,0 +1,46 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import logger from '../../../../server/utils/logger.js'; +import getAllIncidents from '../../../../server/middleware/incident/getAll.js'; +import { fetchAllIncidents } from '../../../../server/database/queries/incident.js'; + +vi.mock('../../../../server/database/queries/incident.js'); +vi.mock('../../../../server/utils/logger.js'); + +describe('getAllIncidents', () => { + let fakeRequest, fakeResponse; + + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeResponse.json = vi.fn(); + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.send = vi.fn().mockReturnThis(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return incidents on success', async () => { + fetchAllIncidents.mockResolvedValue([{ id: 1 }]); + + await getAllIncidents(fakeRequest, fakeResponse); + + expect(fakeResponse.json).toHaveBeenCalledWith([{ id: 1 }]); + }); + + it('should handle errors and log', async () => { + fetchAllIncidents.mockImplementation(() => { + throw new Error('fail'); + }); + + await getAllIncidents(fakeRequest, fakeResponse); + + expect(logger.error).toHaveBeenCalled(); + expect(fakeResponse.status).toHaveBeenCalledWith(500); + expect(fakeResponse.send).toHaveBeenCalledWith({ + message: 'Something went wrong', + }); + }); +}); diff --git a/test/server/middleware/incident/getUsingId.test.js b/test/server/middleware/incident/getUsingId.test.js new file mode 100644 index 00000000..ecb66fdc --- /dev/null +++ b/test/server/middleware/incident/getUsingId.test.js @@ -0,0 +1,50 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { handleError } from '../../../../server/utils/errors.js'; +import { fetchIncident } from '../../../../server/database/queries/incident.js'; +import fetchIncidentUsingId from '../../../../server/middleware/incident/getUsingId.js'; + +vi.mock('../../../../server/database/queries/incident.js'); +vi.mock('../../../../server/utils/errors.js'); + +describe('fetchIncidentUsingId', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.query = { incidentId: 'id' }; + fakeResponse.json = vi.fn(); + fakeResponse.status = vi.fn().mockReturnThis(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should throw UnprocessableError if incidentId is missing', async () => { + fakeRequest.query.incidentId = undefined; + + await fetchIncidentUsingId(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalled(); + }); + + it('should return 404 if incident not found', async () => { + fetchIncident.mockResolvedValue(null); + + await fetchIncidentUsingId(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(404); + expect(fakeResponse.json).toHaveBeenCalledWith({ + error: 'Incident not found', + }); + }); + + it('should return data if found', async () => { + fetchIncident.mockResolvedValue({ id: 1 }); + + await fetchIncidentUsingId(fakeRequest, fakeResponse); + + expect(fakeResponse.json).toHaveBeenCalledWith({ id: 1 }); + }); +}); diff --git a/test/server/middleware/incident/update.test.js b/test/server/middleware/incident/update.test.js new file mode 100644 index 00000000..0eaa2aba --- /dev/null +++ b/test/server/middleware/incident/update.test.js @@ -0,0 +1,43 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import statusCache from '../../../../server/cache/status.js'; +import IncidentValidator from '../../../../shared/validators/incident.js'; +import { updateIncident } from '../../../../server/database/queries/incident.js'; +import updateIncidentMiddleware from '../../../../server/middleware/incident/update.js'; + +vi.mock('../../../../shared/validators/incident.js'); +vi.mock('../../../../server/database/queries/incident.js'); +vi.mock('../../../../server/cache/status.js'); + +describe('updateIncidentMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.body = { incident: { messages: [{}, {}], incidentId: 'id' } }; + fakeResponse.json = vi.fn(); + fakeResponse.status = vi.fn().mockReturnThis(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if incident is invalid', async () => { + IncidentValidator.mockReturnValue('invalid'); + await updateIncidentMiddleware(fakeRequest, fakeResponse); + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.json).toHaveBeenCalledWith({ message: 'invalid' }); + }); + + it('should update incident and return data if valid', async () => { + IncidentValidator.mockReturnValue(false); + updateIncident.mockResolvedValue({ updated: true }); + + await updateIncidentMiddleware(fakeRequest, fakeResponse); + + expect(updateIncident).toHaveBeenCalled(); + expect(statusCache.addIncident).toHaveBeenCalled(); + expect(fakeResponse.json).toHaveBeenCalledWith({ updated: true }); + }); +}); diff --git a/test/server/middleware/incident/updateMessage.test.js b/test/server/middleware/incident/updateMessage.test.js new file mode 100644 index 00000000..860b54d7 --- /dev/null +++ b/test/server/middleware/incident/updateMessage.test.js @@ -0,0 +1,110 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { + fetchIncident, + updateIncident, +} from '../../../../server/database/queries/incident.js'; +import statusCache from '../../../../server/cache/status.js'; +import { incidentMessageValidator } from '../../../../shared/validators/incident.js'; +import updateIncidentMessageMiddleware from '../../../../server/middleware/incident/updateMessage.js'; + +vi.mock('../../../../shared/validators/incident.js'); +vi.mock('../../../../server/database/queries/incident.js'); +vi.mock('../../../../server/cache/status.js'); + +describe('updateIncidentMessageMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.body = { + message: 'msg', + status: 'ok', + monitorIds: ['m'], + incidentId: 'id', + position: 0, + }; + fakeRequest.locals = { user: { email: 'e' } }; + + fakeResponse.json = vi.fn(); + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.locals = { user: { email: 'e' } }; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if incidentId or position is missing', async () => { + fakeRequest.body.incidentId = undefined; + + await updateIncidentMessageMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.json).toHaveBeenCalledWith({ + message: 'Incident id is required', + }); + }); + + it('should return 400 if message is invalid', async () => { + fakeRequest.body.incidentId = 'id'; + fakeRequest.body.position = 0; + incidentMessageValidator.mockReturnValue('invalid'); + + await updateIncidentMessageMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.json).toHaveBeenCalledWith({ message: 'invalid' }); + }); + + it('should return 404 if incident not found', async () => { + incidentMessageValidator.mockReturnValue(false); + fetchIncident.mockResolvedValue(null); + + await updateIncidentMessageMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(404); + expect(fakeResponse.json).toHaveBeenCalledWith({ + message: 'Incident not found', + }); + }); + + it('should return 404 if message not found at position', async () => { + incidentMessageValidator.mockReturnValue(false); + fetchIncident.mockResolvedValue({ messages: [] }); + + await updateIncidentMessageMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(404); + expect(fakeResponse.json).toHaveBeenCalledWith({ + message: 'Message not found', + }); + }); + + it('should update message and return data if valid', async () => { + incidentMessageValidator.mockReturnValue(false); + + fetchIncident.mockResolvedValue({ + messages: [ + { message: 'old', status: 'old', email: 'old', monitorIds: ['m'] }, + ], + status: 'old', + monitorIds: ['m'], + incidentId: 'id', + }); + + updateIncident.mockResolvedValue({ updated: true }); + + fakeResponse = { + status: vi.fn().mockReturnThis(), + json: vi.fn(), + locals: { user: { email: 'e' } }, + }; + + await updateIncidentMessageMiddleware(fakeRequest, fakeResponse); + + expect(updateIncident).toHaveBeenCalled(); + expect(statusCache.addIncident).toHaveBeenCalled(); + expect(fakeResponse.json).toHaveBeenCalledWith({ updated: true }); + }); +}); diff --git a/test/server/middleware/invites/create.test.js b/test/server/middleware/invites/create.test.js new file mode 100644 index 00000000..9b783ec7 --- /dev/null +++ b/test/server/middleware/invites/create.test.js @@ -0,0 +1,60 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { handleError } from '../../../../server/utils/errors.js'; +import { createInvite } from '../../../../server/database/queries/invite.js'; +import { isValidBitFlags } from '../../../../shared/permissions/isValidBitFlags.js'; +import createInviteMiddleware from '../../../../server/middleware/invites/create.js'; + +vi.mock('../../../../shared/permissions/isValidBitFlags.js'); +vi.mock('../../../../server/database/queries/invite.js'); +vi.mock('../../../../server/utils/errors.js'); + +describe('createInviteMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.body = { expiry: 'e', limit: 1, permission: 1 }; + fakeRequest.locals = { user: { email: 'e' } }; + + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.send = vi.fn(); + fakeResponse.locals = { user: { email: 'e' } }; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if permission is invalid', async () => { + isValidBitFlags.mockReturnValue(false); + + await createInviteMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.send).toHaveBeenCalledWith({ + message: 'Invalid permission flags provided', + }); + }); + + it('should create invite and return success', async () => { + isValidBitFlags.mockReturnValue(true); + createInvite.mockResolvedValue('invite'); + + await createInviteMiddleware(fakeRequest, fakeResponse); + + expect(createInvite).toHaveBeenCalledWith('e', 'e', 1, 1); + expect(fakeResponse.status).toHaveBeenCalledWith(200); + expect(fakeResponse.send).toHaveBeenCalledWith({ invite: 'invite' }); + }); + + it('should handle errors gracefully', async () => { + isValidBitFlags.mockImplementation(() => { + throw new Error('fail'); + }); + + await createInviteMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalled(); + }); +}); diff --git a/test/server/middleware/invites/delete.test.js b/test/server/middleware/invites/delete.test.js new file mode 100644 index 00000000..376721f4 --- /dev/null +++ b/test/server/middleware/invites/delete.test.js @@ -0,0 +1,70 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { + deleteInvite, + fetchInviteUsingId, +} from '../../../../server/database/queries/invite.js'; +import { handleError } from '../../../../server/utils/errors.js'; +import deleteInviteMiddleware from '../../../../server/middleware/invites/delete.js'; + +vi.mock('../../../../server/database/queries/invite.js'); +vi.mock('../../../../server/utils/errors.js'); + +describe('deleteInviteMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.body = { id: 'id' }; + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.send = vi.fn(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if id is missing', async () => { + fakeRequest.body.id = undefined; + + await deleteInviteMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.send).toHaveBeenCalledWith({ + message: 'No invite id provided', + }); + }); + + it('should return 404 if invite not found', async () => { + fetchInviteUsingId.mockResolvedValue(null); + + await deleteInviteMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(404); + expect(fakeResponse.send).toHaveBeenCalledWith({ + message: 'Invite not found', + }); + }); + + it('should delete invite and return success', async () => { + fetchInviteUsingId.mockResolvedValue({ id: 'id' }); + + await deleteInviteMiddleware(fakeRequest, fakeResponse); + + expect(deleteInvite).toHaveBeenCalledWith('id'); + expect(fakeResponse.status).toHaveBeenCalledWith(200); + expect(fakeResponse.send).toHaveBeenCalledWith({ + message: 'Invite has been deleted successfully', + }); + }); + + it('should handle errors gracefully', async () => { + fetchInviteUsingId.mockImplementation(() => { + throw new Error('fail'); + }); + + await deleteInviteMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalled(); + }); +}); diff --git a/test/server/middleware/invites/getAll.test.js b/test/server/middleware/invites/getAll.test.js new file mode 100644 index 00000000..ccf4c74d --- /dev/null +++ b/test/server/middleware/invites/getAll.test.js @@ -0,0 +1,41 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { handleError } from '../../../../server/utils/errors.js'; +import { fetchAllInvites } from '../../../../server/database/queries/invite.js'; +import getAllInvitesMiddleware from '../../../../server/middleware/invites/getAll.js'; + +vi.mock('../../../../server/database/queries/invite.js'); +vi.mock('../../../../server/utils/errors.js'); + +describe('getAllInvitesMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.send = vi.fn(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return invites on success', async () => { + fetchAllInvites.mockResolvedValue([{ id: 1 }]); + + await getAllInvitesMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(200); + expect(fakeResponse.send).toHaveBeenCalledWith({ invites: [{ id: 1 }] }); + }); + + it('should handle errors gracefully', async () => { + fetchAllInvites.mockImplementation(() => { + throw new Error('fail'); + }); + + await getAllInvitesMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalled(); + }); +}); diff --git a/test/server/middleware/invites/pause.test.js b/test/server/middleware/invites/pause.test.js new file mode 100644 index 00000000..03a1f75d --- /dev/null +++ b/test/server/middleware/invites/pause.test.js @@ -0,0 +1,70 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { + fetchInviteUsingId, + pauseInvite, +} from '../../../../server/database/queries/invite.js'; +import { handleError } from '../../../../server/utils/errors.js'; +import pauseInviteMiddleware from '../../../../server/middleware/invites/pause.js'; + +vi.mock('../../../../server/database/queries/invite.js'); +vi.mock('../../../../server/utils/errors.js'); + +describe('pauseInviteMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.body = { id: 'id', paused: true }; + fakeResponse.status = vi.fn().mockReturnThis(); + fakeResponse.send = vi.fn(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if id is missing', async () => { + fakeRequest.body.id = undefined; + + await pauseInviteMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(400); + expect(fakeResponse.send).toHaveBeenCalledWith({ + message: 'No invite id provided', + }); + }); + + it('should return 404 if invite not found', async () => { + fetchInviteUsingId.mockResolvedValue(null); + + await pauseInviteMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse.status).toHaveBeenCalledWith(404); + expect(fakeResponse.send).toHaveBeenCalledWith({ + message: 'Invite not found', + }); + }); + + it('should pause invite and return success', async () => { + fetchInviteUsingId.mockResolvedValue({ id: 'id' }); + + await pauseInviteMiddleware(fakeRequest, fakeResponse); + expect(pauseInvite).toHaveBeenCalledWith('id', true); + + expect(fakeResponse.status).toHaveBeenCalledWith(200); + expect(fakeResponse.send).toHaveBeenCalledWith({ + message: 'Invite has been paused successfully', + }); + }); + + it('should handle errors gracefully', async () => { + fetchInviteUsingId.mockImplementation(() => { + throw new Error('fail'); + }); + + await pauseInviteMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalled(); + }); +}); diff --git a/test/server/middleware/monitor/pause.test.js b/test/server/middleware/monitor/pause.test.js new file mode 100644 index 00000000..13a9ee13 --- /dev/null +++ b/test/server/middleware/monitor/pause.test.js @@ -0,0 +1,119 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import cache from '../../../../server/cache/index.js'; +import statusCache from '../../../../server/cache/status.js'; +import { handleError } from '../../../../server/utils/errors.js'; +import monitorPause from '../../../../server/middleware/monitor/pause.js'; +import { pauseMonitor } from '../../../../server/database/queries/monitor.js'; + +vi.mock('../../../../server/cache/index.js'); +vi.mock('../../../../server/cache/status.js'); +vi.mock('../../../../server/database/queries/monitor.js'); +vi.mock('../../../../server/utils/errors.js'); + +describe('monitorPause middleware', () => { + let fakeRequest, fakeResponse; + + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeResponse.sendStatus = vi.fn().mockReturnThis(); + statusCache.reloadMonitor = vi + .fn() + .mockImplementation(() => Promise.resolve(true)); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should call handleError if monitorId is missing', async () => { + fakeRequest.body = { pause: true }; + + await monitorPause(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + expect(handleError.mock.calls[0][0].message).toMatch(/No monitorId/); + expect(fakeResponse.sendStatus).not.toHaveBeenCalled(); + }); + + it('should call handleError if pause is not boolean-like', async () => { + fakeRequest.body = { monitorId: 'abc', pause: 'notabool' }; + + await monitorPause(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + expect(handleError.mock.calls[0][0].message).toMatch( + /Pause should be a boolean/ + ); + expect(fakeResponse.sendStatus).not.toHaveBeenCalled(); + }); + + it('should pause the monitor and remove it from cache when pause is true', async () => { + fakeRequest.body = { monitorId: 'abc', pause: true }; + + await monitorPause(fakeRequest, fakeResponse); + + expect(pauseMonitor).toHaveBeenCalledWith('abc', true); + expect(cache.removeMonitor).toHaveBeenCalledWith('abc'); + expect(cache.checkStatus).not.toHaveBeenCalled(); + expect(statusCache.reloadMonitor).toHaveBeenCalledWith('abc'); + expect(fakeResponse.sendStatus).toHaveBeenCalledWith(200); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should unpause the monitor and check status when pause is false', async () => { + fakeRequest.body = { monitorId: 'abc', pause: false }; + + await monitorPause(fakeRequest, fakeResponse); + + expect(pauseMonitor).toHaveBeenCalledWith('abc', false); + expect(cache.removeMonitor).not.toHaveBeenCalled(); + expect(cache.checkStatus).toHaveBeenCalledWith('abc'); + expect(statusCache.reloadMonitor).toHaveBeenCalledWith('abc'); + expect(fakeResponse.sendStatus).toHaveBeenCalledWith(200); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should treat string "true" as true and string "false" as false', async () => { + fakeRequest.body = { monitorId: 'abc', pause: 'true' }; + + await monitorPause(fakeRequest, fakeResponse); + + expect(pauseMonitor).toHaveBeenCalledWith('abc', true); + expect(cache.removeMonitor).toHaveBeenCalledWith('abc'); + + fakeRequest.body = { monitorId: 'abc', pause: 'false' }; + await monitorPause(fakeRequest, fakeResponse); + + expect(pauseMonitor).toHaveBeenCalledWith('abc', false); + expect(cache.checkStatus).toHaveBeenCalledWith('abc'); + }); + + it('should handle error thrown by pauseMonitor', async () => { + pauseMonitor.mockImplementationOnce(() => { + throw new Error('db error'); + }); + + fakeRequest.body = { monitorId: 'abc', pause: true }; + + await monitorPause(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + expect(handleError.mock.calls[0][0].message).toMatch(/db error/); + expect(fakeResponse.sendStatus).not.toHaveBeenCalled(); + }); + + it('should not throw if statusCache.reloadMonitor rejects', async () => { + statusCache.reloadMonitor.mockImplementationOnce(() => + Promise.reject(new Error('reload error')) + ); + + fakeRequest.body = { monitorId: 'abc', pause: true }; + + await monitorPause(fakeRequest, fakeResponse); + + expect(fakeResponse.sendStatus).toHaveBeenCalledWith(200); + expect(handleError).not.toHaveBeenCalled(); + }); +}); diff --git a/test/server/middleware/notification/create.test.js b/test/server/middleware/notification/create.test.js new file mode 100644 index 00000000..9328fe67 --- /dev/null +++ b/test/server/middleware/notification/create.test.js @@ -0,0 +1,80 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import randomId from '../../../../server/utils/randomId.js'; +import { handleError } from '../../../../server/utils/errors.js'; +import { UnprocessableError } from '../../../../shared/utils/errors.js'; +import { createNotification } from '../../../../server/database/queries/notification.js'; +import NotificationValidators from '../../../../shared/validators/notifications/index.js'; +import NotificationCreateMiddleware from '../../../../server/middleware/notifications/create.js'; + +vi.mock('../../../../shared/utils/errors.js'); +vi.mock('../../../../server/utils/errors.js'); +vi.mock('../../../../shared/validators/notifications/index.js'); +vi.mock('../../../../server/database/queries/notification.js'); +vi.mock('../../../../server/utils/randomId.js'); + +describe('NotificationCreateMiddleware', () => { + let fakeRequest, fakeResponse; + + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeResponse.locals = { user: { email: 'test' } }; + + createNotification.mockReturnValue({ + id: 'uniqueId', + email: 'test', + isEnabled: true, + }); + + handleError = vi.fn(); + NotificationValidators.Discord = vi.fn(); + randomId.mockReturnValue('uniqueId'); + }); + + afterEach(() => vi.clearAllMocks()); + + it('should call handleError if platform is invalid', async () => { + fakeRequest.body = { platform: 'invalid' }; + + await NotificationCreateMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith( + expect.any(UnprocessableError), + fakeResponse + ); + }); + + it('should create notification and return 201', async () => { + fakeRequest.body = { platform: 'Discord', data: { foo: 'bar' } }; + + const response = await NotificationCreateMiddleware( + fakeRequest, + fakeResponse + ); + + expect(NotificationValidators.Discord).toHaveBeenCalled(); + expect(createNotification).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'uniqueId', + email: 'test', + isEnabled: true, + }) + ); + expect(response._getStatusCode()).toBe(201); + expect(response._getData()).toEqual( + expect.objectContaining({ id: expect.any(String) }) + ); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should call handleError if createNotification throws', async () => { + createNotification.mockImplementationOnce(() => { + throw new Error('fail'); + }); + fakeRequest.body = { platform: 'Discord', data: {} }; + await NotificationCreateMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/notification/delete.test.js b/test/server/middleware/notification/delete.test.js new file mode 100644 index 00000000..d36d0b66 --- /dev/null +++ b/test/server/middleware/notification/delete.test.js @@ -0,0 +1,58 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { handleError } from '../../../../server/utils/errors.js'; +import { UnprocessableError } from '../../../../shared/utils/errors.js'; +import { deleteNotification } from '../../../../server/database/queries/notification.js'; +import NotificationDeleteMiddleware from '../../../../server/middleware/notifications/delete.js'; +import { afterEach } from 'vitest'; + +vi.mock('../../../../server/utils/errors.js'); +vi.mock('../../../../shared/utils/errors.js'); +vi.mock('../../../../server/database/queries/notification.js'); + +describe('NotificationDeleteMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeRequest.query = { notificationId: 'id' }; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should throw if notificationId is missing', async () => { + fakeRequest.query = {}; + + await NotificationDeleteMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith( + expect.any(UnprocessableError), + fakeResponse + ); + }); + + it('should delete notification and return 200', async () => { + const response = await NotificationDeleteMiddleware( + fakeRequest, + fakeResponse + ); + + expect(deleteNotification).toHaveBeenCalledWith('id'); + + expect(response._getStatusCode()).toBe(200); + expect(response._getData()).toBe('Notification deleted'); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should call handleError if deleteNotification throws', async () => { + deleteNotification.mockImplementationOnce(() => { + throw new Error('fail'); + }); + + await NotificationDeleteMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/notification/disable.test.js b/test/server/middleware/notification/disable.test.js new file mode 100644 index 00000000..b7c64ffd --- /dev/null +++ b/test/server/middleware/notification/disable.test.js @@ -0,0 +1,67 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { handleError } from '../../../../server/utils/errors.js'; +import { UnprocessableError } from '../../../../shared/utils/errors.js'; +import { toggleNotification } from '../../../../server/database/queries/notification.js'; +import NotificationToggleMiddleware from '../../../../server/middleware/notifications/disable.js'; + +vi.mock('../../../../server/utils/errors.js'); +vi.mock('../../../../shared/utils/errors.js'); +vi.mock('../../../../server/database/queries/notification.js'); + +describe('NotificationToggleMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should throw if notificationId is missing', async () => { + fakeRequest.query = { isEnabled: 'true' }; + await NotificationToggleMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith( + expect.any(UnprocessableError), + fakeResponse + ); + }); + + it('should throw if isEnabled is not boolean', async () => { + fakeRequest.query = { notificationId: 'id', isEnabled: 'maybe' }; + + await NotificationToggleMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith( + expect.any(UnprocessableError), + fakeResponse + ); + }); + + it('should toggle notification and return 200', async () => { + fakeRequest.query = { notificationId: 'id', isEnabled: 'true' }; + + const response = await NotificationToggleMiddleware( + fakeRequest, + fakeResponse + ); + + expect(toggleNotification).toHaveBeenCalledWith('id', true); + expect(response._getStatusCode()).toBe(200); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should call handleError if toggleNotification throws', async () => { + toggleNotification.mockImplementationOnce(() => { + throw new Error('fail'); + }); + + fakeRequest.query = { notificationId: 'id', isEnabled: 'true' }; + + await NotificationToggleMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/notification/edit.test.js b/test/server/middleware/notification/edit.test.js new file mode 100644 index 00000000..1e26a6fc --- /dev/null +++ b/test/server/middleware/notification/edit.test.js @@ -0,0 +1,88 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { handleError } from '../../../../server/utils/errors.js'; +import { UnprocessableError } from '../../../../shared/utils/errors.js'; +import { editNotification } from '../../../../server/database/queries/notification.js'; +import NotificationValidators from '../../../../shared/validators/notifications/index.js'; +import NotificationEditMiddleware from '../../../../server/middleware/notifications/edit.js'; + +vi.mock('../../../../server/utils/errors.js'); +vi.mock('../../../../shared/utils/errors.js'); +vi.mock('../../../../shared/validators/notifications/index.js'); +vi.mock('../../../../server/database/queries/notification.js'); + +describe('NotificationEditMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + editNotification = vi.fn(function () { + return Promise.resolve({ id: 'id', email: 'test', isEnabled: true }); + }); + + NotificationValidators.Discord = vi.fn(function (data) { + return { + ...data, + platform: 'Discord', + valid: true, + }; + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should call handleError if platform is invalid', async () => { + fakeRequest.body = { platform: 'invalid' }; + + await NotificationEditMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith( + expect.any(UnprocessableError), + fakeResponse + ); + }); + + it('should edit notification and return result', async () => { + fakeRequest.body = { + platform: 'Discord', + data: { foo: 'bar' }, + id: 'id', + email: 'test', + isEnabled: true, + }; + + const response = await NotificationEditMiddleware( + fakeRequest, + fakeResponse + ); + + expect(NotificationValidators.Discord).toHaveBeenCalled(); + expect(editNotification).toHaveBeenCalledWith( + expect.objectContaining({ id: 'id', email: 'test', isEnabled: true }) + ); + expect(response._getJSONData()).toEqual( + expect.objectContaining({ id: 'id' }) + ); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should call handleError if editNotification throws', async () => { + editNotification.mockImplementationOnce(() => { + throw new Error('fail'); + }); + + fakeRequest.body = { + platform: 'Discord', + data: {}, + id: 'id', + email: 'test', + isEnabled: true, + }; + + await NotificationEditMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/notification/getAll.test.js b/test/server/middleware/notification/getAll.test.js new file mode 100644 index 00000000..7f893de0 --- /dev/null +++ b/test/server/middleware/notification/getAll.test.js @@ -0,0 +1,41 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { handleError } from '../../../../server/utils/errors.js'; +import { fetchNotifications } from '../../../../server/database/queries/notification.js'; +import NotificationGetAllMiddleware from '../../../../server/middleware/notifications/getAll.js'; + +vi.mock('../../../../server/utils/errors.js'); +vi.mock('../../../../server/database/queries/notification.js'); + +describe('NotificationGetAllMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fetchNotifications = vi.fn(function () { + return Promise.resolve([{ id: 'id' }]); + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should fetch notifications and return them', async () => { + await NotificationGetAllMiddleware(fakeRequest, fakeResponse); + + expect(fetchNotifications).toHaveBeenCalled(); + expect(fakeResponse._getJSONData()).toEqual([{ id: 'id' }]); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should call handleError if fetchNotifications throws', async () => { + fetchNotifications.mockImplementationOnce(() => { + throw new Error('fail'); + }); + + await NotificationGetAllMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/notification/getUsingId.test.js b/test/server/middleware/notification/getUsingId.test.js new file mode 100644 index 00000000..72044b35 --- /dev/null +++ b/test/server/middleware/notification/getUsingId.test.js @@ -0,0 +1,67 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import logger from '../../../../server/utils/logger.js'; +import { handleError } from '../../../../server/utils/errors.js'; +import { fetchNotificationById } from '../../../../server/database/queries/notification.js'; +import NotificationGetUsingIdMiddleware from '../../../../server/middleware/notifications/getUsingId.js'; + +vi.mock('../../../../server/utils/errors.js'); +vi.mock('../../../../server/database/queries/notification.js'); +vi.mock('../../../../server/utils/logger.js'); + +describe('NotificationGetUsingIdMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fetchNotificationById = vi.fn(function (id) { + return id === 'exists' ? { id: 'exists' } : null; + }); + + logger.error = vi.fn(); + }); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should return 200 and notification if found', async () => { + fakeRequest.query = { notificationId: 'exists' }; + + await NotificationGetUsingIdMiddleware(fakeRequest, fakeResponse); + + expect(fetchNotificationById).toHaveBeenCalledWith('exists'); + expect(fakeResponse._getStatusCode()).toBe(200); + expect(fakeResponse._getData()).toEqual({ id: 'exists' }); + expect(logger.error).not.toHaveBeenCalled(); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should return 404 and log error if not found', async () => { + fakeRequest.query = { notificationId: 'notfound' }; + await NotificationGetUsingIdMiddleware(fakeRequest, fakeResponse); + + expect(fetchNotificationById).toHaveBeenCalledWith('notfound'); + expect(fakeResponse._getStatusCode()).toBe(404); + expect(fakeResponse._getData()).toEqual({ + message: 'Notification not found', + }); + expect(logger.error).toHaveBeenCalledWith( + 'Notification - getById', + expect.objectContaining({ notificationId: 'notfound' }) + ); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should call handleError if fetchNotificationById throws', async () => { + fetchNotificationById.mockImplementationOnce(() => { + throw new Error('fail'); + }); + + fakeRequest.query = { notificationId: 'exists' }; + + await NotificationGetUsingIdMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/notification/test.test.js b/test/server/middleware/notification/test.test.js new file mode 100644 index 00000000..2c659db5 --- /dev/null +++ b/test/server/middleware/notification/test.test.js @@ -0,0 +1,91 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { handleError } from '../../../../server/utils/errors.js'; +import { UnprocessableError } from '../../../../shared/utils/errors.js'; +import NotificationServices from '../../../../server/notifications/index.js'; +import NotificationValidators from '../../../../shared/validators/notifications/index.js'; +import NotificationTestMiddleware from '../../../../server/middleware/notifications/test.js'; + +vi.mock('../../../../server/utils/errors.js'); +vi.mock('../../../../shared/utils/errors.js'); +vi.mock('../../../../shared/validators/notifications/index.js'); +vi.mock('../../../../server/notifications/index.js'); + +describe('NotificationTestMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + NotificationServices.Discord = vi.fn().mockImplementation(function () { + return { + test: vi.fn(function () { + return Promise.resolve(); + }), + }; + }); + + NotificationValidators.Discord = vi.fn(function (data) { + return { + ...data, + platform: 'Discord', + valid: true, + }; + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should call handleError if platform is invalid', async () => { + fakeRequest.body = { platform: 'invalid' }; + + await NotificationTestMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith( + expect.any(UnprocessableError), + fakeResponse + ); + }); + + it('should call handleError if service class is missing', async () => { + NotificationValidators.Discord.mockReturnValueOnce({ + platform: 'notfound', + }); + + fakeRequest.body = { platform: 'Discord', data: {} }; + + await NotificationTestMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith( + expect.any(UnprocessableError), + fakeResponse + ); + }); + + it('should test notification and return 200', async () => { + fakeRequest.body = { platform: 'Discord', data: {} }; + + await NotificationTestMiddleware(fakeRequest, fakeResponse); + + expect(NotificationValidators.Discord).toHaveBeenCalled(); + expect(NotificationServices.Discord).toHaveBeenCalled(); + expect(fakeResponse._getStatusCode()).toBe(200); + expect(fakeResponse._getData()).toBe('Test notification sent'); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should call handleError if service.test throws', async () => { + NotificationServices.Discord.mockImplementationOnce(() => ({ + test: vi.fn(function () { + throw new Error('fail'); + }), + })); + + fakeRequest.body = { platform: 'Discord', data: {} }; + + await NotificationTestMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/provider/configure.test.js b/test/server/middleware/provider/configure.test.js new file mode 100644 index 00000000..238ea92d --- /dev/null +++ b/test/server/middleware/provider/configure.test.js @@ -0,0 +1,113 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { + createProvider, + fetchProvider, + updateProvider, +} from '../../../../server/database/queries/provider.js'; +import { handleError } from '../../../../server/utils/errors.js'; +import ProviderValidator from '../../../../shared/validators/provider.js'; +import configureProviderMiddleware from '../../../../server/middleware/provider/configure.js'; + +vi.mock('../../../../server/utils/errors.js'); +vi.mock('../../../../server/database/queries/provider.js'); +vi.mock('../../../../shared/validators/provider.js'); + +describe('configureProviderMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + fakeResponse.locals = { user: { email: 'test@example.com' } }; + + ProviderValidator = vi.fn(function () { + return false; + }); + createProvider = vi.fn(function () { + return Promise.resolve(); + }); + fetchProvider = vi.fn(function () { + return null; + }); + updateProvider = vi.fn(function () { + return Promise.resolve(); + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if validation fails', async () => { + ProviderValidator.mockReturnValueOnce('invalid'); + fakeRequest.body = { + provider: 'github', + clientId: 'id', + clientSecret: 'secret', + data: {}, + }; + + await configureProviderMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse._getStatusCode()).toBe(400); + expect(fakeResponse._getJSONData()).toEqual({ error: 'invalid' }); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should update provider if exists', async () => { + fetchProvider.mockResolvedValueOnce(true); + fakeRequest.body = { + provider: 'github', + clientId: 'id', + clientSecret: 'secret', + data: { foo: 'bar' }, + }; + + await configureProviderMiddleware(fakeRequest, fakeResponse); + + expect(updateProvider).toHaveBeenCalledWith( + 'github', + expect.objectContaining({ provider: 'github' }) + ); + expect(fakeResponse._getStatusCode()).toBe(200); + expect(fakeResponse._getJSONData()).toEqual( + expect.objectContaining({ provider: 'github' }) + ); + }); + + it('should create provider if not exists', async () => { + fetchProvider.mockResolvedValueOnce(false); + fakeRequest.body = { + provider: 'github', + clientId: 'id', + clientSecret: 'secret', + data: { foo: 'bar' }, + }; + + await configureProviderMiddleware(fakeRequest, fakeResponse); + + expect(createProvider).toHaveBeenCalledWith( + expect.objectContaining({ provider: 'github' }) + ); + expect(fakeResponse._getStatusCode()).toBe(200); + expect(fakeResponse._getJSONData()).toEqual( + expect.objectContaining({ provider: 'github' }) + ); + }); + + it('should call handleError if error thrown', async () => { + fetchProvider.mockImplementationOnce(() => { + throw new Error('fail'); + }); + + fakeRequest.body = { + provider: 'github', + clientId: 'id', + clientSecret: 'secret', + data: {}, + }; + + await configureProviderMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/provider/delete.test.js b/test/server/middleware/provider/delete.test.js new file mode 100644 index 00000000..637d2e8f --- /dev/null +++ b/test/server/middleware/provider/delete.test.js @@ -0,0 +1,42 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { handleError } from '../../../../server/utils/errors.js'; +import { deleteProvider } from '../../../../server/database/queries/provider.js'; +import deleteProviderMiddleware from '../../../../server/middleware/provider/delete.js'; + +vi.mock('../../../../server/utils/errors.js'); +vi.mock('../../../../server/database/queries/provider.js'); + +describe('deleteProviderMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + deleteProvider = vi.fn(function () { + return Promise.resolve(); + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should call deleteProvider with provider from body', async () => { + fakeRequest.body = { provider: 'github' }; + + await deleteProviderMiddleware(fakeRequest, fakeResponse); + + expect(deleteProvider).toHaveBeenCalledWith('github'); + }); + + it('should call handleError if error thrown', async () => { + deleteProvider.mockImplementationOnce(() => { + throw new Error('fail'); + }); + + fakeRequest.body = { provider: 'github' }; + + await deleteProviderMiddleware(fakeRequest, fakeResponse); + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/provider/getAll.test.js b/test/server/middleware/provider/getAll.test.js new file mode 100644 index 00000000..ba230337 --- /dev/null +++ b/test/server/middleware/provider/getAll.test.js @@ -0,0 +1,41 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { handleError } from '../../../../server/utils/errors.js'; +import { fetchProviders } from '../../../../server/database/queries/provider.js'; +import getAllProvidersMiddleware from '../../../../server/middleware/provider/getAll.js'; + +vi.mock('../../../../server/utils/errors.js'); +vi.mock('../../../../server/database/queries/provider.js'); + +describe('getAllProvidersMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fetchProviders = vi.fn(function () { + return Promise.resolve([{ provider: 'github' }]); + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should fetch providers and return them', async () => { + await getAllProvidersMiddleware(fakeRequest, fakeResponse); + + expect(fetchProviders).toHaveBeenCalled(); + expect(fakeResponse._getJSONData()).toEqual([{ provider: 'github' }]); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should call handleError if fetchProviders throws', async () => { + fetchProviders.mockImplementationOnce(() => { + throw new Error('fail'); + }); + + await getAllProvidersMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/status/create.test.js b/test/server/middleware/status/create.test.js new file mode 100644 index 00000000..54dcf18b --- /dev/null +++ b/test/server/middleware/status/create.test.js @@ -0,0 +1,74 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import statusCache from '../../../../server/cache/status.js'; +import { handleError } from '../../../../server/utils/errors.js'; +import { createStatusPage } from '../../../../server/database/queries/status.js'; +import createStatusPageMiddleware from '../../../../server/middleware/status/create.js'; + +vi.mock('../../../../shared/utils/errors.js'); +vi.mock('../../../../server/utils/errors.js'); +vi.mock('../../../../server/database/queries/status.js'); +vi.mock('../../../../shared/validators/status/layout.js'); +vi.mock('../../../../shared/validators/status/settings.js'); +vi.mock('../../../../server/cache/status.js'); + +describe('createStatusPageMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fakeResponse.locals = { user: { email: 'test' } }; + + statusCache.addNewStatusPage = vi.fn(function () { + return Promise.resolve(); + }); + createStatusPage = vi.fn(function () { + return Promise.resolve({ id: 'id' }); + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if no monitors', async () => { + fakeRequest.body = { + settings: {}, + layout: [{ type: 'uptime', monitors: [] }], + }; + + await createStatusPageMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse._getStatusCode()).toBe(400); + expect(fakeResponse._getData()).toHaveProperty('message'); + }); + + it('should create status page and return 200', async () => { + fakeRequest.body = { + settings: {}, + layout: [{ type: 'uptime', monitors: ['m1'] }], + }; + + await createStatusPageMiddleware(fakeRequest, fakeResponse); + + expect(createStatusPage).toHaveBeenCalled(); + expect(statusCache.addNewStatusPage).toHaveBeenCalled(); + expect(fakeResponse._getStatusCode()).toBe(200); + expect(fakeResponse._getData()).toHaveProperty('message'); + expect(fakeResponse._getData()).toHaveProperty('data'); + }); + + it('should call handleError if error thrown', async () => { + createStatusPage.mockImplementationOnce(() => { + throw new Error('fail'); + }); + + fakeRequest.body = { + settings: {}, + layout: [{ type: 'uptime', monitors: ['m1'] }], + }; + + await createStatusPageMiddleware(fakeRequest, fakeResponse); + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/status/defaultPage.test.js b/test/server/middleware/status/defaultPage.test.js new file mode 100644 index 00000000..c43b9909 --- /dev/null +++ b/test/server/middleware/status/defaultPage.test.js @@ -0,0 +1,89 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { getUserByEmail } from '../../../../server/database/queries/user.js'; +import { userSessionExists } from '../../../../server/database/queries/session.js'; +import { fetchStatusPageUsingUrl } from '../../../../server/database/queries/status.js'; +import defaultPageMiddleware from '../../../../server/middleware/status/defaultPage.js'; + +vi.mock('../../../../shared/utils/cookies.js'); +vi.mock('../../../../server/database/queries/session.js'); +vi.mock('../../../../server/database/queries/status.js'); +vi.mock('../../../../server/database/queries/user.js'); + +describe('defaultPageMiddleware', () => { + let fakeRequest, fakeResponse, fakeNext; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + fakeNext = vi.fn(); + + userSessionExists = vi.fn(function () { + return true; + }); + fetchStatusPageUsingUrl = vi.fn(function () { + return Promise.resolve({ + settings: JSON.stringify({ isPublic: true }), + }); + }); + getUserByEmail = vi.fn(function () { + return true; + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should redirect to /home if no statusPage', async () => { + fetchStatusPageUsingUrl.mockResolvedValueOnce(null); + fakeResponse.redirect = vi.fn(); + + await defaultPageMiddleware(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith('/home'); + }); + + it('should call next if public', async () => { + await defaultPageMiddleware(fakeRequest, fakeResponse, fakeNext); + + expect(fakeNext).toHaveBeenCalled(); + }); + + it('should redirect to /home if not public and no session', async () => { + fetchStatusPageUsingUrl.mockResolvedValueOnce({ + settings: JSON.stringify({ isPublic: false }), + }); + + userSessionExists.mockResolvedValueOnce(false); + fakeResponse.redirect = vi.fn(); + + await defaultPageMiddleware(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith('/home'); + }); + + it('should redirect to /home if user not in db', async () => { + fetchStatusPageUsingUrl.mockResolvedValueOnce({ + settings: JSON.stringify({ isPublic: false }), + }); + + userSessionExists.mockResolvedValueOnce({ email: 'test' }); + getUserByEmail.mockResolvedValueOnce(false); + fakeResponse.redirect = vi.fn(); + + await defaultPageMiddleware(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith('/home'); + }); + + it('should redirect to /home on error', async () => { + fetchStatusPageUsingUrl.mockImplementationOnce(() => { + throw new Error('fail'); + }); + + fakeResponse.redirect = vi.fn(); + + await defaultPageMiddleware(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith('/home'); + }); +}); diff --git a/test/server/middleware/status/delete.test.js b/test/server/middleware/status/delete.test.js new file mode 100644 index 00000000..92adde50 --- /dev/null +++ b/test/server/middleware/status/delete.test.js @@ -0,0 +1,51 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import statusCache from '../../../../server/cache/status.js'; +import { handleError } from '../../../../server/utils/errors.js'; +import { deleteStatusPage } from '../../../../server/database/queries/status.js'; +import deleteStatusPageMiddleware from '../../../../server/middleware/status/delete.js'; + +vi.mock('../../../../server/cache/status.js'); +vi.mock('../../../../server/database/queries/status.js'); +vi.mock('../../../../server/utils/errors.js'); + +describe('deleteStatusPageMiddleware', () => { + let fakeRequest, fakeResponse; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + deleteStatusPage = vi.fn(function () { + return Promise.resolve(); + }); + statusCache.deleteStatusPage = vi.fn(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if no statusPageId', async () => { + fakeRequest.body = {}; + await deleteStatusPageMiddleware(fakeRequest, fakeResponse); + expect(fakeResponse._getStatusCode()).toBe(400); + expect(fakeResponse._getData()).toHaveProperty('message'); + }); + + it('should delete status page and return 200', async () => { + fakeRequest.body = { statusPageId: 'id' }; + await deleteStatusPageMiddleware(fakeRequest, fakeResponse); + expect(deleteStatusPage).toHaveBeenCalledWith('id'); + expect(statusCache.deleteStatusPage).toHaveBeenCalledWith('id'); + expect(fakeResponse._getStatusCode()).toBe(200); + expect(fakeResponse._getData()).toHaveProperty('message'); + }); + + it('should call handleError if error thrown', async () => { + deleteStatusPage.mockImplementationOnce(() => { + throw new Error('fail'); + }); + fakeRequest.body = { statusPageId: 'id' }; + await deleteStatusPageMiddleware(fakeRequest, fakeResponse); + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/status/edit.test.js b/test/server/middleware/status/edit.test.js new file mode 100644 index 00000000..da98abb0 --- /dev/null +++ b/test/server/middleware/status/edit.test.js @@ -0,0 +1,88 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import statusCache from '../../../../server/cache/status.js'; +import { handleError } from '../../../../server/utils/errors.js'; +import editStatusPageMiddleware from '../../../../server/middleware/status/edit.js'; +import { updateStatusPage } from '../../../../server/database/queries/status.js'; + +vi.mock('../../../../server/utils/errors.js'); +vi.mock('../../../../server/database/queries/status.js'); +vi.mock('../../../../shared/validators/status/layout.js'); +vi.mock('../../../../shared/validators/status/settings.js'); +vi.mock('../../../../server/cache/status.js'); + +describe('editStatusPageMiddleware', () => { + let fakeRequest, fakeResponse; + + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + fakeResponse.locals = { user: { email: 'test' } }; + + updateStatusPage = vi.fn(function () { + return Promise.resolve({ id: 'id' }); + }); + statusCache.updateStatusPage = vi.fn(function () { + return Promise.resolve(); + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if no statusId', async () => { + fakeRequest.body = { + settings: {}, + layout: [{ type: 'uptime', monitors: ['m1'] }], + }; + + await editStatusPageMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse._getStatusCode()).toBe(400); + expect(fakeResponse._getData()).toHaveProperty('message'); + }); + + it('should return 400 if no monitors', async () => { + fakeRequest.body = { + statusId: 'id', + settings: {}, + layout: [{ type: 'uptime', monitors: [] }], + }; + + await editStatusPageMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse._getStatusCode()).toBe(400); + expect(fakeResponse._getData()).toHaveProperty('message'); + }); + + it('should update status page and return 200', async () => { + fakeRequest.body = { + statusId: 'id', + settings: {}, + layout: [{ type: 'uptime', monitors: ['m1'] }], + }; + + await editStatusPageMiddleware(fakeRequest, fakeResponse); + + expect(updateStatusPage).toHaveBeenCalled(); + expect(statusCache.updateStatusPage).toHaveBeenCalled(); + expect(fakeResponse._getStatusCode()).toBe(200); + expect(fakeResponse._getData()).toHaveProperty('message'); + expect(fakeResponse._getData()).toHaveProperty('data'); + }); + + it('should call handleError if error thrown', async () => { + updateStatusPage.mockImplementationOnce(() => { + throw new Error('fail'); + }); + fakeRequest.body = { + statusId: 'id', + settings: {}, + layout: [{ type: 'uptime', monitors: ['m1'] }], + }; + + await editStatusPageMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/status/getAll.test.js b/test/server/middleware/status/getAll.test.js new file mode 100644 index 00000000..958059d9 --- /dev/null +++ b/test/server/middleware/status/getAll.test.js @@ -0,0 +1,42 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { handleError } from '../../../../server/utils/errors.js'; +import { fetchAllStatusPages } from '../../../../server/database/queries/status.js'; +import getAllStatusPagesMiddleware from '../../../../server/middleware/status/getAll.js'; + +vi.mock('../../../../server/database/queries/status.js'); +vi.mock('../../../../server/utils/errors.js'); + +describe('getAllStatusPagesMiddleware', () => { + let fakeRequest, fakeResponse; + + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fetchAllStatusPages = vi.fn(function () { + return Promise.resolve([{ id: 'id' }]); + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should fetch all status pages and return them', async () => { + await getAllStatusPagesMiddleware(fakeRequest, fakeResponse); + + expect(fetchAllStatusPages).toHaveBeenCalled(); + expect(fakeResponse._getJSONData()).toEqual([{ id: 'id' }]); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should call handleError if fetchAllStatusPages throws', async () => { + fetchAllStatusPages.mockImplementationOnce(() => { + throw new Error('fail'); + }); + + await getAllStatusPagesMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/status/getUsingId.test.js b/test/server/middleware/status/getUsingId.test.js new file mode 100644 index 00000000..f5de6073 --- /dev/null +++ b/test/server/middleware/status/getUsingId.test.js @@ -0,0 +1,61 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { handleError } from '../../../../server/utils/errors.js'; +import getUsingIdMiddleware from '../../../../server/middleware/status/getUsingId.js'; +import { fetchStatusPageUsingUrl } from '../../../../server/database/queries/status.js'; + +vi.mock('../../../../server/database/queries/status.js'); +vi.mock('../../../../server/utils/errors.js'); + +describe('getUsingIdMiddleware', () => { + let fakeRequest, fakeResponse; + + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + fetchStatusPageUsingUrl = vi.fn(function (id) { + return id === 'exists' ? { id: 'exists' } : null; + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 404 if no statusPageId', async () => { + fakeRequest.query = {}; + + await getUsingIdMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse._getStatusCode()).toBe(404); + }); + + it('should return 404 if not found', async () => { + fakeRequest.query = { statusPageId: 'notfound' }; + + await getUsingIdMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse._getStatusCode()).toBe(404); + }); + + it('should return 200 and cleaned status page if found', async () => { + fakeRequest.query = { statusPageId: 'exists' }; + + await getUsingIdMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse._getStatusCode()).toBe(200); + expect(fakeResponse._getJSONData()).toMatchObject({ id: 'exists' }); + }); + + it('should call handleError if error thrown', async () => { + fetchStatusPageUsingUrl.mockImplementationOnce(() => { + throw new Error('fail'); + }); + + fakeRequest.query = { statusPageId: 'exists' }; + + await getUsingIdMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/status/statusPageUsingId.test.js b/test/server/middleware/status/statusPageUsingId.test.js new file mode 100644 index 00000000..5e1e7f77 --- /dev/null +++ b/test/server/middleware/status/statusPageUsingId.test.js @@ -0,0 +1,95 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { getUserByEmail } from '../../../../server/database/queries/user.js'; +import { userSessionExists } from '../../../../server/database/queries/session.js'; +import { fetchStatusPageUsingUrl } from '../../../../server/database/queries/status.js'; +import getStatusPageUsingIdMiddleware from '../../../../server/middleware/status/statusPageUsingId.js'; + +vi.mock('../../../../server/database/queries/session.js'); +vi.mock('../../../../server/database/queries/status.js'); +vi.mock('../../../../server/database/queries/user.js'); + +describe('getStatusPageUsingIdMiddleware', () => { + let fakeRequest, fakeResponse, fakeNext; + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + fakeNext = vi.fn(); + + fakeResponse.redirect = vi.fn(); + getUserByEmail = vi.fn(function () { + return true; + }); + + fetchStatusPageUsingUrl = vi.fn(function (id) { + return id === 'exists' ? { settings: { isPublic: true } } : null; + }); + + userSessionExists = vi.fn(function () { + return true; + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should redirect to / if id is default', async () => { + fakeRequest.params = { id: 'default' }; + + await getStatusPageUsingIdMiddleware(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith('/'); + }); + + it('should redirect to /404 if not found', async () => { + fakeRequest.params = { id: 'notfound' }; + + await getStatusPageUsingIdMiddleware(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith('/404'); + }); + + it('should call next if public', async () => { + fakeRequest.params = { id: 'exists' }; + + await getStatusPageUsingIdMiddleware(fakeRequest, fakeResponse, fakeNext); + + expect(fakeNext).toHaveBeenCalled(); + }); + + it('should redirect to /home if not public and no session', async () => { + fetchStatusPageUsingUrl.mockResolvedValueOnce({ + settings: { isPublic: false }, + }); + userSessionExists.mockResolvedValueOnce(false); + fakeRequest.params = { id: 'exists' }; + + await getStatusPageUsingIdMiddleware(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith('/home'); + }); + + it('should redirect to /home if user not in db', async () => { + fetchStatusPageUsingUrl.mockResolvedValueOnce({ + settings: { isPublic: false }, + }); + userSessionExists.mockResolvedValueOnce({ email: 'test' }); + getUserByEmail.mockResolvedValueOnce(false); + fakeRequest.params = { id: 'exists' }; + + await getStatusPageUsingIdMiddleware(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith('/home'); + }); + + it('should redirect to /404 on error', async () => { + fetchStatusPageUsingUrl.mockImplementationOnce(() => { + throw new Error('fail'); + }); + fakeRequest.params = { id: 'exists' }; + + await getStatusPageUsingIdMiddleware(fakeRequest, fakeResponse, fakeNext); + + expect(fakeResponse.redirect).toHaveBeenCalledWith('/404'); + }); +}); diff --git a/test/server/middleware/tokens/create.test.js b/test/server/middleware/tokens/create.test.js new file mode 100644 index 00000000..58b14e6b --- /dev/null +++ b/test/server/middleware/tokens/create.test.js @@ -0,0 +1,63 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { handleError } from '../../../../server/utils/errors.js'; +import TokenValidator from '../../../../shared/validators/token.js'; +import { apiTokenCreate } from '../../../../server/database/queries/tokens.js'; +import createApiTokenMiddleware from '../../../../server/middleware/tokens/create.js'; + +vi.mock('../../../../shared/validators/token.js'); +vi.mock('../../../../server/database/queries/tokens.js'); +vi.mock('../../../../server/utils/errors.js', () => ({ handleError: vi.fn() })); + +describe('createApiTokenMiddleware', () => { + let fakeRequest, fakeResponse; + + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + fakeResponse.locals = { user: { email: 'test@example.com' } }; + + apiTokenCreate = vi.fn(() => Promise.resolve({ id: 'id' })); + TokenValidator = vi.fn(() => false); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if validation fails', async () => { + TokenValidator.mockReturnValueOnce('invalid'); + fakeRequest.body = { name: 'n', permission: 'p' }; + + await createApiTokenMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse._getStatusCode()).toBe(400); + expect(fakeResponse._getData()).toEqual( + expect.objectContaining({ message: 'invalid' }) + ); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should create token and return 200', async () => { + fakeRequest.body = { name: 'n', permission: 'p' }; + + await createApiTokenMiddleware(fakeRequest, fakeResponse); + + expect(apiTokenCreate).toHaveBeenCalledWith('test@example.com', 'p', 'n'); + expect(fakeResponse._getStatusCode()).toBe(200); + expect(fakeResponse._getData()).toEqual( + expect.objectContaining({ id: 'id' }) + ); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should call handleError if error thrown', async () => { + apiTokenCreate.mockImplementationOnce(() => { + throw new Error('fail'); + }); + fakeRequest.body = { name: 'n', permission: 'p' }; + + await createApiTokenMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/tokens/delete.test.js b/test/server/middleware/tokens/delete.test.js new file mode 100644 index 00000000..f04a35d9 --- /dev/null +++ b/test/server/middleware/tokens/delete.test.js @@ -0,0 +1,52 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { handleError } from '../../../../server/utils/errors.js'; +import { apiTokenDelete } from '../../../../server/database/queries/tokens.js'; +import deleteApiTokenMiddleware from '../../../../server/middleware/tokens/delete.js'; + +vi.mock('../../../../server/database/queries/tokens.js'); +vi.mock('../../../../server/utils/errors.js'); + +describe('deleteApiTokenMiddleware', () => { + let fakeRequest, fakeResponse; + + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + apiTokenDelete = vi.fn(() => Promise.resolve()); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if no token', async () => { + fakeRequest.body = {}; + await deleteApiTokenMiddleware(fakeRequest, fakeResponse); + expect(fakeResponse._getStatusCode()).toBe(400); + expect(fakeResponse._getData()).toEqual( + expect.objectContaining({ message: 'Token is required' }) + ); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should delete token and return 200', async () => { + fakeRequest.body = { token: 't' }; + await deleteApiTokenMiddleware(fakeRequest, fakeResponse); + expect(apiTokenDelete).toHaveBeenCalledWith('t'); + expect(fakeResponse._getStatusCode()).toBe(200); + expect(fakeResponse._getData()).toEqual( + expect.objectContaining({ message: 'Token deleted successfully' }) + ); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should call handleError if error thrown', async () => { + apiTokenDelete.mockImplementationOnce(() => { + throw new Error('fail'); + }); + fakeRequest.body = { token: 't' }; + await deleteApiTokenMiddleware(fakeRequest, fakeResponse); + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/tokens/getAll.test.js b/test/server/middleware/tokens/getAll.test.js new file mode 100644 index 00000000..5cd0b5c2 --- /dev/null +++ b/test/server/middleware/tokens/getAll.test.js @@ -0,0 +1,40 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { handleError } from '../../../../server/utils/errors.js'; +import { getAllApiTokens } from '../../../../server/database/queries/tokens.js'; +import getAllApiTokensMiddleware from '../../../../server/middleware/tokens/getAll.js'; + +vi.mock('../../../../server/database/queries/tokens.js'); +vi.mock('../../../../server/utils/errors.js'); + +describe('getAllApiTokensMiddleware', () => { + let fakeRequest, fakeResponse; + + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + getAllApiTokens = vi.fn(() => Promise.resolve([{ id: 'id' }])); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should fetch all tokens and return them', async () => { + await getAllApiTokensMiddleware(fakeRequest, fakeResponse); + + expect(getAllApiTokens).toHaveBeenCalled(); + expect(fakeResponse._getJSONData()).toEqual({ tokens: [{ id: 'id' }] }); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should call handleError if getAllApiTokens throws', async () => { + getAllApiTokens.mockImplementationOnce(() => { + throw new Error('fail'); + }); + + await getAllApiTokensMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/tokens/update.test.js b/test/server/middleware/tokens/update.test.js new file mode 100644 index 00000000..8e5efcd6 --- /dev/null +++ b/test/server/middleware/tokens/update.test.js @@ -0,0 +1,62 @@ +import { createRequest, createResponse } from 'node-mocks-http'; +import { handleError } from '../../../../server/utils/errors.js'; +import TokenValidator from '../../../../shared/validators/token.js'; +import { apiTokenUpdate } from '../../../../server/database/queries/tokens.js'; +import updateApiTokenMiddleware from '../../../../server/middleware/tokens/update.js'; + +vi.mock('../../../../shared/validators/token.js'); +vi.mock('../../../../server/database/queries/tokens.js'); +vi.mock('../../../../server/utils/errors.js'); + +describe('updateApiTokenMiddleware', () => { + let fakeRequest, fakeResponse; + + beforeEach(() => { + fakeRequest = createRequest(); + fakeResponse = createResponse(); + + apiTokenUpdate = vi.fn(() => Promise.resolve({ id: 'id' })); + TokenValidator = vi.fn(() => false); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return 400 if validation fails', async () => { + TokenValidator.mockReturnValueOnce('invalid'); + fakeRequest.body = { token: 't', name: 'n', permission: 'p' }; + + await updateApiTokenMiddleware(fakeRequest, fakeResponse); + + expect(fakeResponse._getStatusCode()).toBe(400); + expect(fakeResponse._getData()).toEqual( + expect.objectContaining({ message: 'invalid' }) + ); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should update token and return 200', async () => { + fakeRequest.body = { token: 't', name: 'n', permission: 'p' }; + + await updateApiTokenMiddleware(fakeRequest, fakeResponse); + + expect(apiTokenUpdate).toHaveBeenCalledWith('t', 'n', 'p'); + expect(fakeResponse._getStatusCode()).toBe(200); + expect(fakeResponse._getData()).toEqual( + expect.objectContaining({ id: 'id' }) + ); + expect(handleError).not.toHaveBeenCalled(); + }); + + it('should call handleError if error thrown', async () => { + apiTokenUpdate.mockImplementationOnce(() => { + throw new Error('fail'); + }); + fakeRequest.body = { token: 't', name: 'n', permission: 'p' }; + + await updateApiTokenMiddleware(fakeRequest, fakeResponse); + + expect(handleError).toHaveBeenCalledWith(expect.any(Error), fakeResponse); + }); +}); diff --git a/test/server/middleware/user/update/avatar.test.js b/test/server/middleware/user/update/avatar.test.js index b478a0f5..f60f3720 100644 --- a/test/server/middleware/user/update/avatar.test.js +++ b/test/server/middleware/user/update/avatar.test.js @@ -4,7 +4,6 @@ import userUpdateAvatar from '../../../../../server/middleware/user/update/avata import validators from '../../../../../shared/validators'; vi.mock('../../../../../server/database/queries/user'); -vi.mock('../../../../../shared/validators'); describe('userUpdateAvatar - Middleware', () => { const user = { @@ -54,7 +53,7 @@ describe('userUpdateAvatar - Middleware', () => { }); it('should return 400 when avatar is invalid', async () => { - validators.user.isAvatar = vi.fn().mockReturnValue(true); + fakeRequest.body.avatar = '!@#%#$^&$%&'; await userUpdateAvatar(fakeRequest, fakeResponse); diff --git a/test/server/middleware/user/update/username.test.js b/test/server/middleware/user/update/username.test.js index 474cd3a1..e9e87017 100644 --- a/test/server/middleware/user/update/username.test.js +++ b/test/server/middleware/user/update/username.test.js @@ -1,10 +1,8 @@ import { createRequest, createResponse } from 'node-mocks-http'; import { updateUserDisplayname } from '../../../../../server/database/queries/user'; import userUpdateUsername from '../../../../../server/middleware/user/update/username'; -import validators from '../../../../../shared/validators'; vi.mock('../../../../../server/database/queries/user'); -vi.mock('../../../../../shared/validators'); describe('userUpdateUsername - Middleware', () => { const user = { @@ -29,7 +27,7 @@ describe('userUpdateUsername - Middleware', () => { }); afterEach(() => { - vi.restoreAllMocks(); + vi.clearAllMocks(); }); it('should return 200 when username is missing', async () => { @@ -53,7 +51,7 @@ describe('userUpdateUsername - Middleware', () => { }); it('should return 400 when username is invalid', async () => { - validators.auth.username = vi.fn().mockReturnValue(true); + fakeRequest.body.displayName = '*^%$#!@#'; await userUpdateUsername(fakeRequest, fakeResponse);