diff --git a/.prettierrc b/.prettierrc index f7de837..7317e6e 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,7 @@ { "useTabs": true, - "trailingComma": "none", - "printWidth": 100, + "trailingComma": "all", + "printWidth": 90, "plugins": ["prettier-plugin-svelte"], "overrides": [ { diff --git a/package-lock.json b/package-lock.json index ac2709f..750ebe3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@types/bcrypt": "^5.0.2", "@types/jsonwebtoken": "^9.0.7", "@types/node": "^22.10.7", + "@vitest/coverage-v8": "^2.1.8", "eslint": "^9.7.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.36.0", @@ -49,6 +50,63 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", + "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", + "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -665,6 +723,88 @@ "url": "https://github.com/sponsors/nzakas" } }, + "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/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/@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", @@ -785,6 +925,17 @@ "node": ">= 8" } }, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.28", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", @@ -1416,6 +1567,39 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@vitest/coverage-v8": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.8.tgz", + "integrity": "sha512-2Y7BPlKH18mAZYAW1tYByudlCYrQyl5RGvnnDYJKW5tCiO5qg3KSAy3XAxcxKz900a0ZXxWtKrMuZLe3lKBpJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.7", + "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.12", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.8", + "vitest": "2.1.8" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz", @@ -1981,6 +2165,13 @@ "dev": true, "license": "MIT" }, + "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/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -2450,6 +2641,36 @@ "dev": true, "license": "ISC" }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "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/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -2600,6 +2821,13 @@ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "license": "ISC" }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -2737,6 +2965,92 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2944,6 +3258,13 @@ "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/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -2954,6 +3275,18 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -3331,6 +3664,13 @@ "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", @@ -3373,6 +3713,23 @@ "node": ">=8" } }, + "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/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -3889,6 +4246,22 @@ "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/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -3901,6 +4274,20 @@ "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-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -4070,6 +4457,78 @@ "node": ">=10" } }, + "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.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/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/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/test-exclude/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/tiny-glob": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", @@ -4488,6 +4947,110 @@ "node": ">=0.10.0" } }, + "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/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/wrap-ansi/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/wrap-ansi/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/wrap-ansi/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/wrap-ansi/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/wrap-ansi/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/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 11f3334..52e9624 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@types/bcrypt": "^5.0.2", "@types/jsonwebtoken": "^9.0.7", "@types/node": "^22.10.7", + "@vitest/coverage-v8": "^2.1.8", "eslint": "^9.7.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.36.0", diff --git a/src/app.d.ts b/src/app.d.ts index 330a6f2..8985880 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,6 +1,6 @@ // See https://svelte.dev/docs/kit/types#app.d.ts -import type { LocalCredentials } from "$lib/server/requestTools"; +import type { LocalCredentials } from "$lib/server/getRequestBody"; // for information about these interfaces declare global { diff --git a/src/demo.spec.ts b/src/demo.spec.ts deleted file mode 100644 index e07cbbd..0000000 --- a/src/demo.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -describe('sum test', () => { - it('adds 1 + 2 to equal 3', () => { - expect(1 + 2).toBe(3); - }); -}); diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 6b5c931..0d2f6c9 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,15 +1,29 @@ -import { isAuthorized } from "$lib/server/requestTools"; -import { unauthorizedResponse } from "$lib/server/responseBodies"; -import type { Handle } from "@sveltejs/kit"; +import * as auth from "$lib/server/auth"; +import { forbiddenResponse, unauthorizedResponse } from "$lib/server/responseBodies"; +import { routeAuth, type Method } from "$lib/server/routeAuth"; +import { type Handle } from "@sveltejs/kit"; export const handle: Handle = async ({ event, resolve }) => { - const auth = await isAuthorized(event); + const creds = await auth.authenticate(event); - if (!auth) { + if (!creds) { return unauthorizedResponse(); } - event.locals.user = auth; + const authResult = auth.isAuthorized( + routeAuth, + event.request.method as Method, + event.url.pathname, + creds, + ); + + if (authResult === auth.AuthorizationResult.Denied) { + return forbiddenResponse(); + } else if (authResult === auth.AuthorizationResult.Unauthenticated) { + return unauthorizedResponse(); + } + + event.locals.user = creds; return await resolve(event); }; diff --git a/src/lib/GameData.ts b/src/lib/GameData.ts index 084a8a6..255f84e 100644 --- a/src/lib/GameData.ts +++ b/src/lib/GameData.ts @@ -1,5 +1,5 @@ import { hasOnlyKeys, hasProperty } from "./validation"; -import type { Id } from "./Id"; +import { isId, type Id } from "./Id"; import type { State } from "./State"; export interface GameData { @@ -13,7 +13,15 @@ export function isGameData(target: unknown): target is GameData { return false; } - if (!hasProperty(target, "players", "string[]")) { + if ("players" in (target as any)) { + const { players } = target as any; + + for (const player of players) { + if (!isId(player)) { + return false; + } + } + } else { return false; } diff --git a/src/lib/GameEvent.ts b/src/lib/GameEvent.ts index 8f5fea4..d3d5227 100644 --- a/src/lib/GameEvent.ts +++ b/src/lib/GameEvent.ts @@ -174,9 +174,7 @@ export class RollForFirst implements GameEvent { state.dieCount = 6; } else { // ...otherwise, setup for tie breaking rolls. - state.scores = scores.map((_, i) => - ties.has(i) ? FIRST_ROLL_PENDING : FIRST_ROLL_LOST, - ); + state.scores = scores.map((_, i) => (ties.has(i) ? FIRST_ROLL_PENDING : FIRST_ROLL_LOST)); } } } @@ -368,9 +366,7 @@ export class Score implements GameEvent { // Increment the index of the active player, circling back to 1 if the player // who just scored was the last player in the array. state.playing = - playerCount - 1 === this.player - ? (state.playing = 0) - : (state.playing = this.player + 1); + playerCount - 1 === this.player ? (state.playing = 0) : (state.playing = this.player + 1); state.dieCount = 6; delete state.heldScore; @@ -387,7 +383,10 @@ export class Score implements GameEvent { } function scorePips(count: number, pips: number) { - if (coa + if (count < 3) { + // If not a three of a kind, return the raw dice value... + return pipScore(pips) * count; + } // ...otherwise, this is a three or more of a kind. if (pips === 1) { diff --git a/src/lib/Id.ts b/src/lib/Id.ts index 3209e1f..b70ae66 100644 --- a/src/lib/Id.ts +++ b/src/lib/Id.ts @@ -6,6 +6,14 @@ export function createId(): Id { return new ObjectId(); } +export function idFromString(str: string) { + return new ObjectId(str); +} + +export function stringFromId(id: Id) { + return id.toString(); +} + export function isId(target: unknown): target is Id { return target instanceof ObjectId; } diff --git a/src/lib/server/requestTools.ts b/src/lib/server/auth.ts similarity index 65% rename from src/lib/server/requestTools.ts rename to src/lib/server/auth.ts index 45e6356..cd2ceb6 100644 --- a/src/lib/server/requestTools.ts +++ b/src/lib/server/auth.ts @@ -1,31 +1,35 @@ import { JWT_SECRET } from "$env/static/private"; import type { RequestEvent } from "@sveltejs/kit"; import jwt from "jsonwebtoken"; -import { routeAuth, type Method, type RouteAuthRule } from "./routeAuth"; +import { type Method, type RouteAuthRule } from "./routeAuth"; +import type { Listing } from "$lib/Listing"; +import type { LoginData } from "$lib/Login"; -export type LocalCredentials = +export type LocalCredentials = ( | { kind: "Basic"; payload: { username: string; password: string } } | { kind: "Bearer"; payload: jwt.JwtPayload | string } - | { kind: "None" }; + | { kind: "None" } +) & { role: string }; -export async function getRequestBody( - req: Request, - validation?: (target: unknown) => target is T -): Promise { - if (req.body === null) { - throw new Error("no body is present on the request"); - } - - const body = await req.json(); - - if (validation && !validation(body)) { - throw new Error("body validation failed"); - } - - return body; +export enum AuthorizationResult { + Allowed, + Denied, + Unauthenticated, } -export async function isAuthorized(event: RequestEvent): Promise { +export async function createToken(listing: Listing) { + return await jwt.sign( + { sub: listing.id, username: listing.data.username, role: listing.data.role }, + JWT_SECRET, + { + expiresIn: "1d", + }, + ); +} + +export async function authenticate( + event: RequestEvent, +): Promise { let path = event.url.pathname; let tokenKind: "Basic" | "Bearer" | "None"; let tokenRole: string; @@ -35,7 +39,7 @@ export async function isAuthorized(event: RequestEvent): Promise( + req: Request, + validation?: (target: unknown) => target is T, +): Promise { + if (req.body === null) { + throw new Error("no body is present on the request"); + } + + const body = await req.json(); + + if (validation && !validation(body)) { + throw new Error("body validation failed"); + } + + return body; +} diff --git a/src/lib/server/routeAuth.ts b/src/lib/server/routeAuth.ts index 6c28fa0..6d9130b 100644 --- a/src/lib/server/routeAuth.ts +++ b/src/lib/server/routeAuth.ts @@ -13,13 +13,13 @@ export const routeAuth: { [k: string]: RouteAuthRule[] } = { // a Basic token. Other than that, they cannot do anything! default: [ { action: "allow", methods: ["POST"], endpoint: "/api/users", tokenKind: "None" }, - { action: "allow", methods: ["POST"], endpoint: "/api/token", tokenKind: "Basic" } + { action: "allow", methods: ["POST"], endpoint: "/api/token", tokenKind: "Basic" }, ], // player is anyone else. They are authorized to hit any endpoint, using any method, // with a Bearer token. player: [ { action: "allow", methods: ["*"], endpoint: "*" }, - { action: "deny", methods: ["POST"], endpoint: "/api/token" } - ] + { action: "deny", methods: ["POST"], endpoint: "/api/token" }, + ], }; diff --git a/src/lib/server/test/Game.test.ts b/src/lib/server/test/Game.spec.ts similarity index 62% rename from src/lib/server/test/Game.test.ts rename to src/lib/server/test/Game.spec.ts index 5a23424..91fc5a9 100644 --- a/src/lib/server/test/Game.test.ts +++ b/src/lib/server/test/Game.spec.ts @@ -1,15 +1,19 @@ -import { describe, it } from "node:test"; -import { Game } from "../../Game"; +import { describe, it } from "vitest"; +import { Game } from "$lib/server/Game"; import { deepEqual, ok, throws } from "node:assert/strict"; +import { createId, idFromString, stringFromId } from "$lib/Id"; +import { equal } from "node:assert"; describe("Game", () => { + const idString = stringFromId(createId()); describe("addPlayer", () => { it("should push a player id into the player array", () => { const game = new Game(); deepEqual(game.players, []); - game.addPlayer("some-id"); - deepEqual(game.players, ["some-id"]); + game.addPlayer(idFromString(idString)); + equal(game.players.length, 1); + equal(stringFromId(game.players[0]), idString); }); }); diff --git a/src/lib/server/test/GameData.test.ts b/src/lib/server/test/GameData.spec.ts similarity index 66% rename from src/lib/server/test/GameData.test.ts rename to src/lib/server/test/GameData.spec.ts index 69d66eb..45bc7cf 100644 --- a/src/lib/server/test/GameData.test.ts +++ b/src/lib/server/test/GameData.spec.ts @@ -1,26 +1,29 @@ -import { describe, it } from "node:test"; -import { GameData, isGameData } from "../../GameData"; +import { describe, it } from "vitest"; +import { type GameData, isGameData } from "$lib/GameData"; import { equal, ok } from "node:assert/strict"; +import { createId, idFromString, stringFromId } from "$lib/Id"; describe("GameData", () => { + const idString = stringFromId(createId()); + describe("isGameData", () => { it("rejects a malformed object", () => { let data: unknown = { - players: ["id", 3], + players: [idFromString(idString), idString], isStarted: false, state: {}, }; equal(isGameData(data), false); data = { - players: ["id"], + players: [idFromString(idString)], isStarted: null, state: {}, }; equal(isGameData(data), false); data = { - players: ["id"], + players: [idFromString(idString)], isStarted: false, }; equal(isGameData(data), false); @@ -28,7 +31,7 @@ describe("GameData", () => { it("rejects an object with extra properties", () => { const data: GameData & { extra: boolean } = { - players: ["id"], + players: [idFromString(idString)], isStarted: false, state: {}, extra: true, @@ -39,7 +42,7 @@ describe("GameData", () => { it("should accept a proper GameData object", () => { const data: GameData = { - players: ["id"], + players: [idFromString(idString)], state: {}, isStarted: false, }; diff --git a/src/lib/server/test/GameEvent.test.ts b/src/lib/server/test/GameEvent.spec.ts similarity index 93% rename from src/lib/server/test/GameEvent.test.ts rename to src/lib/server/test/GameEvent.spec.ts index f783c25..ca5ac72 100644 --- a/src/lib/server/test/GameEvent.test.ts +++ b/src/lib/server/test/GameEvent.spec.ts @@ -12,19 +12,14 @@ import { } from "../../GameEvent"; import type { GameEventData } from "../../GameEvent"; import type { GameData } from "../../GameData"; -import { describe, it } from "node:test"; +import { describe, it } from "vitest"; import type { State } from "../../State"; import { doesNotThrow, deepStrictEqual, equal, ok, throws } from "assert"; +import { createId, idFromString, stringFromId } from "$lib/Id"; describe("Game Events", () => { describe("isGameEventData", () => { it("should return false if the target is not an object", () => { - // const target = { - // kind: GameEventKind.Hold, - // player: 0, - // value: [1, 4], - // }; - equal(isGameEventData("target"), false); }); @@ -60,10 +55,13 @@ describe("Game Events", () => { }); describe("getGameEvent", () => { + const idString = stringFromId(createId()); + const anotherIdString = stringFromId(createId()); + it("should throw if the kind is unkown", () => { const data: GameData = { isStarted: false, - players: ["42", "1,"], + players: [idFromString(idString), idFromString(anotherIdString)], state: {}, }; @@ -79,7 +77,7 @@ describe("Game Events", () => { it("should throw when SeatPlayers has the wrong number of players", () => { const data: GameData = { isStarted: true, - players: ["42", "1,"], + players: [idFromString(idString), idFromString(anotherIdString)], state: {}, }; @@ -94,7 +92,7 @@ describe("Game Events", () => { it("should return a SeatPlayers object when the number of players is correct", () => { const data: GameData = { isStarted: true, - players: ["42", "1,"], + players: [idFromString(idString), idFromString(anotherIdString)], state: {}, }; @@ -109,7 +107,7 @@ describe("Game Events", () => { it("should throw an error if the player passes a full roll with Roll", () => { const data: GameData = { isStarted: true, - players: ["42", "1,"], + players: [idFromString(idString), idFromString(anotherIdString)], state: {}, }; @@ -125,7 +123,7 @@ describe("Game Events", () => { it("should return a Roll object with dice values when the player passes a die count as a value", () => { const data: GameData = { isStarted: true, - players: ["42", "1,"], + players: [idFromString(idString), idFromString(anotherIdString)], state: {}, }; @@ -145,7 +143,7 @@ describe("Game Events", () => { it("should return the class that corresponds with a given kind", () => { const data: GameData = { isStarted: true, - players: ["42", "1,"], + players: [idFromString(idString), idFromString(anotherIdString)], state: {}, }; @@ -222,10 +220,7 @@ describe("Game Events", () => { }); it("should throw if the value is not a number", () => { - throws( - () => - new RollForFirst({ kind: GameEventKind.RollForFirst, player: 0, value: [4] }), - ); + throws(() => new RollForFirst({ kind: GameEventKind.RollForFirst, player: 0, value: [4] })); }); }); @@ -267,7 +262,6 @@ describe("Game Events", () => { const state: State = { scores: [FIRST_ROLL_PENDING, FIRST_ROLL_PENDING] }; throws(() => ev.run(state)); - console.log("done"); }); it("should throw if the player has already rolled", () => { @@ -296,12 +290,7 @@ describe("Game Events", () => { it("should reset the scores and set the winning player when everyone has rolled", () => { const state: State = { - scores: [ - FIRST_ROLL_PENDING, - FIRST_ROLL_PENDING, - FIRST_ROLL_PENDING, - FIRST_ROLL_PENDING, - ], + scores: [FIRST_ROLL_PENDING, FIRST_ROLL_PENDING, FIRST_ROLL_PENDING, FIRST_ROLL_PENDING], }; let ev = new RollForFirst({ @@ -324,24 +313,14 @@ describe("Game Events", () => { deepStrictEqual(state, { dieCount: 6, - scores: [ - FIRST_ROLL_PENDING, - FIRST_ROLL_PENDING, - FIRST_ROLL_PENDING, - FIRST_ROLL_PENDING, - ], + scores: [FIRST_ROLL_PENDING, FIRST_ROLL_PENDING, FIRST_ROLL_PENDING, FIRST_ROLL_PENDING], playing: 2, }); }); it("should reset tied players for tie breaker", () => { const state: State = { - scores: [ - FIRST_ROLL_PENDING, - FIRST_ROLL_PENDING, - FIRST_ROLL_PENDING, - FIRST_ROLL_PENDING, - ], + scores: [FIRST_ROLL_PENDING, FIRST_ROLL_PENDING, FIRST_ROLL_PENDING, FIRST_ROLL_PENDING], }; let ev = new RollForFirst({ @@ -361,23 +340,13 @@ describe("Game Events", () => { ev.run(state); deepStrictEqual(state, { - scores: [ - FIRST_ROLL_PENDING, - FIRST_ROLL_LOST, - FIRST_ROLL_LOST, - FIRST_ROLL_PENDING, - ], + scores: [FIRST_ROLL_PENDING, FIRST_ROLL_LOST, FIRST_ROLL_LOST, FIRST_ROLL_PENDING], }); }); it("should throw if a player whose lost tries to roll again", () => { const state: State = { - scores: [ - FIRST_ROLL_PENDING, - FIRST_ROLL_LOST, - FIRST_ROLL_LOST, - FIRST_ROLL_PENDING, - ], + scores: [FIRST_ROLL_PENDING, FIRST_ROLL_LOST, FIRST_ROLL_LOST, FIRST_ROLL_PENDING], }; const ev = new RollForFirst({ @@ -390,12 +359,7 @@ describe("Game Events", () => { it("should allow tied players to keep rolling until somoene wins", () => { const state: State = { - scores: [ - FIRST_ROLL_PENDING, - FIRST_ROLL_PENDING, - FIRST_ROLL_LOST, - FIRST_ROLL_PENDING, - ], + scores: [FIRST_ROLL_PENDING, FIRST_ROLL_PENDING, FIRST_ROLL_LOST, FIRST_ROLL_PENDING], }; // simulate another 3-way tie @@ -415,12 +379,7 @@ describe("Game Events", () => { deepStrictEqual( state, { - scores: [ - FIRST_ROLL_PENDING, - FIRST_ROLL_PENDING, - FIRST_ROLL_LOST, - FIRST_ROLL_PENDING, - ], + scores: [FIRST_ROLL_PENDING, FIRST_ROLL_PENDING, FIRST_ROLL_LOST, FIRST_ROLL_PENDING], }, "shouldn't change in a 3-way tie", ); @@ -438,12 +397,7 @@ describe("Game Events", () => { deepStrictEqual( state, { - scores: [ - FIRST_ROLL_PENDING, - FIRST_ROLL_LOST, - FIRST_ROLL_LOST, - FIRST_ROLL_PENDING, - ], + scores: [FIRST_ROLL_PENDING, FIRST_ROLL_LOST, FIRST_ROLL_LOST, FIRST_ROLL_PENDING], }, "should update for a smaller tie", ); diff --git a/src/lib/server/test/Listing.test.ts b/src/lib/server/test/Listing.spec.ts similarity index 77% rename from src/lib/server/test/Listing.test.ts rename to src/lib/server/test/Listing.spec.ts index 6079d50..a574e6a 100644 --- a/src/lib/server/test/Listing.test.ts +++ b/src/lib/server/test/Listing.spec.ts @@ -1,7 +1,8 @@ -import { describe, it } from "node:test"; +import { describe, it } from "vitest"; import { createNewListing, updateListing } from "../modifyListing"; -import { Game } from "../../Game"; +import { Game } from "$lib/server/Game"; import { deepEqual, equal, ok } from "node:assert/strict"; +import { isId } from "$lib/Id"; describe("Listing", () => { describe("createNewListing", () => { @@ -10,10 +11,10 @@ describe("Listing", () => { const listing = createNewListing(game); ok(listing.data instanceof Game); - ok(listing.createdAt instanceof Date); ok(listing.modifiedAt === null); ok(!listing.deleted); - equal(typeof listing.id, "string"); + ok(isId(listing.id)); + equal(typeof listing.createdAt, "string"); }); }); @@ -27,7 +28,7 @@ describe("Listing", () => { const updatedListing = updateListing(listing, update); deepEqual(updatedListing.data, update); - ok(updatedListing.modifiedAt instanceof Date); + equal(typeof updatedListing.modifiedAt, "string"); }); }); }); diff --git a/src/lib/server/test/getDiceRoll.test.ts b/src/lib/server/test/getDiceRoll.spec.ts similarity index 68% rename from src/lib/server/test/getDiceRoll.test.ts rename to src/lib/server/test/getDiceRoll.spec.ts index f63bbf4..26570f9 100644 --- a/src/lib/server/test/getDiceRoll.test.ts +++ b/src/lib/server/test/getDiceRoll.spec.ts @@ -1,5 +1,5 @@ -import { describe, it } from "node:test"; -import { getDiceRoll } from "../../getDiceRoll"; +import { describe, it } from "vitest"; +import { getDiceRoll } from "$lib/server/getDiceRoll"; import { deepEqual } from "node:assert/strict"; function testRandom() { @@ -11,5 +11,8 @@ describe("getDiceRoll", () => { it("should return an array of numbers from 1 to 6 with a given length", () => { let rand = getDiceRoll(6, testRandom()); deepEqual(rand, [0, 1, 3, 3, 4, 6]); + + rand = getDiceRoll(3, testRandom()); + deepEqual(rand, [0, 1, 3]); }); }); diff --git a/src/lib/server/test/validation.test.ts b/src/lib/server/test/validation.spec.ts similarity index 91% rename from src/lib/server/test/validation.test.ts rename to src/lib/server/test/validation.spec.ts index d54e7ae..5b149f9 100644 --- a/src/lib/server/test/validation.test.ts +++ b/src/lib/server/test/validation.spec.ts @@ -1,4 +1,4 @@ -import { describe, it } from "node:test"; +import { describe, it } from "vitest"; import { equal, ok } from "node:assert/strict"; import { hasProperty, hasOnlyKeys } from "../../validation"; @@ -46,7 +46,7 @@ describe("validation", () => { third: false, fourth: null, fifth: { something: "important" }, - sixth: ["one", "two"], + sixth: ["one", "two"] }; ok(hasProperty(target, "first", "string")); @@ -59,7 +59,7 @@ describe("validation", () => { it("should return false if passed an array type and the property isn't an array", () => { const target = { - arr: "not array", + arr: "not array" }; equal(hasProperty(target, "arr", "string[]"), false); @@ -67,7 +67,7 @@ describe("validation", () => { it("should return false if the defined array contains a non-matching element", () => { const target = { - arr: ["I", "was", "born", "in", 1989], + arr: ["I", "was", "born", "in", 1989] }; equal(hasProperty(target, "arr", "string[]"), false); @@ -75,7 +75,7 @@ describe("validation", () => { it("should return true if all the elements in a defined array match", () => { const target = { - arr: ["I", "was", "born", "in", "1989"], + arr: ["I", "was", "born", "in", "1989"] }; ok(hasProperty(target, "arr", "string[]")); @@ -83,7 +83,7 @@ describe("validation", () => { it("should return true if all the elements in a defined array match one of multiple types", () => { const target = { - arr: ["I", "was", "born", "in", 1989], + arr: ["I", "was", "born", "in", 1989] }; ok(hasProperty(target, "arr", "(string|number)[]")); @@ -91,7 +91,7 @@ describe("validation", () => { it("should return true if type is null but property is nullable", () => { const target = { - nullable: null, + nullable: null }; ok(hasProperty(target, "nullable", "string", true)); @@ -107,7 +107,7 @@ describe("validation", () => { const target = { one: "one", two: "two", - three: "three", + three: "three" }; const keys = ["one", "two"]; @@ -119,7 +119,7 @@ describe("validation", () => { const target = { one: "one", two: "two", - three: "three", + three: "three" }; const keys = ["one", "two", "three"]; @@ -129,7 +129,7 @@ describe("validation", () => { it("should return true if the target has only a subset of the provided keys", () => { const target = { - one: "one", + one: "one" }; const keys = ["one", "two", "three"]; diff --git a/src/routes/api/token/+server.ts b/src/routes/api/token/+server.ts index 0eb3f1f..9a2c8ce 100644 --- a/src/routes/api/token/+server.ts +++ b/src/routes/api/token/+server.ts @@ -1,17 +1,15 @@ import { isListing } from "$lib/Listing"; import { isLoginData } from "$lib/Login"; import { readListingByQuery } from "$lib/server/mongo"; -import { getRequestBody } from "$lib/server/requestTools"; import { badRequestResponse, notFoundResponse, singleResponse, - unauthorizedResponse + unauthorizedResponse, } from "$lib/server/responseBodies"; import type { RequestHandler } from "@sveltejs/kit"; -import { JWT_SECRET } from "$env/static/private"; import { compare } from "bcrypt"; -import jwt from "jsonwebtoken"; +import { createToken } from "$lib/server/auth"; export const POST: RequestHandler = async ({ locals }): Promise => { try { @@ -25,9 +23,9 @@ export const POST: RequestHandler = async ({ locals }): Promise => { const listing = await readListingByQuery( "logins", { - "data.username": username + "data.username": username, }, - (target) => isListing(target, isLoginData) + (target) => isListing(target, isLoginData), ); if (!listing) { @@ -35,14 +33,7 @@ export const POST: RequestHandler = async ({ locals }): Promise => { } if (await compare(password, listing.data.password)) { - const token = await jwt.sign( - { sub: listing.id, username, role: listing.data.role }, - JWT_SECRET, - { - expiresIn: "1d" - } - ); - + const token = await createToken(listing); return singleResponse(token); } diff --git a/src/routes/api/users/+server.ts b/src/routes/api/users/+server.ts index 96f4e20..f5a9ef2 100644 --- a/src/routes/api/users/+server.ts +++ b/src/routes/api/users/+server.ts @@ -5,18 +5,16 @@ import { badRequestResponse, forbiddenResponse, serverErrorResponse, - singleResponse + singleResponse, } from "$lib/server/responseBodies"; import type { RequestHandler } from "@sveltejs/kit"; export const POST: RequestHandler = async ({ request }): Promise => { let body: unknown; - console.log("here"); try { body = await request.json(); } catch (err) { - console.log(err); return badRequestResponse("body is required"); } diff --git a/src/tests/hooks.server.spec.ts b/src/tests/hooks.server.spec.ts new file mode 100644 index 0000000..93157f2 --- /dev/null +++ b/src/tests/hooks.server.spec.ts @@ -0,0 +1,83 @@ +import type { Cookies, RequestEvent } from "@sveltejs/kit"; +import { describe, it, expect, afterEach } from "vitest"; +import * as auth from "../lib/server/auth"; +import { handle } from "../hooks.server"; +import { createId } from "$lib/Id"; + +let events: RequestEvent[] = []; + +// Mock RequestEvent data that can be passed into the handle function. It doesn't matter +// that most of these fields are incoherent, the only things that will be tested here are +// that this event is passed to the isAuthorized function, and that the locas are +// populated. +const event: RequestEvent = { + cookies: {} as Cookies, + fetch: async (): Promise => { + return new Response(); + }, + getClientAddress: () => "", + locals: { user: {} }, + params: {}, + platform: undefined, + request: new Request(new URL("https://localhost/api")), + route: { id: "" }, + setHeaders: () => {}, + url: new URL("https://localhost/api"), + isDataRequest: false, + isSubRequest: false, +}; + +const resolve = async (event: RequestEvent): Promise => { + events.push(event); + return new Response(); +}; + +describe("handle", () => { + afterEach(() => { + event.locals.user = {}; + event.request.headers.delete("authorization"); + events = []; + }); + + it("returns unauthorized response if caller isn't properly authenticated", async () => { + event.request.headers.set("authorization", "Nonesense Token"); + const res = await handle({ event, resolve }); + expect(res.status).to.equal(401); + }); + + it("returns unauthorized response if caller is missing required auth header", async () => { + const res = await handle({ event, resolve }); + expect(res.status).to.equal(401); + }); + + it("returns forbidden response if caller isn't authorized", async () => { + // This is a weird scenario, but it does reflect the way I expect this to work. + // Svelte Kit does not seem to provide me with a tool that I can use to authorize + // users in one place. I would have to check their role at the start of each + // endpoint function (yuck). The endpoint below doesn't exist, but it will still + // return 403 instead of 404 because the auth check happens before the route + // matching, and the user isn't authorized to hit this nonesense endpoint. + const ev: RequestEvent = { + ...event, + url: new URL("https://localhost/api/some/secret/route"), + request: new Request("https://localhost/api/some/secret/route"), + }; + + const token = await auth.createToken({ + id: createId(), + createdAt: new Date().toString(), + modifiedAt: new Date().toString(), + deleted: false, + data: { + password: "somethin' secret!", + username: "Mr. Man", + role: "default", + }, + }); + + ev.request.headers.set("authorization", `Bearer ${token}`); + + const res = await handle({ event: ev, resolve }); + expect(res.status).to.equal(403); + }); +}); diff --git a/tests/requests.http b/src/tests/requests.http similarity index 99% rename from tests/requests.http rename to src/tests/requests.http index 28f019a..3afe69f 100644 --- a/tests/requests.http +++ b/src/tests/requests.http @@ -33,7 +33,7 @@ Content-Type: application/json } ### - + POST https://localhost:5173/api/games/de4cdb8c-0346-4ac6-a7a8-b4135b2d79e3/turns Accept: application/json Content-Type: application/json diff --git a/vite.config.ts b/vite.config.ts index 284484a..92570e2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from "vitest/config"; +import { configDefaults, coverageConfigDefaults, defineConfig } from "vitest/config"; import { sveltekit } from "@sveltejs/kit/vite"; import { readFileSync } from "fs"; @@ -8,12 +8,15 @@ export default defineConfig({ server: { https: { key: readFileSync(`${__dirname}/cert/key.pem`), - cert: readFileSync(`${__dirname}/cert/cert.pem`) + cert: readFileSync(`${__dirname}/cert/cert.pem`), }, - proxy: {} + proxy: {}, }, test: { - include: ["src/**/*.{test,spec}.{js,ts}"] - } + include: ["src/**/*.{test,spec}.{js,ts}"], + coverage: { + exclude: [...coverageConfigDefaults.exclude, "svelte.config.js"], + }, + }, });