test: frontend test suite — Vitest infrastructure, auth/connection stores, vault composable, admin middleware

28 tests across 4 spec files. Vitest + happy-dom configured with Nuxt auto-import
shims ($$fetch, navigateTo, defineNuxtRouteMiddleware) so stores and composables
resolve cleanly outside the Nuxt runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell 2026-03-14 19:06:14 -04:00
parent 5abbffca9b
commit f01e357647
8 changed files with 1161 additions and 36 deletions

View File

@ -23,9 +23,13 @@
}, },
"devDependencies": { "devDependencies": {
"@nuxtjs/tailwindcss": "^6.0.0", "@nuxtjs/tailwindcss": "^6.0.0",
"@pinia/testing": "^0.1.7",
"@primevue/nuxt-module": "^4.0.0", "@primevue/nuxt-module": "^4.0.0",
"@vue/test-utils": "^2.4.6",
"happy-dom": "^20.8.4",
"nuxt": "^3.10.0", "nuxt": "^3.10.0",
"typescript": "^5.3.0" "typescript": "^5.3.0",
"vitest": "^4.1.0"
} }
}, },
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
@ -1706,23 +1710,6 @@
"nuxt": "^3.21.2" "nuxt": "^3.21.2"
} }
}, },
"node_modules/@nuxt/schema": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-4.4.2.tgz",
"integrity": "sha512-/q6C7Qhiricgi+PKR7ovBnJlKTL0memCbA1CzRT+itCW/oeYzUfeMdQ35mGntlBoyRPNrMXbzuSUhfDbSCU57w==",
"extraneous": true,
"license": "MIT",
"dependencies": {
"@vue/shared": "^3.5.30",
"defu": "^6.1.4",
"pathe": "^2.0.3",
"pkg-types": "^2.3.0",
"std-env": "^4.0.0"
},
"engines": {
"node": "^14.18.0 || >=16.10.0"
}
},
"node_modules/@nuxt/telemetry": { "node_modules/@nuxt/telemetry": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/@nuxt/telemetry/-/telemetry-2.7.0.tgz", "resolved": "https://registry.npmjs.org/@nuxt/telemetry/-/telemetry-2.7.0.tgz",
@ -1849,6 +1836,13 @@
"unctx": "^2.4.1" "unctx": "^2.4.1"
} }
}, },
"node_modules/@one-ini/wasm": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
"dev": true,
"license": "MIT"
},
"node_modules/@oxc-minify/binding-android-arm-eabi": { "node_modules/@oxc-minify/binding-android-arm-eabi": {
"version": "0.117.0", "version": "0.117.0",
"resolved": "https://registry.npmjs.org/@oxc-minify/binding-android-arm-eabi/-/binding-android-arm-eabi-0.117.0.tgz", "resolved": "https://registry.npmjs.org/@oxc-minify/binding-android-arm-eabi/-/binding-android-arm-eabi-0.117.0.tgz",
@ -3229,6 +3223,22 @@
"url": "https://github.com/sponsors/posva" "url": "https://github.com/sponsors/posva"
} }
}, },
"node_modules/@pinia/testing": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/@pinia/testing/-/testing-0.1.7.tgz",
"integrity": "sha512-xcDq6Ry/kNhZ5bsUMl7DeoFXwdume1NYzDggCiDUDKoPQ6Mo0eH9VU7bJvBtlurqe6byAntWoX5IhVFqWzRz/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"vue-demi": "^0.14.10"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"pinia": ">=2.2.6"
}
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -4022,6 +4032,13 @@
"dev": true, "dev": true,
"license": "CC0-1.0" "license": "CC0-1.0"
}, },
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"dev": true,
"license": "MIT"
},
"node_modules/@tybys/wasm-util": { "node_modules/@tybys/wasm-util": {
"version": "0.10.1", "version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@ -4033,12 +4050,40 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/deep-eql": "*",
"assertion-error": "^2.0.1"
}
},
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": {
"version": "25.5.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/@types/resolve": { "node_modules/@types/resolve": {
"version": "1.20.2", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@ -4046,6 +4091,23 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/whatwg-mimetype": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
"integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@unhead/vue": { "node_modules/@unhead/vue": {
"version": "2.1.12", "version": "2.1.12",
"resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.1.12.tgz", "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-2.1.12.tgz",
@ -4135,6 +4197,129 @@
"vue": "^3.0.0" "vue": "^3.0.0"
} }
}, },
"node_modules/@vitest/expect": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz",
"integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
"@vitest/spy": "4.1.0",
"@vitest/utils": "4.1.0",
"chai": "^6.2.2",
"tinyrainbow": "^3.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/mocker": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz",
"integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "4.1.0",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"msw": "^2.4.9",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0"
},
"peerDependenciesMeta": {
"msw": {
"optional": true
},
"vite": {
"optional": true
}
}
},
"node_modules/@vitest/mocker/node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0"
}
},
"node_modules/@vitest/pretty-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz",
"integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyrainbow": "^3.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/runner": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz",
"integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "4.1.0",
"pathe": "^2.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz",
"integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.1.0",
"@vitest/utils": "4.1.0",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/spy": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz",
"integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/utils": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz",
"integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.1.0",
"convert-source-map": "^2.0.0",
"tinyrainbow": "^3.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@volar/language-core": { "node_modules/@volar/language-core": {
"version": "2.4.28", "version": "2.4.28",
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
@ -4399,6 +4584,17 @@
"integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/test-utils": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz",
"integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==",
"dev": true,
"license": "MIT",
"dependencies": {
"js-beautify": "^1.14.9",
"vue-component-type-helpers": "^2.0.0"
}
},
"node_modules/@xterm/addon-fit": { "node_modules/@xterm/addon-fit": {
"version": "0.10.0", "version": "0.10.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
@ -4747,6 +4943,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/ast-kit": { "node_modules/ast-kit": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz",
@ -5312,6 +5518,16 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@ -5444,16 +5660,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"node_modules/commondir": { "node_modules/commondir": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@ -5511,6 +5717,24 @@
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/config-chain": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
"integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ini": "^1.3.4",
"proto-list": "~1.2.1"
}
},
"node_modules/config-chain/node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"dev": true,
"license": "ISC"
},
"node_modules/consola": { "node_modules/consola": {
"version": "3.4.2", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
@ -6184,6 +6408,68 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/editorconfig": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz",
"integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@one-ini/wasm": "0.1.1",
"commander": "^10.0.0",
"minimatch": "^9.0.1",
"semver": "^7.5.3"
},
"bin": {
"editorconfig": "bin/editorconfig"
},
"engines": {
"node": ">=14"
}
},
"node_modules/editorconfig/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,
"license": "MIT"
},
"node_modules/editorconfig/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/editorconfig/node_modules/commander": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/editorconfig/node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -6439,6 +6725,16 @@
"url": "https://github.com/sindresorhus/execa?sponsor=1" "url": "https://github.com/sindresorhus/execa?sponsor=1"
} }
}, },
"node_modules/expect-type": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/exsolve": { "node_modules/exsolve": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
@ -6889,6 +7185,24 @@
"uncrypto": "^0.1.3" "uncrypto": "^0.1.3"
} }
}, },
"node_modules/happy-dom": {
"version": "20.8.4",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.8.4.tgz",
"integrity": "sha512-GKhjq4OQCYB4VLFBzv8mmccUadwlAusOZOI7hC1D9xDIT5HhzkJK17c4el2f6R6C715P9xB4uiMxeKUa2nHMwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": ">=20.0.0",
"@types/whatwg-mimetype": "^3.0.2",
"@types/ws": "^8.18.1",
"entities": "^7.0.1",
"whatwg-mimetype": "^3.0.0",
"ws": "^8.18.3"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/has-flag": { "node_modules/has-flag": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -7457,6 +7771,143 @@
"jiti": "lib/jiti-cli.mjs" "jiti": "lib/jiti-cli.mjs"
} }
}, },
"node_modules/js-beautify": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz",
"integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==",
"dev": true,
"license": "MIT",
"dependencies": {
"config-chain": "^1.1.13",
"editorconfig": "^1.0.4",
"glob": "^10.4.2",
"js-cookie": "^3.0.5",
"nopt": "^7.2.1"
},
"bin": {
"css-beautify": "js/bin/css-beautify.js",
"html-beautify": "js/bin/html-beautify.js",
"js-beautify": "js/bin/js-beautify.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/js-beautify/node_modules/abbrev": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
"integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/js-beautify/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,
"license": "MIT"
},
"node_modules/js-beautify/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/js-beautify/node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/js-beautify/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC"
},
"node_modules/js-beautify/node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/js-beautify/node_modules/nopt": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
"integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
"dev": true,
"license": "ISC",
"dependencies": {
"abbrev": "^2.0.0"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/js-beautify/node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -9867,6 +10318,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
"dev": true,
"license": "ISC"
},
"node_modules/quansync": { "node_modules/quansync": {
"version": "0.2.11", "version": "0.2.11",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
@ -10549,6 +11007,13 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
"dev": true,
"license": "ISC"
},
"node_modules/signal-exit": { "node_modules/signal-exit": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@ -10676,6 +11141,13 @@
"node": ">=20.16.0" "node": ">=20.16.0"
} }
}, },
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
"dev": true,
"license": "MIT"
},
"node_modules/standard-as-callback": { "node_modules/standard-as-callback": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
@ -10922,6 +11394,16 @@
"url": "https://opencollective.com/svgo" "url": "https://opencollective.com/svgo"
} }
}, },
"node_modules/svgo/node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"node_modules/system-architecture": { "node_modules/system-architecture": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz",
@ -11300,6 +11782,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
"dev": true,
"license": "MIT"
},
"node_modules/tinyclip": { "node_modules/tinyclip": {
"version": "0.1.12", "version": "0.1.12",
"resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.12.tgz", "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.12.tgz",
@ -11335,6 +11824,16 @@
"url": "https://github.com/sponsors/SuperchupuDev" "url": "https://github.com/sponsors/SuperchupuDev"
} }
}, },
"node_modules/tinyrainbow": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
"integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -11530,6 +12029,13 @@
"node": ">=18.12.0" "node": ">=18.12.0"
} }
}, },
"node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true,
"license": "MIT"
},
"node_modules/unenv": { "node_modules/unenv": {
"version": "2.0.0-rc.24", "version": "2.0.0-rc.24",
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
@ -12409,6 +12915,88 @@
"@types/estree": "^1.0.0" "@types/estree": "^1.0.0"
} }
}, },
"node_modules/vitest": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz",
"integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "4.1.0",
"@vitest/mocker": "4.1.0",
"@vitest/pretty-format": "4.1.0",
"@vitest/runner": "4.1.0",
"@vitest/snapshot": "4.1.0",
"@vitest/spy": "4.1.0",
"@vitest/utils": "4.1.0",
"es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0",
"magic-string": "^0.30.21",
"obug": "^2.1.1",
"pathe": "^2.0.3",
"picomatch": "^4.0.3",
"std-env": "^4.0.0-rc.1",
"tinybench": "^2.9.0",
"tinyexec": "^1.0.2",
"tinyglobby": "^0.2.15",
"tinyrainbow": "^3.0.3",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0",
"why-is-node-running": "^2.3.0"
},
"bin": {
"vitest": "vitest.mjs"
},
"engines": {
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "4.1.0",
"@vitest/browser-preview": "4.1.0",
"@vitest/browser-webdriverio": "4.1.0",
"@vitest/ui": "4.1.0",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0"
},
"peerDependenciesMeta": {
"@edge-runtime/vm": {
"optional": true
},
"@opentelemetry/api": {
"optional": true
},
"@types/node": {
"optional": true
},
"@vitest/browser-playwright": {
"optional": true
},
"@vitest/browser-preview": {
"optional": true
},
"@vitest/browser-webdriverio": {
"optional": true
},
"@vitest/ui": {
"optional": true
},
"happy-dom": {
"optional": true
},
"jsdom": {
"optional": true
},
"vite": {
"optional": false
}
}
},
"node_modules/vscode-uri": { "node_modules/vscode-uri": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
@ -12447,6 +13035,13 @@
"ufo": "^1.6.1" "ufo": "^1.6.1"
} }
}, },
"node_modules/vue-component-type-helpers": {
"version": "2.2.12",
"resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.12.tgz",
"integrity": "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==",
"dev": true,
"license": "MIT"
},
"node_modules/vue-demi": { "node_modules/vue-demi": {
"version": "0.14.10", "version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
@ -12509,6 +13104,16 @@
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/whatwg-mimetype": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-url": { "node_modules/whatwg-url": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
@ -12536,6 +13141,23 @@
"node": "^18.17.0 || >=20.5.0" "node": "^18.17.0 || >=20.5.0"
} }
}, },
"node_modules/why-is-node-running": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
"dev": true,
"license": "MIT",
"dependencies": {
"siginfo": "^2.0.0",
"stackback": "0.0.2"
},
"bin": {
"why-is-node-running": "cli.js"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi": { "node_modules/wrap-ansi": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",

View File

@ -5,26 +5,33 @@
"scripts": { "scripts": {
"dev": "nuxi dev", "dev": "nuxi dev",
"build": "nuxi generate", "build": "nuxi generate",
"preview": "nuxi preview" "preview": "nuxi preview",
"test": "vitest run",
"test:watch": "vitest",
"test:cov": "vitest run --coverage"
}, },
"dependencies": { "dependencies": {
"@pinia/nuxt": "^0.5.0", "@pinia/nuxt": "^0.5.0",
"@primevue/themes": "^4.0.0", "@primevue/themes": "^4.0.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-search": "^0.15.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/addon-webgl": "^0.18.0",
"@xterm/xterm": "^5.4.0",
"guacamole-common-js": "^1.5.0", "guacamole-common-js": "^1.5.0",
"lucide-vue-next": "^0.300.0", "lucide-vue-next": "^0.300.0",
"monaco-editor": "^0.45.0", "monaco-editor": "^0.45.0",
"pinia": "^2.1.0", "pinia": "^2.1.0",
"primevue": "^4.0.0", "primevue": "^4.0.0"
"@xterm/xterm": "^5.4.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-search": "^0.15.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/addon-webgl": "^0.18.0"
}, },
"devDependencies": { "devDependencies": {
"@nuxtjs/tailwindcss": "^6.0.0", "@nuxtjs/tailwindcss": "^6.0.0",
"@pinia/testing": "^0.1.7",
"@primevue/nuxt-module": "^4.0.0", "@primevue/nuxt-module": "^4.0.0",
"@vue/test-utils": "^2.4.6",
"happy-dom": "^20.8.4",
"nuxt": "^3.10.0", "nuxt": "^3.10.0",
"typescript": "^5.3.0" "typescript": "^5.3.0",
"vitest": "^4.1.0"
} }
} }

