diff --git a/.idea/.idea.DotNetAngular/.idea/workspace.xml b/.idea/.idea.DotNetAngular/.idea/workspace.xml
index 146affb..e82d605 100644
--- a/.idea/.idea.DotNetAngular/.idea/workspace.xml
+++ b/.idea/.idea.DotNetAngular/.idea/workspace.xml
@@ -19,10 +19,33 @@
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -33,13 +56,13 @@
-
+
@@ -65,33 +88,33 @@
- {
- "keyToString": {
- ".NET Launch Settings Profile.API: Angular_dev.executor": "Run",
- "RunOnceActivity.MCP Project settings loaded": "true",
- "RunOnceActivity.ShowReadmeOnStart": "true",
- "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
- "RunOnceActivity.cidr.known.project.marker": "true",
- "RunOnceActivity.git.unshallow": "true",
- "RunOnceActivity.readMode.enableVisualFormatting": "true",
- "RunOnceActivity.typescript.service.memoryLimit.init": "true",
- "cidr.known.project.marker": "true",
- "codeWithMe.voiceChat.enabledByDefault": "false",
- "com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
- "git-widget-placeholder": "develop",
- "git.auto.fetch.suggestion.counter": "1",
- "junie.onboarding.icon.badge.shown": "true",
- "node.js.detected.package.eslint": "true",
- "node.js.detected.package.tslint": "true",
- "node.js.selected.package.eslint": "(autodetect)",
- "node.js.selected.package.tslint": "(autodetect)",
- "nodejs_package_manager_path": "npm",
- "settings.editor.selected.configurable": "vcs.Git",
- "to.speed.mode.migration.done": "true",
- "ts.external.directory.path": "/home/natlinux/RiderProjects/DotNetAngular/src/ClientApp/node_modules/typescript/lib",
- "vue.rearranger.settings.migration": "true"
+
+}]]>
@@ -236,6 +259,7 @@
+
diff --git a/src/ClientApp/package-lock.json b/src/ClientApp/package-lock.json
index ae83c88..acc8ba3 100644
--- a/src/ClientApp/package-lock.json
+++ b/src/ClientApp/package-lock.json
@@ -34,8 +34,10 @@
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
+ "karma-firefox-launcher": "^2.1.3",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
+ "puppeteer": "^24.43.0",
"typescript": "~5.9.3"
}
},
@@ -2912,6 +2914,150 @@
"url": "https://opencollective.com/popperjs"
}
},
+ "node_modules/@puppeteer/browsers": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.1.tgz",
+ "integrity": "sha512-zmS4RTK9fbrc++WlAJhxYbfz3IjDeOmkK/CwwbLmk7ydfS9e2CiEeRJHEPvjDVElO/bwXbidwGA37Bsm6LzCnQ==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.4.3",
+ "extract-zip": "^2.0.1",
+ "progress": "^2.0.3",
+ "proxy-agent": "^6.5.0",
+ "semver": "^7.7.4",
+ "tar-fs": "^3.1.1",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "browsers": "lib/cjs/main-cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/@puppeteer/browsers/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/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,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/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,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/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,
+ "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/@puppeteer/browsers/node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.4.tgz",
@@ -3552,6 +3698,12 @@
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="
},
+ "node_modules/@tootallnate/quickjs-emscripten": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
+ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
+ "dev": true
+ },
"node_modules/@tufjs/canonical-json": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
@@ -3696,6 +3848,16 @@
"@types/node": "*"
}
},
+ "node_modules/@types/yauzl": {
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@vitejs/plugin-basic-ssl": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz",
@@ -3865,12 +4027,135 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "node_modules/ast-types": {
+ "version": "0.13.4",
+ "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
+ "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/b4a": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
+ "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==",
+ "dev": true,
+ "peerDependencies": {
+ "react-native-b4a": "*"
+ },
+ "peerDependenciesMeta": {
+ "react-native-b4a": {
+ "optional": true
+ }
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
+ "node_modules/bare-events": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
+ "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
+ "dev": true,
+ "peerDependencies": {
+ "bare-abort-controller": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-abort-controller": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-fs": {
+ "version": "4.7.1",
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz",
+ "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==",
+ "dev": true,
+ "dependencies": {
+ "bare-events": "^2.5.4",
+ "bare-path": "^3.0.0",
+ "bare-stream": "^2.6.4",
+ "bare-url": "^2.2.2",
+ "fast-fifo": "^1.3.2"
+ },
+ "engines": {
+ "bare": ">=1.16.0"
+ },
+ "peerDependencies": {
+ "bare-buffer": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-buffer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-os": {
+ "version": "3.9.1",
+ "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz",
+ "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==",
+ "dev": true,
+ "engines": {
+ "bare": ">=1.14.0"
+ }
+ },
+ "node_modules/bare-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
+ "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
+ "dev": true,
+ "dependencies": {
+ "bare-os": "^3.0.1"
+ }
+ },
+ "node_modules/bare-stream": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz",
+ "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==",
+ "dev": true,
+ "dependencies": {
+ "streamx": "^2.25.0",
+ "teex": "^1.0.1"
+ },
+ "peerDependencies": {
+ "bare-abort-controller": "*",
+ "bare-buffer": "*",
+ "bare-events": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-abort-controller": {
+ "optional": true
+ },
+ "bare-buffer": {
+ "optional": true
+ },
+ "bare-events": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-url": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.3.tgz",
+ "integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==",
+ "dev": true,
+ "dependencies": {
+ "bare-path": "^3.0.0"
+ }
+ },
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
@@ -3891,6 +4176,15 @@
"node": ">=6.0.0"
}
},
+ "node_modules/basic-ftp": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz",
+ "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/beasties": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.1.tgz",
@@ -4040,6 +4334,15 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -4167,6 +4470,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/caniuse-lite": {
"version": "1.0.30001791",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz",
@@ -4227,6 +4539,28 @@
"node": ">=18"
}
},
+ "node_modules/chromium-bidi": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz",
+ "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==",
+ "dev": true,
+ "dependencies": {
+ "mitt": "^3.0.1",
+ "zod": "^3.24.1"
+ },
+ "peerDependencies": {
+ "devtools-protocol": "*"
+ }
+ },
+ "node_modules/chromium-bidi/node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/cli-cursor": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
@@ -4494,6 +4828,32 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/cosmiconfig": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz",
+ "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==",
+ "dev": true,
+ "dependencies": {
+ "env-paths": "^2.2.1",
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/d-fischer"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.9.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -4542,6 +4902,15 @@
"integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==",
"dev": true
},
+ "node_modules/data-uri-to-buffer": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
+ "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/date-format": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz",
@@ -4567,6 +4936,20 @@
}
}
},
+ "node_modules/degenerator": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
+ "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
+ "dev": true,
+ "dependencies": {
+ "ast-types": "^0.13.4",
+ "escodegen": "^2.1.0",
+ "esprima": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -4596,6 +4979,12 @@
"node": ">=8"
}
},
+ "node_modules/devtools-protocol": {
+ "version": "0.0.1608973",
+ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz",
+ "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==",
+ "dev": true
+ },
"node_modules/di": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
@@ -4708,6 +5097,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
"node_modules/engine.io": {
"version": "6.6.7",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.7.tgz",
@@ -4835,6 +5233,15 @@
"integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
"dev": true
},
+ "node_modules/error-ex": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+ "dev": true,
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -4920,6 +5327,68 @@
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"dev": true
},
+ "node_modules/escodegen": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
+ "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
+ "dev": true,
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/escodegen/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -4935,6 +5404,15 @@
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"dev": true
},
+ "node_modules/events-universal": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
+ "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
+ "dev": true,
+ "dependencies": {
+ "bare-events": "^2.7.0"
+ }
+ },
"node_modules/eventsource": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
@@ -5029,12 +5507,38 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"dev": true
},
+ "node_modules/extract-zip": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
+ "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.1.1",
+ "get-stream": "^5.1.0",
+ "yauzl": "^2.10.0"
+ },
+ "bin": {
+ "extract-zip": "cli.js"
+ },
+ "engines": {
+ "node": ">= 10.17.0"
+ },
+ "optionalDependencies": {
+ "@types/yauzl": "^2.9.1"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
+ "node_modules/fast-fifo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
+ "dev": true
+ },
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
@@ -5051,6 +5555,15 @@
}
]
},
+ "node_modules/fd-slicer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+ "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+ "dev": true,
+ "dependencies": {
+ "pend": "~1.2.0"
+ }
+ },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -5263,6 +5776,35 @@
"node": ">= 0.4"
}
},
+ "node_modules/get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "dev": true,
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-uri": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
+ "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
+ "dev": true,
+ "dependencies": {
+ "basic-ftp": "^5.0.2",
+ "data-uri-to-buffer": "^6.0.2",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -5571,6 +6113,22 @@
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
"dev": true
},
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -5615,6 +6173,12 @@
"node": ">= 0.10"
}
},
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true
+ },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -5627,6 +6191,21 @@
"node": ">=8"
}
},
+ "node_modules/is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "dev": true,
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -5720,6 +6299,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dev": true,
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/isbinaryfile": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz",
@@ -5833,6 +6424,18 @@
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -6001,6 +6604,31 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/karma-firefox-launcher": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.3.tgz",
+ "integrity": "sha512-LMM2bseebLbYjODBOVt7TCPP9OI2vZIXCavIXhkO9m+10Uj5l7u/SKoeRmYx8FYHTVGZSpk6peX+3BMHC1WwNw==",
+ "dev": true,
+ "dependencies": {
+ "is-wsl": "^2.2.0",
+ "which": "^3.0.0"
+ }
+ },
+ "node_modules/karma-firefox-launcher/node_modules/which": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz",
+ "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
"node_modules/karma-jasmine": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz",
@@ -6319,6 +6947,12 @@
"node": ">=10"
}
},
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true
+ },
"node_modules/listr2": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz",
@@ -6788,6 +7422,12 @@
"node": ">= 18"
}
},
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+ "dev": true
+ },
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
@@ -6882,6 +7522,15 @@
"node": ">= 0.6"
}
},
+ "node_modules/netmask": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz",
+ "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
"node_modules/node-addon-api": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
@@ -7195,6 +7844,38 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/pac-proxy-agent": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
+ "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
+ "dev": true,
+ "dependencies": {
+ "@tootallnate/quickjs-emscripten": "^0.23.0",
+ "agent-base": "^7.1.2",
+ "debug": "^4.3.4",
+ "get-uri": "^6.0.1",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.6",
+ "pac-resolver": "^7.0.1",
+ "socks-proxy-agent": "^8.0.5"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/pac-resolver": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
+ "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
+ "dev": true,
+ "dependencies": {
+ "degenerator": "^5.0.0",
+ "netmask": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/pacote": {
"version": "21.3.1",
"resolved": "https://registry.npmjs.org/pacote/-/pacote-21.3.1.tgz",
@@ -7226,6 +7907,42 @@
"node": "^20.17.0 || >=22.9.0"
}
},
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse-json/node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true
+ },
"node_modules/parse5": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
@@ -7350,6 +8067,12 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+ "dev": true
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -7456,6 +8179,15 @@
"node": "^20.17.0 || >=22.9.0"
}
},
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/promise-retry": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
@@ -7482,12 +8214,116 @@
"node": ">= 0.10"
}
},
+ "node_modules/proxy-agent": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
+ "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
+ "dev": true,
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "^4.3.4",
+ "http-proxy-agent": "^7.0.1",
+ "https-proxy-agent": "^7.0.6",
+ "lru-cache": "^7.14.1",
+ "pac-proxy-agent": "^7.1.0",
+ "proxy-from-env": "^1.1.0",
+ "socks-proxy-agent": "^8.0.5"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/proxy-agent/node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true
+ },
+ "node_modules/pump": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
+ "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
+ "dev": true,
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
"node_modules/punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
"dev": true
},
+ "node_modules/puppeteer": {
+ "version": "24.43.0",
+ "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.0.tgz",
+ "integrity": "sha512-DRnMFz+J3s4lFUQcjqKl0/7h0jzlCZuUFU9lNjtKrnMl5WI1RwCaIItpHVu9empuPyUreYueN0sUW3/pnfdqsg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "dependencies": {
+ "@puppeteer/browsers": "2.13.1",
+ "chromium-bidi": "14.0.0",
+ "cosmiconfig": "^9.0.0",
+ "devtools-protocol": "0.0.1608973",
+ "puppeteer-core": "24.43.0",
+ "typed-query-selector": "^2.12.2"
+ },
+ "bin": {
+ "puppeteer": "lib/cjs/puppeteer/node/cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/puppeteer-core": {
+ "version": "24.43.0",
+ "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.0.tgz",
+ "integrity": "sha512-cCRNXsUlhyPoKDz6+TiSpfZpRS3mD6Y1YFKhkdr6ik6TMfuJb7fAtXq9ThUFc4sphxObDk3BuAvdxc1Y6YOnqQ==",
+ "dev": true,
+ "dependencies": {
+ "@puppeteer/browsers": "2.13.1",
+ "chromium-bidi": "14.0.0",
+ "debug": "^4.4.3",
+ "devtools-protocol": "0.0.1608973",
+ "typed-query-selector": "^2.12.2",
+ "webdriver-bidi-protocol": "0.4.1",
+ "ws": "^8.20.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/puppeteer-core/node_modules/ws": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
+ "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/qjobs": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz",
@@ -7577,6 +8413,15 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"dev": true
},
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/restore-cursor": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
@@ -8231,6 +9076,17 @@
"node": ">=8.0"
}
},
+ "node_modules/streamx": {
+ "version": "2.25.0",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
+ "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
+ "dev": true,
+ "dependencies": {
+ "events-universal": "^1.0.0",
+ "fast-fifo": "^1.3.2",
+ "text-decoder": "^1.1.0"
+ }
+ },
"node_modules/string-width": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz",
@@ -8289,6 +9145,32 @@
"node": ">=18"
}
},
+ "node_modules/tar-fs": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz",
+ "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==",
+ "dev": true,
+ "dependencies": {
+ "pump": "^3.0.0",
+ "tar-stream": "^3.1.5"
+ },
+ "optionalDependencies": {
+ "bare-fs": "^4.0.1",
+ "bare-path": "^3.0.0"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz",
+ "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==",
+ "dev": true,
+ "dependencies": {
+ "b4a": "^1.6.4",
+ "bare-fs": "^4.5.5",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
+ }
+ },
"node_modules/tar/node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
@@ -8298,6 +9180,24 @@
"node": ">=18"
}
},
+ "node_modules/teex": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
+ "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
+ "dev": true,
+ "dependencies": {
+ "streamx": "^2.12.5"
+ }
+ },
+ "node_modules/text-decoder": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
+ "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
+ "dev": true,
+ "dependencies": {
+ "b4a": "^1.6.4"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -8376,6 +9276,12 @@
"node": ">= 0.6"
}
},
+ "node_modules/typed-query-selector": {
+ "version": "2.12.2",
+ "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz",
+ "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==",
+ "dev": true
+ },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -8607,6 +9513,12 @@
"dev": true,
"optional": true
},
+ "node_modules/webdriver-bidi-protocol": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz",
+ "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==",
+ "dev": true
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -8781,6 +9693,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/yauzl": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+ "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+ "dev": true,
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
+ },
"node_modules/yoctocolors": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
diff --git a/src/ClientApp/package.json b/src/ClientApp/package.json
index 199741b..d217390 100644
--- a/src/ClientApp/package.json
+++ b/src/ClientApp/package.json
@@ -38,8 +38,10 @@
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
+ "karma-firefox-launcher": "^2.1.3",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
+ "puppeteer": "^24.43.0",
"typescript": "~5.9.3"
}
}
diff --git a/src/ClientApp/src/app/app.component.spec.ts b/src/ClientApp/src/app/app.component.spec.ts
index f105511..7e18ab3 100644
--- a/src/ClientApp/src/app/app.component.spec.ts
+++ b/src/ClientApp/src/app/app.component.spec.ts
@@ -1,10 +1,23 @@
-import { TestBed } from '@angular/core/testing';
-import { AppComponent } from './app.component';
+import {TestBed} from '@angular/core/testing';
+import {AppComponent} from './app.component';
+import {AuthService} from './infrastructure/services/auth-service';
+import {ActivatedRoute, provideRouter} from '@angular/router';
describe('AppComponent', () => {
beforeEach(async () => {
+ localStorage.clear();
+ const authSpy = jasmine.createSpyObj('AuthService', [
+ 'getUsernameFromToken', 'getRoleFromToken', 'getEmailFromToken', 'getUserIdFromToken',
+ 'isLoggedIn', 'getToken', 'signOut', 'decodedToken'
+ ]);
+
await TestBed.configureTestingModule({
imports: [AppComponent],
+ providers: [
+ provideRouter([]),
+ {provide: AuthService, useValue: authSpy},
+ {provide: ActivatedRoute, useValue: {snapshot: {data: {}}, firstChild: null}},
+ ]
}).compileComponents();
});
@@ -19,11 +32,4 @@ describe('AppComponent', () => {
const app = fixture.componentInstance;
expect(app.title).toEqual('ClientApp');
});
-
- it('should render title', () => {
- const fixture = TestBed.createComponent(AppComponent);
- fixture.detectChanges();
- const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.querySelector('h1')?.textContent).toContain('Hello, ClientApp');
- });
});
diff --git a/src/ClientApp/src/app/infrastructure/services/auth-service.spec.ts b/src/ClientApp/src/app/infrastructure/services/auth-service.spec.ts
index 4d7c3f9..8c30554 100644
--- a/src/ClientApp/src/app/infrastructure/services/auth-service.spec.ts
+++ b/src/ClientApp/src/app/infrastructure/services/auth-service.spec.ts
@@ -1,22 +1,45 @@
+import {TestBed} from '@angular/core/testing';
+import {provideHttpClient} from '@angular/common/http';
import {AuthService} from './auth-service';
-import {TestBed} from "@angular/core/testing";
-import {provideHttpClient} from "@angular/common/http";
-import {ActivatedRoute} from "@angular/router";
-import {of} from "rxjs";
describe('AuthService', () => {
let service: AuthService;
beforeEach(() => {
+ localStorage.clear();
TestBed.configureTestingModule({
- providers: [provideHttpClient(),
- { provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } }
- ]
+ providers: [provideHttpClient()]
});
service = TestBed.inject(AuthService);
- })
+ });
- it('should create an instance', () => {
+ it('should be created', () => {
expect(service).toBeTruthy();
});
+
+ it('should store and retrieve token', () => {
+ service.storeToken('test-token');
+ expect(service.getToken()).toBe('test-token');
+ });
+
+ it('should store and retrieve refresh token', () => {
+ service.storeRefreshToken('test-refresh');
+ expect(service.getRefreshToken()).toBe('test-refresh');
+ });
+
+ it('should be logged in after storing token', () => {
+ service.storeToken('some-token');
+ expect(service.isLoggedIn()).toBeTrue();
+ });
+
+ it('should not be logged in without token', () => {
+ expect(service.isLoggedIn()).toBeFalse();
+ });
+
+ it('should clear tokens on signOut', () => {
+ service.storeToken('test-token');
+ service.storeRefreshToken('test-refresh');
+ service.signOut();
+ expect(service.isLoggedIn()).toBeFalse();
+ });
});
diff --git a/src/ClientApp/src/app/infrastructure/services/dark-mode.service.spec.ts b/src/ClientApp/src/app/infrastructure/services/dark-mode.service.spec.ts
index 905dfa6..83572cc 100644
--- a/src/ClientApp/src/app/infrastructure/services/dark-mode.service.spec.ts
+++ b/src/ClientApp/src/app/infrastructure/services/dark-mode.service.spec.ts
@@ -1,5 +1,4 @@
import {TestBed} from '@angular/core/testing';
-
import {DarkModeService} from './dark-mode.service';
describe('DarkModeService', () => {
@@ -13,4 +12,16 @@ describe('DarkModeService', () => {
it('should be created', () => {
expect(service).toBeTruthy();
});
+
+ it('should toggle from null to dark', () => {
+ service.darkModeSignal.set(null);
+ service.updateDarkMode();
+ expect(service.darkModeSignal()).toBe('dark');
+ });
+
+ it('should toggle from dark to null', () => {
+ service.darkModeSignal.set('dark');
+ service.updateDarkMode();
+ expect(service.darkModeSignal()).toBeNull();
+ });
});
diff --git a/src/ClientApp/src/app/infrastructure/services/loading.service.spec.ts b/src/ClientApp/src/app/infrastructure/services/loading.service.spec.ts
index dd3193c..a08211c 100644
--- a/src/ClientApp/src/app/infrastructure/services/loading.service.spec.ts
+++ b/src/ClientApp/src/app/infrastructure/services/loading.service.spec.ts
@@ -1,6 +1,5 @@
-import { TestBed } from '@angular/core/testing';
-
-import { LoadingService } from './loading.service';
+import {TestBed} from '@angular/core/testing';
+import {LoadingService} from './loading.service';
describe('LoadingService', () => {
let service: LoadingService;
@@ -13,4 +12,26 @@ describe('LoadingService', () => {
it('should be created', () => {
expect(service).toBeTruthy();
});
+
+ it('should start with loading false', () => {
+ expect(service.loading()).toBeFalse();
+ });
+
+ it('should set loading true on show()', () => {
+ service.show();
+ expect(service.loading()).toBeTrue();
+ });
+
+ it('should set loading false on hide()', () => {
+ service.show();
+ service.hide();
+ expect(service.loading()).toBeFalse();
+ });
+
+ it('should track per-item loading state', () => {
+ service.showItem('item-1');
+ expect(service.itemLoading()['item-1']).toBeTrue();
+ service.hideItem('item-1');
+ expect(service.itemLoading()['item-1']).toBeFalse();
+ });
});
diff --git a/src/ClientApp/src/app/infrastructure/services/reset-password.service.spec.ts b/src/ClientApp/src/app/infrastructure/services/reset-password.service.spec.ts
index ab4e9da..13a5080 100644
--- a/src/ClientApp/src/app/infrastructure/services/reset-password.service.spec.ts
+++ b/src/ClientApp/src/app/infrastructure/services/reset-password.service.spec.ts
@@ -1,23 +1,47 @@
-import { TestBed } from '@angular/core/testing';
-
-import { ResetPasswordService } from './reset-password.service';
-import {provideHttpClient} from "@angular/common/http";
-import {ActivatedRoute} from "@angular/router";
-import {of} from "rxjs";
+import {TestBed} from '@angular/core/testing';
+import {provideHttpClient} from '@angular/common/http';
+import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';
+import {ResetPasswordService} from './reset-password.service';
+import {environment} from '../../../environments/environment.development';
describe('ResetPasswordService', () => {
let service: ResetPasswordService;
+ let httpTesting: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
- providers: [provideHttpClient(),
- { provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } }
+ providers: [
+ provideHttpClient(),
+ provideHttpClientTesting(),
]
});
service = TestBed.inject(ResetPasswordService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
});
it('should be created', () => {
expect(service).toBeTruthy();
});
+
+ it('should POST to send-reset-email on sendResetPasswordLink', () => {
+ service.sendResetPasswordLink('test@example.com').subscribe();
+
+ const req = httpTesting.expectOne(`${environment.baseUrl}auth/send-reset-email/test@example.com`);
+ expect(req.request.method).toBe('POST');
+ req.flush({value: 'Reset email sent successfully'});
+ });
+
+ it('should POST to reset-password on resetPassword', () => {
+ const resetObj = {email: 'test@example.com', emailToken: 'token', newPassword: 'NewPass1!', confirmPassword: 'NewPass1!'};
+ service.resetPassword(resetObj).subscribe();
+
+ const req = httpTesting.expectOne(`${environment.baseUrl}auth/reset-password`);
+ expect(req.request.method).toBe('POST');
+ expect(req.request.body).toEqual(resetObj);
+ req.flush({value: 'Password reset successfully'});
+ });
});
diff --git a/src/ClientApp/src/app/infrastructure/services/toast.service.spec.ts b/src/ClientApp/src/app/infrastructure/services/toast.service.spec.ts
index 577c8aa..4426078 100644
--- a/src/ClientApp/src/app/infrastructure/services/toast.service.spec.ts
+++ b/src/ClientApp/src/app/infrastructure/services/toast.service.spec.ts
@@ -1,5 +1,4 @@
import {TestBed} from '@angular/core/testing';
-
import {ToastService} from './toast.service';
describe('ToastService', () => {
@@ -13,4 +12,29 @@ describe('ToastService', () => {
it('should be created', () => {
expect(service).toBeTruthy();
});
+
+ it('should start with empty toasts', () => {
+ expect(service.toasts.length).toBe(0);
+ });
+
+ it('should add a toast', () => {
+ service.show('Test message');
+ expect(service.toasts.length).toBe(1);
+ expect(service.toasts[0].message).toBe('Test message');
+ });
+
+ it('should add a toast with options', () => {
+ service.show('Warning', {classname: 'bg-warning', delay: 3000});
+ expect(service.toasts[0].classname).toBe('bg-warning');
+ expect(service.toasts[0].delay).toBe(3000);
+ });
+
+ it('should remove a toast', () => {
+ service.show('Message 1');
+ service.show('Message 2');
+ const toastToRemove = service.toasts[0];
+ service.remove(toastToRemove);
+ expect(service.toasts.length).toBe(1);
+ expect(service.toasts[0].message).toBe('Message 2');
+ });
});
diff --git a/src/ClientApp/src/app/infrastructure/services/user-store.service.spec.ts b/src/ClientApp/src/app/infrastructure/services/user-store.service.spec.ts
index 26a04f6..7220390 100644
--- a/src/ClientApp/src/app/infrastructure/services/user-store.service.spec.ts
+++ b/src/ClientApp/src/app/infrastructure/services/user-store.service.spec.ts
@@ -1,5 +1,4 @@
import {TestBed} from '@angular/core/testing';
-
import {UserStoreService} from './user-store.service';
describe('UserStoreService', () => {
@@ -13,4 +12,35 @@ describe('UserStoreService', () => {
it('should be created', () => {
expect(service).toBeTruthy();
});
+
+ it('should set and get username', (done) => {
+ service.setUsernameForStore('testuser');
+ service.getUsernameFromStore().subscribe(val => {
+ expect(val).toBe('testuser');
+ done();
+ });
+ });
+
+ it('should set and get role', (done) => {
+ service.setRoleForStore('Admin');
+ service.getRoleFromStore().subscribe(val => {
+ expect(val).toBe('Admin');
+ done();
+ });
+ });
+
+ it('should set and get email', (done) => {
+ service.setEmailForStore('test@example.com');
+ service.getEmailFromStore().subscribe(val => {
+ expect(val).toBe('test@example.com');
+ done();
+ });
+ });
+
+ it('should start with empty defaults', (done) => {
+ service.getUsernameFromStore().subscribe(val => {
+ expect(val).toBe('');
+ done();
+ });
+ });
});
diff --git a/src/ClientApp/src/app/infrastructure/services/user.service.spec.ts b/src/ClientApp/src/app/infrastructure/services/user.service.spec.ts
index ca647a6..3fb9636 100644
--- a/src/ClientApp/src/app/infrastructure/services/user.service.spec.ts
+++ b/src/ClientApp/src/app/infrastructure/services/user.service.spec.ts
@@ -1,23 +1,49 @@
import {TestBed} from '@angular/core/testing';
-
+import {provideHttpClient} from '@angular/common/http';
+import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';
import {UserService} from './user.service';
-import {provideHttpClient} from "@angular/common/http";
-import {ActivatedRoute} from "@angular/router";
-import {of} from "rxjs";
+import {environment} from '../../../environments/environment.development';
describe('UserService', () => {
let service: UserService;
+ let httpTesting: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
- providers: [provideHttpClient(),
- { provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } }
+ providers: [
+ provideHttpClient(),
+ provideHttpClientTesting(),
]
});
service = TestBed.inject(UserService);
+ httpTesting = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => {
+ httpTesting.verify();
});
it('should be created', () => {
expect(service).toBeTruthy();
});
+
+ it('should call GET with page params on getAllUsers', () => {
+ service.getAllUsers(1, 10).subscribe();
+
+ const req = httpTesting.expectOne(r =>
+ r.url === `${environment.baseUrl}user` &&
+ r.params.get('pageNumber') === '1' &&
+ r.params.get('pageSize') === '10'
+ );
+ expect(req.request.method).toBe('GET');
+ req.flush({value: {items: [], totalPages: 1}});
+ });
+
+ it('should call DELETE on deleteUser', () => {
+ service.deleteUser(5).subscribe();
+
+ const req = httpTesting.expectOne(`${environment.baseUrl}user/5`);
+ expect(req.request.method).toBe('DELETE');
+ req.flush({value: 'User deleted successfully'});
+ });
});
diff --git a/src/ClientApp/src/app/infrastructure/utilities/password-validator.spec.ts b/src/ClientApp/src/app/infrastructure/utilities/password-validator.spec.ts
index 55b4199..dd19682 100644
--- a/src/ClientApp/src/app/infrastructure/utilities/password-validator.spec.ts
+++ b/src/ClientApp/src/app/infrastructure/utilities/password-validator.spec.ts
@@ -1,7 +1,47 @@
-import { PasswordValidator } from './password-validator';
+import {FormControl} from '@angular/forms';
+import {PasswordValidator} from './password-validator';
describe('PasswordValidator', () => {
- it('should create an instance', () => {
- expect(new PasswordValidator()).toBeTruthy();
+ it('should return null for a valid strong password', () => {
+ const control = new FormControl('ValidP@ss1');
+ const result = PasswordValidator.strong(control);
+ expect(result).toBeNull();
+ });
+
+ it('should return uppercase error when password has no uppercase', () => {
+ const control = new FormControl('lowercase1@');
+ const result = PasswordValidator.strong(control);
+ expect(result?.['uppercase']).toBeTrue();
+ });
+
+ it('should return lowercase error when password has no lowercase', () => {
+ const control = new FormControl('UPPERCASE1@');
+ const result = PasswordValidator.strong(control);
+ expect(result?.['lowercase']).toBeTrue();
+ });
+
+ it('should return number error when password has no digit', () => {
+ const control = new FormControl('NoDigit@aa');
+ const result = PasswordValidator.strong(control);
+ expect(result?.['number']).toBeTrue();
+ });
+
+ it('should return special error when password has no special character', () => {
+ const control = new FormControl('NoSpecialChar1');
+ const result = PasswordValidator.strong(control);
+ expect(result?.['special']).toBeTrue();
+ });
+
+ it('should return minlength error when password is too short', () => {
+ const control = new FormControl('Sh0rt!');
+ const result = PasswordValidator.strong(control);
+ expect(result?.['minlength']).toBeTrue();
+ });
+
+ it('should return multiple errors for a completely invalid password', () => {
+ const control = new FormControl('');
+ const result = PasswordValidator.strong(control);
+ expect(result).not.toBeNull();
+ expect(Object.keys(result!).length).toBeGreaterThan(1);
});
});
diff --git a/src/ClientApp/src/app/infrastructure/utilities/reset-password-validator.spec.ts b/src/ClientApp/src/app/infrastructure/utilities/reset-password-validator.spec.ts
new file mode 100644
index 0000000..2be66ae
--- /dev/null
+++ b/src/ClientApp/src/app/infrastructure/utilities/reset-password-validator.spec.ts
@@ -0,0 +1,23 @@
+import {FormControl, FormGroup, ValidatorFn} from '@angular/forms';
+import {ConfirmPasswordValidator} from './reset-password-validator';
+
+describe('ConfirmPasswordValidator', () => {
+ const createForm = (password: string, confirm: string) => {
+ return new FormGroup({
+ password: new FormControl(password),
+ confirmPassword: new FormControl(confirm),
+ }, {validators: ConfirmPasswordValidator('password', 'confirmPassword') as ValidatorFn});
+ };
+
+ it('should set error when passwords do not match', () => {
+ const form = createForm('Pass123!', 'Different!');
+ form.updateValueAndValidity();
+ expect(form.get('confirmPassword')?.errors?.['ConfirmPasswordValidator']).toBeTrue();
+ });
+
+ it('should not set error when passwords match', () => {
+ const form = createForm('Pass123!', 'Pass123!');
+ form.updateValueAndValidity();
+ expect(form.get('confirmPassword')?.errors).toBeNull();
+ });
+});
diff --git a/src/ClientApp/src/app/infrastructure/utilities/validate-form.spec.ts b/src/ClientApp/src/app/infrastructure/utilities/validate-form.spec.ts
index a551dac..bbe383a 100644
--- a/src/ClientApp/src/app/infrastructure/utilities/validate-form.spec.ts
+++ b/src/ClientApp/src/app/infrastructure/utilities/validate-form.spec.ts
@@ -1,7 +1,30 @@
+import {FormControl, FormGroup, Validators} from '@angular/forms';
import ValidateForm from './validate-form';
describe('ValidateForm', () => {
- it('should create an instance', () => {
- expect(new ValidateForm()).toBeTruthy();
+ it('should mark all controls as dirty', () => {
+ const form = new FormGroup({
+ name: new FormControl('', Validators.required),
+ email: new FormControl('', Validators.required),
+ });
+ ValidateForm.validateAllFormFields(form);
+ expect(form.get('name')?.dirty).toBeTrue();
+ expect(form.get('email')?.dirty).toBeTrue();
+ });
+
+ it('should recursively mark nested form groups', () => {
+ const form = new FormGroup({
+ user: new FormGroup({
+ name: new FormControl('', Validators.required),
+ }),
+ });
+ ValidateForm.validateAllFormFields(form);
+ const userGroup = form.get('user') as FormGroup;
+ expect(userGroup.get('name')?.dirty).toBeTrue();
+ });
+
+ it('should not throw for empty form group', () => {
+ const form = new FormGroup({});
+ expect(() => ValidateForm.validateAllFormFields(form)).not.toThrow();
});
});
diff --git a/src/ClientApp/src/app/presentation/authentication/forget-password-popup/forget-password-popup.component.spec.ts b/src/ClientApp/src/app/presentation/authentication/forget-password-popup/forget-password-popup.component.spec.ts
index 9c0a00c..96b27b1 100644
--- a/src/ClientApp/src/app/presentation/authentication/forget-password-popup/forget-password-popup.component.spec.ts
+++ b/src/ClientApp/src/app/presentation/authentication/forget-password-popup/forget-password-popup.component.spec.ts
@@ -1,24 +1,32 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
-
import {ForgetPasswordPopupComponent} from './forget-password-popup.component';
-import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
-import {provideHttpClient} from "@angular/common/http";
-import {ActivatedRoute} from "@angular/router";
-import {of} from "rxjs";
+import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap';
+import {ResetPasswordService} from '../../../infrastructure/services/reset-password.service';
+import {ToastService} from '../../../infrastructure/services/toast.service';
+import {LoadingService} from '../../../infrastructure/services/loading.service';
+import {of, throwError} from 'rxjs';
describe('ForgetPasswordPopupComponent', () => {
let component: ForgetPasswordPopupComponent;
let fixture: ComponentFixture;
+ let resetService: jasmine.SpyObj;
+ let toastService: jasmine.SpyObj;
+ let activeModal: jasmine.SpyObj;
beforeEach(async () => {
+ resetService = jasmine.createSpyObj('ResetPasswordService', ['sendResetPasswordLink']);
+ toastService = jasmine.createSpyObj('ToastService', ['show']);
+ activeModal = jasmine.createSpyObj('NgbActiveModal', ['close']);
+
await TestBed.configureTestingModule({
imports: [ForgetPasswordPopupComponent],
- providers: [provideHttpClient(),
- { provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } },
- NgbActiveModal
+ providers: [
+ {provide: ResetPasswordService, useValue: resetService},
+ {provide: ToastService, useValue: toastService},
+ {provide: NgbActiveModal, useValue: activeModal},
+ LoadingService,
]
- })
- .compileComponents();
+ }).compileComponents();
fixture = TestBed.createComponent(ForgetPasswordPopupComponent);
component = fixture.componentInstance;
@@ -28,4 +36,39 @@ describe('ForgetPasswordPopupComponent', () => {
it('should create ForgetPasswordPopupComponent', () => {
expect(component).toBeTruthy();
});
+
+ it('should validate email format', () => {
+ expect(component.checkValidEmail('invalid')).toBeFalse();
+ expect(component.checkValidEmail('test@example.com')).toBeTrue();
+ expect(component.isValidEmail).toBeTrue();
+ });
+
+ it('should call sendResetPasswordLink on confirm with valid email', () => {
+ resetService.sendResetPasswordLink.and.returnValue(of({value: 'Email sent'}));
+ component.resetPasswordEmail = 'test@example.com';
+ component.isValidEmail = true;
+
+ component.confirmToSend();
+
+ expect(resetService.sendResetPasswordLink).toHaveBeenCalledWith('test@example.com');
+ });
+
+ it('should not call API when email is invalid', () => {
+ component.resetPasswordEmail = 'invalid-email';
+ component.confirmToSend();
+
+ expect(resetService.sendResetPasswordLink).not.toHaveBeenCalled();
+ });
+
+ it('should show error toast on failure', () => {
+ resetService.sendResetPasswordLink.and.returnValue(throwError(() => ({
+ error: {error: {message: 'User not found'}}
+ })));
+ component.resetPasswordEmail = 'test@example.com';
+ component.isValidEmail = true;
+
+ component.confirmToSend();
+
+ expect(toastService.show).toHaveBeenCalledWith('User not found', jasmine.any(Object));
+ });
});
diff --git a/src/ClientApp/src/app/presentation/authentication/login/login.component.spec.ts b/src/ClientApp/src/app/presentation/authentication/login/login.component.spec.ts
index 0837578..e898134 100644
--- a/src/ClientApp/src/app/presentation/authentication/login/login.component.spec.ts
+++ b/src/ClientApp/src/app/presentation/authentication/login/login.component.spec.ts
@@ -1,23 +1,38 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
-
import {LoginComponent} from './login.component';
-import {ActivatedRoute, provideRouter, Router} from "@angular/router";
-import {Title} from "@angular/platform-browser";
-import {of} from "rxjs";
-import {provideHttpClient} from "@angular/common/http";
+import {AuthService} from '../../../infrastructure/services/auth-service';
+import {ToastService} from '../../../infrastructure/services/toast.service';
+import {UserStoreService} from '../../../infrastructure/services/user-store.service';
+import {LoadingService} from '../../../infrastructure/services/loading.service';
+import {ActivatedRoute, Router} from '@angular/router';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {of, throwError} from 'rxjs';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture;
+ let authService: jasmine.SpyObj;
+ let router: jasmine.SpyObj;
+ let toastService: jasmine.SpyObj;
beforeEach(async () => {
+ authService = jasmine.createSpyObj('AuthService', ['login', 'storeToken', 'storeRefreshToken', 'decodedToken']);
+ router = jasmine.createSpyObj('Router', ['navigate']);
+ toastService = jasmine.createSpyObj('ToastService', ['show']);
+ const modalService = jasmine.createSpyObj('NgbModal', ['open']);
+
await TestBed.configureTestingModule({
imports: [LoginComponent],
- providers: [provideHttpClient(),
- { provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } }
+ providers: [
+ {provide: AuthService, useValue: authService},
+ {provide: Router, useValue: router},
+ {provide: ToastService, useValue: toastService},
+ {provide: NgbModal, useValue: modalService},
+ {provide: ActivatedRoute, useValue: {snapshot: {data: {}}, firstChild: null}},
+ UserStoreService,
+ LoadingService,
]
- })
- .compileComponents();
+ }).compileComponents();
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
@@ -27,4 +42,63 @@ describe('LoginComponent', () => {
it('should create LoginComponent', () => {
expect(component).toBeTruthy();
});
+
+ it('should have an invalid form when fields are empty', () => {
+ expect(component.loginForm.valid).toBeFalse();
+ });
+
+ it('should validate email pattern', () => {
+ const emailControl = component.loginForm.get('email');
+ emailControl?.setValue('not-an-email');
+ expect(emailControl?.valid).toBeFalse();
+ emailControl?.setValue('test@example.com');
+ expect(emailControl?.valid).toBeTrue();
+ });
+
+ it('should require password', () => {
+ const passwordControl = component.loginForm.get('password');
+ expect(passwordControl?.valid).toBeFalse();
+ passwordControl?.setValue('somepassword');
+ expect(passwordControl?.valid).toBeTrue();
+ });
+
+ it('should call login on submit when form is valid', () => {
+ const tokenPayload = {username: 'testuser', email: 'test@example.com', role: 'User'};
+ authService.login.and.returnValue(of({
+ value: {token: {accessToken: 'access', refreshToken: 'refresh'}}
+ }));
+ authService.decodedToken.and.returnValue(tokenPayload);
+ router.navigate.and.returnValue(Promise.resolve(true));
+
+ component.loginForm.setValue({
+ email: 'test@example.com',
+ password: 'Password1!',
+ });
+ component.onLogin();
+
+ expect(authService.login).toHaveBeenCalled();
+ expect(authService.storeToken).toHaveBeenCalledWith('access');
+ expect(authService.storeRefreshToken).toHaveBeenCalledWith('refresh');
+ });
+
+ it('should show error toast on login failure', () => {
+ authService.login.and.returnValue(throwError(() => ({
+ error: {error: {message: 'Invalid credentials'}}
+ })));
+
+ component.loginForm.setValue({
+ email: 'test@example.com',
+ password: 'wrong',
+ });
+ component.onLogin();
+
+ expect(toastService.show).toHaveBeenCalledWith('Invalid credentials', jasmine.any(Object));
+ });
+
+ it('should toggle password visibility', () => {
+ component.hideShowPassword();
+ expect(component.isText).toBeFalse();
+ component.hideShowPassword();
+ expect(component.isText).toBeTrue();
+ });
});
diff --git a/src/ClientApp/src/app/presentation/authentication/register/register.component.spec.ts b/src/ClientApp/src/app/presentation/authentication/register/register.component.spec.ts
index 31e36e8..caefc36 100644
--- a/src/ClientApp/src/app/presentation/authentication/register/register.component.spec.ts
+++ b/src/ClientApp/src/app/presentation/authentication/register/register.component.spec.ts
@@ -1,19 +1,31 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
-
import {RegisterComponent} from './register.component';
-import {HttpClient, provideHttpClient} from "@angular/common/http";
-import {ActivatedRoute} from "@angular/router";
-import {of} from "rxjs";
+import {AuthService} from '../../../infrastructure/services/auth-service';
+import {ToastService} from '../../../infrastructure/services/toast.service';
+import {LoadingService} from '../../../infrastructure/services/loading.service';
+import {ActivatedRoute, Router} from '@angular/router';
+import {of, throwError} from 'rxjs';
describe('RegisterComponent', () => {
let component: RegisterComponent;
let fixture: ComponentFixture;
+ let authService: jasmine.SpyObj;
+ let router: jasmine.SpyObj;
+ let toastService: jasmine.SpyObj;
beforeEach(async () => {
+ authService = jasmine.createSpyObj('AuthService', ['register']);
+ router = jasmine.createSpyObj('Router', ['navigate']);
+ toastService = jasmine.createSpyObj('ToastService', ['show']);
+
await TestBed.configureTestingModule({
imports: [RegisterComponent],
- providers: [provideHttpClient(),
- { provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } }
+ providers: [
+ {provide: AuthService, useValue: authService},
+ {provide: Router, useValue: router},
+ {provide: ToastService, useValue: toastService},
+ {provide: ActivatedRoute, useValue: {snapshot: {data: {}}, firstChild: null}},
+ LoadingService,
]
}).compileComponents();
@@ -25,4 +37,68 @@ describe('RegisterComponent', () => {
it('should create RegisterComponent', () => {
expect(component).toBeTruthy();
});
+
+ it('should have an invalid form when fields are empty', () => {
+ expect(component.registerForm.valid).toBeFalse();
+ });
+
+ it('should validate username pattern', () => {
+ const usernameControl = component.registerForm.get('username');
+ usernameControl?.setValue('invalid username!');
+ expect(usernameControl?.valid).toBeFalse();
+ usernameControl?.setValue('valid_user');
+ expect(usernameControl?.valid).toBeTrue();
+ });
+
+ it('should validate email pattern', () => {
+ const emailControl = component.registerForm.get('email');
+ emailControl?.setValue('invalid');
+ expect(emailControl?.valid).toBeFalse();
+ emailControl?.setValue('test@example.com');
+ expect(emailControl?.valid).toBeTrue();
+ });
+
+ it('should validate password strength', () => {
+ const passwordControl = component.registerForm.get('password');
+ passwordControl?.setValue('weak');
+ expect(passwordControl?.valid).toBeFalse();
+ passwordControl?.setValue('StrongP@ss1');
+ expect(passwordControl?.valid).toBeTrue();
+ });
+
+ it('should call register on submit when form is valid', () => {
+ authService.register.and.returnValue(of({isSuccess: true, value: 'Registered'}));
+ router.navigate.and.returnValue(Promise.resolve(true));
+
+ component.registerForm.setValue({
+ username: 'testuser',
+ email: 'test@example.com',
+ password: 'StrongP@ss1',
+ });
+ component.onSignUp();
+
+ expect(authService.register).toHaveBeenCalled();
+ });
+
+ it('should show error toast on registration failure', () => {
+ authService.register.and.returnValue(throwError(() => ({
+ error: {error: {message: 'Email already exists'}}
+ })));
+
+ component.registerForm.setValue({
+ username: 'testuser',
+ email: 'existing@example.com',
+ password: 'StrongP@ss1',
+ });
+ component.onSignUp();
+
+ expect(toastService.show).toHaveBeenCalledWith('Email already exists', jasmine.any(Object));
+ });
+
+ it('should toggle password visibility', () => {
+ component.hideShowPassword();
+ expect(component.isText).toBeFalse();
+ component.hideShowPassword();
+ expect(component.isText).toBeTrue();
+ });
});
diff --git a/src/ClientApp/src/app/presentation/authentication/reset-password/reset-password.component.spec.ts b/src/ClientApp/src/app/presentation/authentication/reset-password/reset-password.component.spec.ts
index 3c7e305..7e32cbc 100644
--- a/src/ClientApp/src/app/presentation/authentication/reset-password/reset-password.component.spec.ts
+++ b/src/ClientApp/src/app/presentation/authentication/reset-password/reset-password.component.spec.ts
@@ -1,32 +1,36 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
-
import {ResetPasswordComponent} from './reset-password.component';
-import {provideHttpClient} from "@angular/common/http";
-import {ActivatedRoute} from "@angular/router";
-import {of} from "rxjs";
-import {ReactiveFormsModule} from "@angular/forms";
+import {ResetPasswordService} from '../../../infrastructure/services/reset-password.service';
+import {ToastService} from '../../../infrastructure/services/toast.service';
+import {ActivatedRoute, Router} from '@angular/router';
+import {of, throwError} from 'rxjs';
describe('ResetPasswordComponent', () => {
let component: ResetPasswordComponent;
let fixture: ComponentFixture;
+ let resetService: jasmine.SpyObj;
+ let router: jasmine.SpyObj;
+ let toastService: jasmine.SpyObj;
beforeEach(async () => {
+ resetService = jasmine.createSpyObj('ResetPasswordService', ['resetPassword']);
+ router = jasmine.createSpyObj('Router', ['navigate']);
+ toastService = jasmine.createSpyObj('ToastService', ['show']);
+
await TestBed.configureTestingModule({
- imports: [ResetPasswordComponent, ReactiveFormsModule],
- providers: [provideHttpClient(),
- { provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } },
+ imports: [ResetPasswordComponent],
+ providers: [
+ {provide: ResetPasswordService, useValue: resetService},
+ {provide: Router, useValue: router},
+ {provide: ToastService, useValue: toastService},
{
provide: ActivatedRoute,
useValue: {
- queryParams: of({
- email: 'test@example.com',
- code: 'test-token'
- })
+ queryParams: of({email: 'test@example.com', code: 'test-token'})
}
}
]
- })
- .compileComponents();
+ }).compileComponents();
fixture = TestBed.createComponent(ResetPasswordComponent);
component = fixture.componentInstance;
@@ -36,4 +40,53 @@ describe('ResetPasswordComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
+
+ it('should read email and token from query params', () => {
+ expect(component.emailToReset).toBe('test@example.com');
+ expect(component.emailToken).toBe('test-token');
+ });
+
+ it('should have an invalid form when fields are empty', () => {
+ expect(component.resetForm.valid).toBeFalse();
+ });
+
+ it('should validate password strength', () => {
+ const passwordControl = component.resetForm.get('password');
+ passwordControl?.setValue('weak');
+ expect(passwordControl?.valid).toBeFalse();
+ passwordControl?.setValue('StrongP@ss1');
+ expect(passwordControl?.valid).toBeTrue();
+ });
+
+ it('should validate password confirmation match', () => {
+ component.resetForm.get('password')?.setValue('StrongP@ss1');
+ component.resetForm.get('confirmPassword')?.setValue('Different!');
+ expect(component.resetForm.valid).toBeFalse();
+ component.resetForm.get('confirmPassword')?.setValue('StrongP@ss1');
+ expect(component.resetForm.valid).toBeTrue();
+ });
+
+ it('should call resetPassword on submit when form is valid', () => {
+ resetService.resetPassword.and.returnValue(of({value: 'Password reset'}));
+ router.navigate.and.returnValue(Promise.resolve(true));
+
+ component.resetForm.get('password')?.setValue('NewStrongP@ss1');
+ component.resetForm.get('confirmPassword')?.setValue('NewStrongP@ss1');
+ component.reset();
+
+ expect(resetService.resetPassword).toHaveBeenCalled();
+ });
+
+ it('should show error alert on failure', () => {
+ spyOn(window, 'alert');
+ resetService.resetPassword.and.returnValue(throwError(() => ({
+ error: {error: {message: 'Invalid token'}}
+ })));
+
+ component.resetForm.get('password')?.setValue('NewStrongP@ss1');
+ component.resetForm.get('confirmPassword')?.setValue('NewStrongP@ss1');
+ component.reset();
+
+ expect(window.alert).toHaveBeenCalledWith('Invalid token');
+ });
});
diff --git a/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.spec.ts b/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.spec.ts
index 5884066..148a886 100644
--- a/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.spec.ts
+++ b/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.spec.ts
@@ -1,6 +1,6 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { AdminDashboardComponent } from './admin-dashboard.component';
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {AdminDashboardComponent} from './admin-dashboard.component';
+import {ActivatedRoute} from '@angular/router';
describe('AdminDashboardComponent', () => {
let component: AdminDashboardComponent;
@@ -8,9 +8,11 @@ describe('AdminDashboardComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [AdminDashboardComponent]
- })
- .compileComponents();
+ imports: [AdminDashboardComponent],
+ providers: [
+ {provide: ActivatedRoute, useValue: {snapshot: {data: {}}, firstChild: null}},
+ ]
+ }).compileComponents();
fixture = TestBed.createComponent(AdminDashboardComponent);
component = fixture.componentInstance;
diff --git a/src/ClientApp/src/app/presentation/components/footer/footer.component.spec.ts b/src/ClientApp/src/app/presentation/components/footer/footer.component.spec.ts
index 3fdd1ad..49d9b72 100644
--- a/src/ClientApp/src/app/presentation/components/footer/footer.component.spec.ts
+++ b/src/ClientApp/src/app/presentation/components/footer/footer.component.spec.ts
@@ -1,9 +1,6 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
-
import {FooterComponent} from './footer.component';
-import {provideHttpClient} from "@angular/common/http";
-import {ActivatedRoute} from "@angular/router";
-import {of} from "rxjs";
+import {ActivatedRoute} from '@angular/router';
describe('FooterComponent', () => {
let component: FooterComponent;
@@ -12,11 +9,10 @@ describe('FooterComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FooterComponent],
- providers: [provideHttpClient(),
- { provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } }
+ providers: [
+ {provide: ActivatedRoute, useValue: {snapshot: {data: {}}, firstChild: null}},
]
- })
- .compileComponents();
+ }).compileComponents();
fixture = TestBed.createComponent(FooterComponent);
component = fixture.componentInstance;
@@ -26,4 +22,8 @@ describe('FooterComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
+
+ it('should have a version', () => {
+ expect(component.version).toBeDefined();
+ });
});
diff --git a/src/ClientApp/src/app/presentation/components/header/header.component.spec.ts b/src/ClientApp/src/app/presentation/components/header/header.component.spec.ts
index 2974309..b6dae41 100644
--- a/src/ClientApp/src/app/presentation/components/header/header.component.spec.ts
+++ b/src/ClientApp/src/app/presentation/components/header/header.component.spec.ts
@@ -1,22 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
-
import {HeaderComponent} from './header.component';
-import {provideHttpClient} from "@angular/common/http";
-import {ActivatedRoute} from "@angular/router";
-import {of} from "rxjs";
+import {AuthService} from '../../../infrastructure/services/auth-service';
+import {ActivatedRoute} from '@angular/router';
-describe('NavbarComponent', () => {
+describe('HeaderComponent', () => {
let component: HeaderComponent;
let fixture: ComponentFixture;
beforeEach(async () => {
+ const authSpy = jasmine.createSpyObj('AuthService', [
+ 'getUsernameFromToken', 'getRoleFromToken', 'getEmailFromToken', 'getUserIdFromToken',
+ 'isLoggedIn', 'getToken', 'signOut', 'decodedToken'
+ ]);
+
await TestBed.configureTestingModule({
imports: [HeaderComponent],
- providers: [provideHttpClient(),
- { provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } }
+ providers: [
+ {provide: AuthService, useValue: authSpy},
+ {provide: ActivatedRoute, useValue: {snapshot: {data: {}}, firstChild: null}},
]
- })
- .compileComponents();
+ }).compileComponents();
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
diff --git a/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.spec.ts b/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.spec.ts
index 138e3af..39a26bd 100644
--- a/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.spec.ts
+++ b/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.spec.ts
@@ -1,16 +1,24 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { UserDashboardComponent } from './user-dashboard.component';
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {UserDashboardComponent} from './user-dashboard.component';
+import {AuthService} from '../../../infrastructure/services/auth-service';
+import {UserStoreService} from '../../../infrastructure/services/user-store.service';
+import {of} from 'rxjs';
describe('UserDashboardComponent', () => {
let component: UserDashboardComponent;
let fixture: ComponentFixture;
beforeEach(async () => {
+ const authSpy = jasmine.createSpyObj('AuthService', [
+ 'getUsernameFromToken', 'getEmailFromToken'
+ ]);
+
await TestBed.configureTestingModule({
- imports: [UserDashboardComponent]
- })
- .compileComponents();
+ imports: [UserDashboardComponent],
+ providers: [
+ {provide: AuthService, useValue: authSpy},
+ ]
+ }).compileComponents();
fixture = TestBed.createComponent(UserDashboardComponent);
component = fixture.componentInstance;
diff --git a/src/ClientApp/src/app/presentation/components/user-table/user-table.component.spec.ts b/src/ClientApp/src/app/presentation/components/user-table/user-table.component.spec.ts
index 1e1eb4f..adf7232 100644
--- a/src/ClientApp/src/app/presentation/components/user-table/user-table.component.spec.ts
+++ b/src/ClientApp/src/app/presentation/components/user-table/user-table.component.spec.ts
@@ -1,16 +1,38 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { UserTableComponent } from './user-table.component';
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {UserTableComponent} from './user-table.component';
+import {UserService} from '../../../infrastructure/services/user.service';
+import {ToastService} from '../../../infrastructure/services/toast.service';
+import {of, throwError} from 'rxjs';
describe('UserTableComponent', () => {
let component: UserTableComponent;
let fixture: ComponentFixture;
+ let userService: jasmine.SpyObj;
+ let toastService: jasmine.SpyObj;
+
+ const mockUsers = {
+ value: {
+ items: [
+ {id: 1, username: 'user1', email: 'user1@test.com', lastLogin: new Date(), UserRoles: [{role: {name: 'Admin'}}]},
+ {id: 2, username: 'user2', email: 'user2@test.com', lastLogin: new Date(), UserRoles: [{role: {name: 'User'}}]},
+ ],
+ totalPages: 3,
+ }
+ };
beforeEach(async () => {
+ userService = jasmine.createSpyObj('UserService', ['getAllUsers', 'deleteUser']);
+ toastService = jasmine.createSpyObj('ToastService', ['show']);
+
+ userService.getAllUsers.and.returnValue(of(mockUsers));
+
await TestBed.configureTestingModule({
- imports: [UserTableComponent]
- })
- .compileComponents();
+ imports: [UserTableComponent],
+ providers: [
+ {provide: UserService, useValue: userService},
+ {provide: ToastService, useValue: toastService},
+ ]
+ }).compileComponents();
fixture = TestBed.createComponent(UserTableComponent);
component = fixture.componentInstance;
@@ -20,4 +42,111 @@ describe('UserTableComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
+
+ it('should load users on init', () => {
+ expect(userService.getAllUsers).toHaveBeenCalledWith(1, 10);
+ expect(component.users.length).toBe(2);
+ expect(component.totalPages).toBe(3);
+ });
+
+ it('should go to next page', () => {
+ component.pageNumber = 1;
+ component.totalPages = 3;
+ component.nextPage();
+ expect(component.pageNumber).toBe(2);
+ expect(userService.getAllUsers).toHaveBeenCalledWith(2, 10);
+ });
+
+ it('should not go beyond last page', () => {
+ component.pageNumber = 3;
+ component.totalPages = 3;
+ component.nextPage();
+ expect(component.pageNumber).toBe(3);
+ });
+
+ it('should go to previous page', () => {
+ component.pageNumber = 2;
+ component.previousPage();
+ expect(component.pageNumber).toBe(1);
+ expect(userService.getAllUsers).toHaveBeenCalledWith(1, 10);
+ });
+
+ it('should not go below first page', () => {
+ component.pageNumber = 1;
+ component.previousPage();
+ expect(component.pageNumber).toBe(1);
+ });
+
+ it('should go to specific page', () => {
+ component.goToPage(2);
+ expect(component.pageNumber).toBe(2);
+ expect(userService.getAllUsers).toHaveBeenCalledWith(2, 10);
+ });
+
+ it('should not go to invalid page', () => {
+ component.goToPage(0);
+ expect(component.pageNumber).toBe(1);
+ component.goToPage(999);
+ expect(component.pageNumber).toBe(1);
+ });
+
+ it('should sort users by column ascending', () => {
+ component.users = [
+ {id: 2, username: 'b_user'} as any,
+ {id: 1, username: 'a_user'} as any,
+ ];
+ component.sort('username');
+ expect(component.users[0].username).toBe('a_user');
+ expect(component.users[1].username).toBe('b_user');
+ expect(component.sortDirection).toBe('asc');
+ });
+
+ it('should toggle sort direction on same column', () => {
+ component.users = [
+ {id: 1, username: 'a_user'} as any,
+ {id: 2, username: 'b_user'} as any,
+ ];
+ component.sort('username');
+ component.sort('username');
+ expect(component.users[0].username).toBe('b_user');
+ expect(component.users[1].username).toBe('a_user');
+ expect(component.sortDirection).toBe('desc');
+ });
+
+ it('should delete user and remove from list', () => {
+ userService.deleteUser.and.returnValue(of({value: 'User deleted successfully'}));
+ component.users = [
+ {id: 1, username: 'user1'} as any,
+ {id: 2, username: 'user2'} as any,
+ ];
+
+ component.deleteUser(1);
+
+ expect(userService.deleteUser).toHaveBeenCalledWith(1);
+ expect(component.users.length).toBe(1);
+ expect(component.users[0].id).toBe(2);
+ expect(toastService.show).toHaveBeenCalledWith('User deleted successfully', jasmine.any(Object));
+ });
+
+ it('should show error toast on delete failure', () => {
+ userService.deleteUser.and.returnValue(throwError(() => ({
+ error: {error: {message: 'User not found'}}
+ })));
+
+ component.deleteUser(99);
+
+ expect(toastService.show).toHaveBeenCalledWith('User not found', jasmine.any(Object));
+ });
+
+ it('should select user and handle confirm deletion', () => {
+ const user = {id: 1, username: 'user1'} as any;
+ userService.deleteUser.and.returnValue(of({value: 'Deleted'}));
+ component.users = [user];
+
+ component.selectUser(user);
+ expect(component.selectedUser).toBe(user);
+
+ component.handleConfirm();
+ expect(userService.deleteUser).toHaveBeenCalledWith(1);
+ });
});
diff --git a/src/ClientApp/src/app/presentation/guards/admin.guard.spec.ts b/src/ClientApp/src/app/presentation/guards/admin.guard.spec.ts
index 0ddaaa9..7633194 100644
--- a/src/ClientApp/src/app/presentation/guards/admin.guard.spec.ts
+++ b/src/ClientApp/src/app/presentation/guards/admin.guard.spec.ts
@@ -1,17 +1,62 @@
import {TestBed} from '@angular/core/testing';
-import {CanActivateFn} from '@angular/router';
-
+import {Router} from '@angular/router';
import {adminGuard} from './admin.guard';
+import {AuthService} from '../../infrastructure/services/auth-service';
+import {ToastService} from '../../infrastructure/services/toast.service';
+import {UserStoreService} from '../../infrastructure/services/user-store.service';
describe('adminGuard', () => {
- const executeGuard: CanActivateFn = (...guardParameters) =>
- TestBed.runInInjectionContext(() => adminGuard(...guardParameters));
+ let authService: jasmine.SpyObj;
+ let router: jasmine.SpyObj;
+ let toastService: jasmine.SpyObj;
beforeEach(() => {
- TestBed.configureTestingModule({});
+ authService = jasmine.createSpyObj('AuthService', ['getRoleFromToken']);
+ router = jasmine.createSpyObj('Router', ['navigate']);
+ toastService = jasmine.createSpyObj('ToastService', ['show']);
+
+ TestBed.configureTestingModule({
+ providers: [
+ UserStoreService,
+ {provide: AuthService, useValue: authService},
+ {provide: Router, useValue: router},
+ {provide: ToastService, useValue: toastService},
+ ]
+ });
});
+ const executeGuard = () =>
+ TestBed.runInInjectionContext(() => adminGuard({} as any, {} as any));
+
it('should be created', () => {
expect(executeGuard).toBeTruthy();
});
+
+ it('should return true for Admin role', (done) => {
+ const userStore = TestBed.inject(UserStoreService);
+ userStore.setRoleForStore('Admin');
+ (executeGuard() as any).subscribe((result: boolean) => {
+ expect(result).toBeTrue();
+ done();
+ });
+ });
+
+ it('should return true for SuperAdmin role', (done) => {
+ const userStore = TestBed.inject(UserStoreService);
+ userStore.setRoleForStore('SuperAdmin');
+ (executeGuard() as any).subscribe((result: boolean) => {
+ expect(result).toBeTrue();
+ done();
+ });
+ });
+
+ it('should return false and redirect for non-admin role', (done) => {
+ const userStore = TestBed.inject(UserStoreService);
+ userStore.setRoleForStore('User');
+ (executeGuard() as any).subscribe((result: boolean) => {
+ expect(result).toBeFalse();
+ expect(router.navigate).toHaveBeenCalledWith(['/unauthorized']);
+ done();
+ });
+ });
});
diff --git a/src/ClientApp/src/app/presentation/guards/authentication.guard.spec.ts b/src/ClientApp/src/app/presentation/guards/authentication.guard.spec.ts
index 71fed82..7ef1b78 100644
--- a/src/ClientApp/src/app/presentation/guards/authentication.guard.spec.ts
+++ b/src/ClientApp/src/app/presentation/guards/authentication.guard.spec.ts
@@ -1,24 +1,45 @@
import {TestBed} from '@angular/core/testing';
-
+import {Router} from '@angular/router';
import {AuthenticationGuard} from './authentication.guard';
-import {provideHttpClient} from "@angular/common/http";
-import {ActivatedRoute} from "@angular/router";
-import {of} from "rxjs";
+import {AuthService} from '../../infrastructure/services/auth-service';
+import {ToastService} from '../../infrastructure/services/toast.service';
describe('AuthenticationGuard', () => {
let guard: AuthenticationGuard;
+ let authService: jasmine.SpyObj;
+ let router: jasmine.SpyObj;
beforeEach(() => {
+ const authSpy = jasmine.createSpyObj('AuthService', ['isLoggedIn']);
+ const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
+ routerSpy.navigate.and.returnValue(Promise.resolve(true));
+ const toastSpy = jasmine.createSpyObj('ToastService', ['show']);
+
TestBed.configureTestingModule({
- providers: [provideHttpClient(),
- { provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } },
+ providers: [
+ AuthenticationGuard,
+ {provide: AuthService, useValue: authSpy},
+ {provide: Router, useValue: routerSpy},
+ {provide: ToastService, useValue: toastSpy},
]
});
guard = TestBed.inject(AuthenticationGuard);
-
+ authService = TestBed.inject(AuthService) as jasmine.SpyObj;
+ router = TestBed.inject(Router) as jasmine.SpyObj;
});
it('should be created', () => {
expect(guard).toBeTruthy();
});
+
+ it('should return true when user is logged in', () => {
+ authService.isLoggedIn.and.returnValue(true);
+ expect(guard.canActivate()).toBeTrue();
+ });
+
+ it('should return false and redirect when user is not logged in', () => {
+ authService.isLoggedIn.and.returnValue(false);
+ expect(guard.canActivate()).toBeFalse();
+ expect(router.navigate).toHaveBeenCalledWith(['/login']);
+ });
});
diff --git a/src/ClientApp/src/app/presentation/guards/guest.guard.spec.ts b/src/ClientApp/src/app/presentation/guards/guest.guard.spec.ts
index 6fdaf10..12b3f9d 100644
--- a/src/ClientApp/src/app/presentation/guards/guest.guard.spec.ts
+++ b/src/ClientApp/src/app/presentation/guards/guest.guard.spec.ts
@@ -1,17 +1,39 @@
-import { TestBed } from '@angular/core/testing';
-import { CanActivateFn } from '@angular/router';
-
-import { guestGuard } from './guest.guard';
+import {TestBed} from '@angular/core/testing';
+import {Router} from '@angular/router';
+import {guestGuard} from './guest.guard';
+import {AuthService} from '../../infrastructure/services/auth-service';
describe('guestGuard', () => {
- const executeGuard: CanActivateFn = (...guardParameters) =>
- TestBed.runInInjectionContext(() => guestGuard(...guardParameters));
+ let authService: jasmine.SpyObj;
+ let router: jasmine.SpyObj;
beforeEach(() => {
- TestBed.configureTestingModule({});
+ authService = jasmine.createSpyObj('AuthService', ['isLoggedIn']);
+ router = jasmine.createSpyObj('Router', ['navigate']);
+
+ TestBed.configureTestingModule({
+ providers: [
+ {provide: AuthService, useValue: authService},
+ {provide: Router, useValue: router},
+ ]
+ });
});
+ const executeGuard = () =>
+ TestBed.runInInjectionContext(() => guestGuard({} as any, {} as any));
+
it('should be created', () => {
expect(executeGuard).toBeTruthy();
});
+
+ it('should return true when user is not logged in', () => {
+ authService.isLoggedIn.and.returnValue(false);
+ expect(executeGuard()).toBeTrue();
+ });
+
+ it('should return false and redirect when user is logged in', () => {
+ authService.isLoggedIn.and.returnValue(true);
+ expect(executeGuard()).toBeFalse();
+ expect(router.navigate).toHaveBeenCalledWith(['/user-dashboard']);
+ });
});
diff --git a/src/ClientApp/src/app/presentation/interceptors/token.interceptor.spec.ts b/src/ClientApp/src/app/presentation/interceptors/token.interceptor.spec.ts
index e98338f..e19efa0 100644
--- a/src/ClientApp/src/app/presentation/interceptors/token.interceptor.spec.ts
+++ b/src/ClientApp/src/app/presentation/interceptors/token.interceptor.spec.ts
@@ -1,17 +1,50 @@
import {TestBed} from '@angular/core/testing';
-import {HttpInterceptorFn} from '@angular/common/http';
-
+import {HttpClient, HttpInterceptorFn, provideHttpClient, withInterceptors} from '@angular/common/http';
+import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';
import {tokenInterceptor} from './token.interceptor';
+import {AuthService} from '../../infrastructure/services/auth-service';
describe('tokenInterceptor', () => {
- const interceptor: HttpInterceptorFn = (req, next) =>
- TestBed.runInInjectionContext(() => tokenInterceptor(req, next));
+ let authService: jasmine.SpyObj;
+ let httpClient: HttpClient;
+ let httpTesting: HttpTestingController;
beforeEach(() => {
- TestBed.configureTestingModule({});
+ authService = jasmine.createSpyObj('AuthService', ['getToken', 'getRefreshToken', 'getUserIdFromToken',
+ 'storeToken', 'storeRefreshToken', 'signOut', 'renewToken']);
+
+ TestBed.configureTestingModule({
+ providers: [
+ provideHttpClient(withInterceptors([tokenInterceptor])),
+ provideHttpClientTesting(),
+ {provide: AuthService, useValue: authService},
+ ]
+ });
+ httpClient = TestBed.inject(HttpClient);
+ httpTesting = TestBed.inject(HttpTestingController);
});
- it('should be created', () => {
- expect(interceptor).toBeTruthy();
+ afterEach(() => {
+ httpTesting.verify();
+ });
+
+ it('should add Authorization header when token exists', () => {
+ authService.getToken.and.returnValue('test-jwt-token');
+
+ httpClient.get('/api/test').subscribe();
+
+ const req = httpTesting.expectOne('/api/test');
+ expect(req.request.headers.get('Authorization')).toBe('Bearer test-jwt-token');
+ req.flush({});
+ });
+
+ it('should not add Authorization header when no token', () => {
+ authService.getToken.and.returnValue(null);
+
+ httpClient.get('/api/test').subscribe();
+
+ const req = httpTesting.expectOne('/api/test');
+ expect(req.request.headers.has('Authorization')).toBeFalse();
+ req.flush({});
});
});