View File

@ -0,0 +1,99 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useVault } from '../../composables/useVault'
const mockFetch = vi.mocked($fetch as ReturnType<typeof vi.fn>)
beforeEach(() => {
vi.clearAllMocks()
})
// ---------------------------------------------------------------------------
// Helper: assert no Authorization header was sent
// ---------------------------------------------------------------------------
function assertNoAuthHeader(callIndex = 0) {
const callArgs = mockFetch.mock.calls[callIndex]
const options = callArgs[1] as Record<string, unknown> | undefined
if (options && options.headers) {
const headers = options.headers as Record<string, unknown>
expect(headers).not.toHaveProperty('Authorization')
}
// If no options object at all — cookie-only, no auth header by definition
}
// ---------------------------------------------------------------------------
// SSH Keys
// ---------------------------------------------------------------------------
describe('listKeys()', () => {
it('fetches /api/ssh-keys without Authorization header', async () => {
mockFetch.mockResolvedValueOnce([])
const { listKeys } = useVault()
await listKeys()
expect(mockFetch).toHaveBeenCalledWith('/api/ssh-keys')
assertNoAuthHeader()
})
})
describe('importKey()', () => {
it('posts key data to /api/ssh-keys', async () => {
mockFetch.mockResolvedValueOnce({ id: 1 })
const { importKey } = useVault()
const payload = { name: 'my-key', privateKey: '-----BEGIN RSA PRIVATE KEY-----' }
await importKey(payload)
expect(mockFetch).toHaveBeenCalledWith('/api/ssh-keys', { method: 'POST', body: payload })
assertNoAuthHeader()
})
})
describe('deleteKey()', () => {
it('sends DELETE to /api/ssh-keys/:id', async () => {
mockFetch.mockResolvedValueOnce(undefined)
const { deleteKey } = useVault()
await deleteKey(7)
expect(mockFetch).toHaveBeenCalledWith('/api/ssh-keys/7', { method: 'DELETE' })
assertNoAuthHeader()
})
})
// ---------------------------------------------------------------------------
// Credentials
// ---------------------------------------------------------------------------
describe('listCredentials()', () => {
it('fetches /api/credentials without Authorization header', async () => {
mockFetch.mockResolvedValueOnce([])
const { listCredentials } = useVault()
await listCredentials()
expect(mockFetch).toHaveBeenCalledWith('/api/credentials')
assertNoAuthHeader()
})
})
describe('createCredential()', () => {
it('posts credential data to /api/credentials', async () => {
const cred = { label: 'prod-db', username: 'admin', password: 'hunter2' }
mockFetch.mockResolvedValueOnce({ id: 5, ...cred })
const { createCredential } = useVault()
await createCredential(cred)
expect(mockFetch).toHaveBeenCalledWith('/api/credentials', { method: 'POST', body: cred })
assertNoAuthHeader()
})
})
describe('updateCredential()', () => {
it('sends PUT to /api/credentials/:id', async () => {
mockFetch.mockResolvedValueOnce(undefined)
const { updateCredential } = useVault()
await updateCredential(5, { password: 'new-pass' })
expect(mockFetch).toHaveBeenCalledWith('/api/credentials/5', { method: 'PUT', body: { password: 'new-pass' } })
assertNoAuthHeader()
})
})
describe('deleteCredential()', () => {
it('sends DELETE to /api/credentials/:id', async () => {
mockFetch.mockResolvedValueOnce(undefined)
const { deleteCredential } = useVault()
await deleteCredential(5)
expect(mockFetch).toHaveBeenCalledWith('/api/credentials/5', { method: 'DELETE' })
assertNoAuthHeader()
})
})

View File

@ -0,0 +1,52 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useAuthStore } from '../../stores/auth.store'
// The middleware calls useAuthStore() and navigateTo() as Nuxt auto-imports.
// We override the global.useAuthStore shim (set in setup.ts) with the real
// store factory so it resolves against the active Pinia instance.
// navigateTo is already vi.fn() from setup.ts.
const mockNavigateTo = vi.mocked(navigateTo as ReturnType<typeof vi.fn>)
// Load the middleware factory.
// defineNuxtRouteMiddleware is a pass-through shim that just returns the fn.
// So `adminMiddleware` will be the inner route handler function.
let adminMiddleware: () => unknown
beforeEach(async () => {
setActivePinia(createPinia())
vi.clearAllMocks()
// Bind useAuthStore to the global so middleware can call it as an auto-import
global.useAuthStore = useAuthStore as any
// Re-import the middleware fresh each test to get the current global binding
const mod = await import('../../middleware/admin?t=' + Date.now())
adminMiddleware = mod.default
})
// ---------------------------------------------------------------------------
// admin middleware
// ---------------------------------------------------------------------------
describe('admin middleware', () => {
it('redirects non-admin users to "/"', () => {
const auth = useAuthStore()
// Non-admin user
auth.user = { id: 2, email: 'bob@example.com', displayName: 'Bob', role: 'user' }
adminMiddleware()
expect(mockNavigateTo).toHaveBeenCalledWith('/')
})
it('allows admin users through without redirecting', () => {
const auth = useAuthStore()
// Admin user
auth.user = { id: 1, email: 'alice@example.com', displayName: 'Alice', role: 'admin' }
adminMiddleware()
expect(mockNavigateTo).not.toHaveBeenCalled()
})
})

38
frontend/tests/setup.ts Normal file
View File

@ -0,0 +1,38 @@
import { vi } from 'vitest'
import { ref, computed, onMounted, watch } from 'vue'
// -------------------------------------------------------------------
// Nuxt auto-import globals
// These are injected by Nuxt at runtime but Vitest doesn't run Nuxt,
// so we shim them here so test files can import store/composable modules
// without hitting "X is not defined" errors.
// -------------------------------------------------------------------
// $fetch — Nuxt's isomorphic fetch wrapper
global.$fetch = vi.fn()
// navigateTo — Nuxt router utility
global.navigateTo = vi.fn()
// Route middleware helpers
global.defineNuxtRouteMiddleware = vi.fn((fn: Function) => fn)
// Plugin helper (used in some plugin files, not in tests directly)
global.defineNuxtPlugin = vi.fn((fn: Function) => fn)
// Page meta (used in page components)
global.definePageMeta = vi.fn()
// Vue Composition API — re-export from Vue so stores using these
// via Nuxt auto-imports resolve correctly in Vitest
global.ref = ref
global.computed = computed
global.onMounted = onMounted
global.watch = watch
// useAuthStore — needed by middleware; forward-declared here so
// admin.ts middleware can resolve it. Individual test files override
// this with the real Pinia store instance.
// (Set to undefined by default — tests that import middleware must
// call setActivePinia first and then the real store will resolve.)
global.useAuthStore = undefined as any

View File

@ -0,0 +1,165 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useAuthStore } from '../../stores/auth.store'
// $fetch and navigateTo are shimmed in tests/setup.ts as globals.
// We re-cast them here so vi.mocked() provides typed mock utilities.
const mockFetch = vi.mocked($fetch as ReturnType<typeof vi.fn>)
const mockNavigateTo = vi.mocked(navigateTo as ReturnType<typeof vi.fn>)
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
})
// ---------------------------------------------------------------------------
// login()
// ---------------------------------------------------------------------------
describe('login()', () => {
it('stores the user and returns the response on success', async () => {
const user = { id: 1, email: 'alice@example.com', displayName: 'Alice', role: 'user' }
mockFetch.mockResolvedValueOnce({ user })
const auth = useAuthStore()
const result = await auth.login('alice@example.com', 'secret')
expect(mockFetch).toHaveBeenCalledWith('/api/auth/login', {
method: 'POST',
body: { email: 'alice@example.com', password: 'secret' },
})
expect(auth.user).toEqual(user)
expect(result).toEqual({ user })
})
it('includes totpCode in body when provided', async () => {
const user = { id: 1, email: 'alice@example.com', displayName: 'Alice', role: 'user' }
mockFetch.mockResolvedValueOnce({ user })
const auth = useAuthStore()
await auth.login('alice@example.com', 'secret', '123456')
expect(mockFetch).toHaveBeenCalledWith('/api/auth/login', {
method: 'POST',
body: { email: 'alice@example.com', password: 'secret', totpCode: '123456' },
})
})
it('returns requires_totp and does NOT set user when TOTP is required', async () => {
mockFetch.mockResolvedValueOnce({ requires_totp: true })
const auth = useAuthStore()
const result = await auth.login('alice@example.com', 'secret')
expect(result).toEqual({ requires_totp: true })
expect(auth.user).toBeNull()
})
it('does NOT include an Authorization header (cookie-only auth)', async () => {
mockFetch.mockResolvedValueOnce({ user: { id: 1, email: 'a@b.com', displayName: null, role: 'user' } })
const auth = useAuthStore()
await auth.login('a@b.com', 'pass')
const callArgs = mockFetch.mock.calls[0]
const options = callArgs[1] as Record<string, unknown>
expect(options).not.toHaveProperty('headers')
})
})
// ---------------------------------------------------------------------------
// logout()
// ---------------------------------------------------------------------------
describe('logout()', () => {
it('clears user state and calls navigateTo("/login")', async () => {
mockFetch.mockResolvedValueOnce({}) // logout POST succeeds
const auth = useAuthStore()
auth.user = { id: 1, email: 'alice@example.com', displayName: 'Alice', role: 'user' }
await auth.logout()
expect(auth.user).toBeNull()
expect(mockNavigateTo).toHaveBeenCalledWith('/login')
})
it('clears user state even when the logout request fails', async () => {
mockFetch.mockRejectedValueOnce(new Error('network error'))
const auth = useAuthStore()
auth.user = { id: 1, email: 'alice@example.com', displayName: 'Alice', role: 'user' }
await auth.logout()
expect(auth.user).toBeNull()
expect(mockNavigateTo).toHaveBeenCalledWith('/login')
})
})
// ---------------------------------------------------------------------------
// fetchProfile()
// ---------------------------------------------------------------------------
describe('fetchProfile()', () => {
it('sets user on success', async () => {
const user = { id: 2, email: 'bob@example.com', displayName: 'Bob', role: 'admin' }
mockFetch.mockResolvedValueOnce(user)
const auth = useAuthStore()
await auth.fetchProfile()
expect(auth.user).toEqual(user)
expect(mockFetch).toHaveBeenCalledWith('/api/auth/profile')
})
it('sets user to null on failure', async () => {
mockFetch.mockRejectedValueOnce(new Error('401'))
const auth = useAuthStore()
auth.user = { id: 1, email: 'a@b.com', displayName: null, role: 'user' }
await auth.fetchProfile()
expect(auth.user).toBeNull()
})
})
// ---------------------------------------------------------------------------
// getWsTicket()
// ---------------------------------------------------------------------------
describe('getWsTicket()', () => {
it('returns the ticket string from the API', async () => {
mockFetch.mockResolvedValueOnce({ ticket: 'abc-xyz-ticket' })
const auth = useAuthStore()
const ticket = await auth.getWsTicket()
expect(ticket).toBe('abc-xyz-ticket')
expect(mockFetch).toHaveBeenCalledWith('/api/auth/ws-ticket', { method: 'POST' })
})
})
// ---------------------------------------------------------------------------
// getters
// ---------------------------------------------------------------------------
describe('isAuthenticated getter', () => {
it('returns false when user is null', () => {
const auth = useAuthStore()
expect(auth.isAuthenticated).toBe(false)
})
it('returns true when user is set', () => {
const auth = useAuthStore()
auth.user = { id: 1, email: 'a@b.com', displayName: null, role: 'user' }
expect(auth.isAuthenticated).toBe(true)
})
})
describe('isAdmin getter', () => {
it('returns false for non-admin role', () => {
const auth = useAuthStore()
auth.user = { id: 1, email: 'a@b.com', displayName: null, role: 'user' }
expect(auth.isAdmin).toBe(false)
})
it('returns true for admin role', () => {
const auth = useAuthStore()
auth.user = { id: 1, email: 'a@b.com', displayName: null, role: 'admin' }
expect(auth.isAdmin).toBe(true)
})
})

View File

@ -0,0 +1,124 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useConnectionStore } from '../../stores/connection.store'
const mockFetch = vi.mocked($fetch as ReturnType<typeof vi.fn>)
const makeHost = (id: number) => ({
id,
name: `host-${id}`,
hostname: `192.168.1.${id}`,
port: 22,
protocol: 'ssh' as const,
groupId: null,
credentialId: null,
tags: [],
notes: null,
color: null,
lastConnectedAt: null,
group: null,
})
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
})
// ---------------------------------------------------------------------------
// fetchHosts()
// ---------------------------------------------------------------------------
describe('fetchHosts()', () => {
it('populates hosts array and resets loading flag', async () => {
const hosts = [makeHost(1), makeHost(2)]
mockFetch.mockResolvedValueOnce(hosts)
const store = useConnectionStore()
await store.fetchHosts()
expect(store.hosts).toEqual(hosts)
expect(store.loading).toBe(false)
expect(mockFetch).toHaveBeenCalledWith('/api/hosts')
})
it('does NOT include Authorization headers (cookie-only auth)', async () => {
mockFetch.mockResolvedValueOnce([])
const store = useConnectionStore()
await store.fetchHosts()
const callArgs = mockFetch.mock.calls[0]
// fetchHosts passes no options — only the URL
expect(callArgs.length).toBe(1)
})
})
// ---------------------------------------------------------------------------
// createHost()
// ---------------------------------------------------------------------------
describe('createHost()', () => {
it('posts to /api/hosts and refreshes the list', async () => {
const newHost = makeHost(10)
// First call: POST to create, second call: GET for fetchHosts
mockFetch
.mockResolvedValueOnce(newHost) // POST
.mockResolvedValueOnce([newHost]) // GET (fetchHosts)
const store = useConnectionStore()
const result = await store.createHost({ name: 'host-10', hostname: '10.0.0.10', port: 22, protocol: 'ssh' })
expect(result).toEqual(newHost)
expect(mockFetch).toHaveBeenNthCalledWith(1, '/api/hosts', {
method: 'POST',
body: { name: 'host-10', hostname: '10.0.0.10', port: 22, protocol: 'ssh' },
})
expect(store.hosts).toEqual([newHost])
})
})
// ---------------------------------------------------------------------------
// deleteHost()
// ---------------------------------------------------------------------------
describe('deleteHost()', () => {
it('sends DELETE and refreshes the list', async () => {
mockFetch
.mockResolvedValueOnce(undefined) // DELETE
.mockResolvedValueOnce([]) // fetchHosts
const store = useConnectionStore()
await store.deleteHost(5)
expect(mockFetch).toHaveBeenNthCalledWith(1, '/api/hosts/5', { method: 'DELETE' })
expect(store.hosts).toEqual([])
})
})
// ---------------------------------------------------------------------------
// Group CRUD
// ---------------------------------------------------------------------------
describe('createGroup()', () => {
it('posts to /api/groups and refreshes the tree', async () => {
mockFetch
.mockResolvedValueOnce(undefined) // POST
.mockResolvedValueOnce([]) // fetchTree
const store = useConnectionStore()
await store.createGroup({ name: 'Dev Servers' })
expect(mockFetch).toHaveBeenNthCalledWith(1, '/api/groups', {
method: 'POST',
body: { name: 'Dev Servers' },
})
})
})
describe('deleteGroup()', () => {
it('sends DELETE and refreshes the tree', async () => {
mockFetch
.mockResolvedValueOnce(undefined) // DELETE
.mockResolvedValueOnce([]) // fetchTree
const store = useConnectionStore()
await store.deleteGroup(3)
expect(mockFetch).toHaveBeenNthCalledWith(1, '/api/groups/3', { method: 'DELETE' })
})
})

18
frontend/vitest.config.ts Normal file
View File

@ -0,0 +1,18 @@
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'happy-dom',
setupFiles: ['./tests/setup.ts'],
},
resolve: {
alias: {
'~': resolve(__dirname, '.'),
'@': resolve(__dirname, '.'),
},
},
})