diff --git a/README.md b/README.md index 84b661a..c95bc35 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,13 @@ Based on [Pastel](https://github.com/vadimdemedes/create-pastel-app) - run `npm install` - run `npx tsx ./source/cli.tsx` +### Writing Tests + +Permit CLI uses [`vitest`](https://vitest.dev/) as a tool for writing tests. It also uses [`ink-testing-library`](https://github.com/vadimdemedes/ink-testing-library) to render the `Ink` components. + +- run `npx vitest` for testing +- run `npx vitest --coverage` for code coverage. + ## CLI ``` diff --git a/package-lock.json b/package-lock.json index 687270f..66fae01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,8 +36,8 @@ "@types/react": "^18.0.32", "@typescript-eslint/eslint-plugin": "^8.12.2", "@typescript-eslint/parser": "^8.12.2", - "@vitest/coverage-istanbul": "^2.1.5", - "@vitest/coverage-v8": "^2.1.5", + "@vitest/coverage-v8": "^2.1.8", + "@vitest/ui": "^2.1.8", "chalk": "^5.2.0", "delay": "^6.0.0", "eslint": "^9.13.0", @@ -50,13 +50,12 @@ "eslint-plugin-sonarjs": "^2.0.4", "globals": "^15.11.0", "ink-testing-library": "^4.0.0", - "jsdom": "^25.0.1", "parser": "^0.1.4", "prettier": "^3.3.3", "ts-node": "^10.9.1", "typescript": "^5.6.3", "typescript-eslint": "^8.11.0", - "vitest": "^2.1.5" + "vitest": "^2.1.8" }, "engines": { "node": ">=16" @@ -3084,10 +3083,17 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.27.3.tgz", - "integrity": "sha512-EzxVSkIvCFxUd4Mgm4xR9YXrcp976qVaHnqom/Tgm+vU79k4vV4eYTjmRvGfeoW8m9LVcsAy/lGjcgVegKEhLQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz", + "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==", "cpu": [ "arm" ], @@ -3099,9 +3105,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.27.3.tgz", - "integrity": "sha512-LJc5pDf1wjlt9o/Giaw9Ofl+k/vLUaYsE2zeQGH85giX2F+wn/Cg8b3c5CDP3qmVmeO5NzwVUzQQxwZvC2eQKw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz", + "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==", "cpu": [ "arm64" ], @@ -3113,9 +3119,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.27.3.tgz", - "integrity": "sha512-OuRysZ1Mt7wpWJ+aYKblVbJWtVn3Cy52h8nLuNSzTqSesYw1EuN6wKp5NW/4eSre3mp12gqFRXOKTcN3AI3LqA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz", + "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==", "cpu": [ "arm64" ], @@ -3127,9 +3133,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.27.3.tgz", - "integrity": "sha512-xW//zjJMlJs2sOrCmXdB4d0uiilZsOdlGQIC/jjmMWT47lkLLoB1nsNhPUcnoqyi5YR6I4h+FjBpILxbEy8JRg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz", + "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==", "cpu": [ "x64" ], @@ -3141,9 +3147,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.27.3.tgz", - "integrity": "sha512-58E0tIcwZ+12nK1WiLzHOD8I0d0kdrY/+o7yFVPRHuVGY3twBwzwDdTIBGRxLmyjciMYl1B/U515GJy+yn46qw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz", + "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==", "cpu": [ "arm64" ], @@ -3155,9 +3161,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.27.3.tgz", - "integrity": "sha512-78fohrpcVwTLxg1ZzBMlwEimoAJmY6B+5TsyAZ3Vok7YabRBUvjYTsRXPTjGEvv/mfgVBepbW28OlMEz4w8wGA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz", + "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==", "cpu": [ "x64" ], @@ -3169,9 +3175,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.27.3.tgz", - "integrity": "sha512-h2Ay79YFXyQi+QZKo3ISZDyKaVD7uUvukEHTOft7kh00WF9mxAaxZsNs3o/eukbeKuH35jBvQqrT61fzKfAB/Q==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz", + "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==", "cpu": [ "arm" ], @@ -3183,9 +3189,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.27.3.tgz", - "integrity": "sha512-Sv2GWmrJfRY57urktVLQ0VKZjNZGogVtASAgosDZ1aUB+ykPxSi3X1nWORL5Jk0sTIIwQiPH7iE3BMi9zGWfkg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz", + "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==", "cpu": [ "arm" ], @@ -3197,9 +3203,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.27.3.tgz", - "integrity": "sha512-FPoJBLsPW2bDNWjSrwNuTPUt30VnfM8GPGRoLCYKZpPx0xiIEdFip3dH6CqgoT0RnoGXptaNziM0WlKgBc+OWQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz", + "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==", "cpu": [ "arm64" ], @@ -3211,9 +3217,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.27.3.tgz", - "integrity": "sha512-TKxiOvBorYq4sUpA0JT+Fkh+l+G9DScnG5Dqx7wiiqVMiRSkzTclP35pE6eQQYjP4Gc8yEkJGea6rz4qyWhp3g==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz", + "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==", "cpu": [ "arm64" ], @@ -3224,10 +3230,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz", + "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.27.3.tgz", - "integrity": "sha512-v2M/mPvVUKVOKITa0oCFksnQQ/TqGrT+yD0184/cWHIu0LoIuYHwox0Pm3ccXEz8cEQDLk6FPKd1CCm+PlsISw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz", + "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==", "cpu": [ "ppc64" ], @@ -3239,9 +3259,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.27.3.tgz", - "integrity": "sha512-LdrI4Yocb1a/tFVkzmOE5WyYRgEBOyEhWYJe4gsDWDiwnjYKjNs7PS6SGlTDB7maOHF4kxevsuNBl2iOcj3b4A==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz", + "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==", "cpu": [ "riscv64" ], @@ -3253,9 +3273,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.27.3.tgz", - "integrity": "sha512-d4wVu6SXij/jyiwPvI6C4KxdGzuZOvJ6y9VfrcleHTwo68fl8vZC5ZYHsCVPUi4tndCfMlFniWgwonQ5CUpQcA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz", + "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==", "cpu": [ "s390x" ], @@ -3267,9 +3287,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.3.tgz", - "integrity": "sha512-/6bn6pp1fsCGEY5n3yajmzZQAh+mW4QPItbiWxs69zskBzJuheb3tNynEjL+mKOsUSFK11X4LYF2BwwXnzWleA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz", + "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==", "cpu": [ "x64" ], @@ -3281,9 +3301,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.27.3.tgz", - "integrity": "sha512-nBXOfJds8OzUT1qUreT/en3eyOXd2EH5b0wr2bVB5999qHdGKkzGzIyKYaKj02lXk6wpN71ltLIaQpu58YFBoQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz", + "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==", "cpu": [ "x64" ], @@ -3295,9 +3315,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.27.3.tgz", - "integrity": "sha512-ogfbEVQgIZOz5WPWXF2HVb6En+kWzScuxJo/WdQTqEgeyGkaa2ui5sQav9Zkr7bnNCLK48uxmmK0TySm22eiuw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz", + "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==", "cpu": [ "arm64" ], @@ -3309,9 +3329,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.27.3.tgz", - "integrity": "sha512-ecE36ZBMLINqiTtSNQ1vzWc5pXLQHlf/oqGp/bSbi7iedcjcNb6QbCBNG73Euyy2C+l/fn8qKWEwxr+0SSfs3w==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz", + "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==", "cpu": [ "ia32" ], @@ -3323,9 +3343,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.27.3.tgz", - "integrity": "sha512-vliZLrDmYKyaUoMzEbMTg2JkerfBjn03KmAw9CykO0Zzkzoyd7o3iZNam/TpyWNjNT+Cz2iO3P9Smv2wgrR+Eg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz", + "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==", "cpu": [ "x64" ], @@ -3678,35 +3698,10 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@vitest/coverage-istanbul": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-2.1.5.tgz", - "integrity": "sha512-jJsS5jeHncmSvzMNE03F1pk8F9etmjzGmGyQnGMkdHdVek/bxK/3vo8Qr3e9XmVuDM3UZKOy1ObeQHgC2OxvHg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@istanbuljs/schema": "^0.1.3", - "debug": "^4.3.7", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-instrument": "^6.0.3", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magicast": "^0.3.5", - "test-exclude": "^7.0.1", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "2.1.5" - } - }, "node_modules/@vitest/coverage-v8": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.5.tgz", - "integrity": "sha512-/RoopB7XGW7UEkUndRXF87A9CwkoZAJW01pj8/3pgmDVsjMH2IKy6H1A38po9tmUlwhSyYs0az82rbKd9Yaynw==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.8.tgz", + "integrity": "sha512-2Y7BPlKH18mAZYAW1tYByudlCYrQyl5RGvnnDYJKW5tCiO5qg3KSAy3XAxcxKz900a0ZXxWtKrMuZLe3lKBpJw==", "dev": true, "license": "MIT", "dependencies": { @@ -3727,8 +3722,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "2.1.5", - "vitest": "2.1.5" + "@vitest/browser": "2.1.8", + "vitest": "2.1.8" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -3737,14 +3732,14 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.5.tgz", - "integrity": "sha512-nZSBTW1XIdpZvEJyoP/Sy8fUg0b8od7ZpGDkTUcfJ7wz/VoZAFzFfLyxVxGFhUjJzhYqSbIpfMtl/+k/dpWa3Q==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.8.tgz", + "integrity": "sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.5", - "@vitest/utils": "2.1.5", + "@vitest/spy": "2.1.8", + "@vitest/utils": "2.1.8", "chai": "^5.1.2", "tinyrainbow": "^1.2.0" }, @@ -3753,13 +3748,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.5.tgz", - "integrity": "sha512-XYW6l3UuBmitWqSUXTNXcVBUCRytDogBsWuNXQijc00dtnU/9OqpXWp4OJroVrad/gLIomAq9aW8yWDBtMthhQ==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz", + "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.5", + "@vitest/spy": "2.1.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.12" }, @@ -3780,9 +3775,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.5.tgz", - "integrity": "sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", + "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3793,13 +3788,13 @@ } }, "node_modules/@vitest/runner": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.5.tgz", - "integrity": "sha512-pKHKy3uaUdh7X6p1pxOkgkVAFW7r2I818vHDthYLvUyjRfkKOU6P45PztOch4DZarWQne+VOaIMwA/erSSpB9g==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.8.tgz", + "integrity": "sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.5", + "@vitest/utils": "2.1.8", "pathe": "^1.1.2" }, "funding": { @@ -3807,13 +3802,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.5.tgz", - "integrity": "sha512-zmYw47mhfdfnYbuhkQvkkzYroXUumrwWDGlMjpdUr4jBd3HZiV2w7CQHj+z7AAS4VOtWxI4Zt4bWt4/sKcoIjg==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz", + "integrity": "sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.5", + "@vitest/pretty-format": "2.1.8", "magic-string": "^0.30.12", "pathe": "^1.1.2" }, @@ -3822,9 +3817,9 @@ } }, "node_modules/@vitest/spy": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.5.tgz", - "integrity": "sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz", + "integrity": "sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==", "dev": true, "license": "MIT", "dependencies": { @@ -3834,14 +3829,36 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/ui": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-2.1.8.tgz", + "integrity": "sha512-5zPJ1fs0ixSVSs5+5V2XJjXLmNzjugHRyV11RqxYVR+oMcogZ9qTuSfKW+OcTV0JeFNznI83BNylzH6SSNJ1+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.8", + "fflate": "^0.8.2", + "flatted": "^3.3.1", + "pathe": "^1.1.2", + "sirv": "^3.0.0", + "tinyglobby": "^0.2.10", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "2.1.8" + } + }, "node_modules/@vitest/utils": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.5.tgz", - "integrity": "sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", + "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.5", + "@vitest/pretty-format": "2.1.8", "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" }, @@ -3903,6 +3920,8 @@ "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "debug": "^4.3.4" }, @@ -4848,6 +4867,8 @@ "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "rrweb-cssom": "^0.7.1" }, @@ -4875,6 +4896,8 @@ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" @@ -4980,7 +5003,9 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/decompress-response": { "version": "6.0.0", @@ -5256,6 +5281,8 @@ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", + "optional": true, + "peer": true, "engines": { "node": ">=0.12" }, @@ -6941,6 +6968,13 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/figlet": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.8.0.tgz", @@ -7583,6 +7617,8 @@ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "whatwg-encoding": "^3.1.1" }, @@ -7603,6 +7639,8 @@ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -7617,6 +7655,8 @@ "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "agent-base": "^7.0.2", "debug": "4" @@ -7640,6 +7680,8 @@ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -8609,7 +8651,9 @@ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/is-regex": { "version": "1.1.4", @@ -8825,23 +8869,6 @@ "node": ">=8" } }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -8966,6 +8993,8 @@ "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "cssstyle": "^4.1.0", "data-urls": "^5.0.0", @@ -9563,6 +9592,16 @@ "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==", "license": "MIT" }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9570,9 +9609,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -9672,7 +9711,9 @@ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/object-assign": { "version": "4.1.1", @@ -9953,6 +9994,8 @@ "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "entities": "^4.5.0" }, @@ -10732,9 +10775,9 @@ } }, "node_modules/rollup": { - "version": "4.27.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.27.3.tgz", - "integrity": "sha512-SLsCOnlmGt9VoZ9Ek8yBK8tAdmPHeppkw+Xa7yDlCEhDTvwYei03JlWo1fdc7YTfLZ4tD8riJCUyAgTbszk1fQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", + "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==", "dev": true, "license": "MIT", "dependencies": { @@ -10748,24 +10791,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.27.3", - "@rollup/rollup-android-arm64": "4.27.3", - "@rollup/rollup-darwin-arm64": "4.27.3", - "@rollup/rollup-darwin-x64": "4.27.3", - "@rollup/rollup-freebsd-arm64": "4.27.3", - "@rollup/rollup-freebsd-x64": "4.27.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.27.3", - "@rollup/rollup-linux-arm-musleabihf": "4.27.3", - "@rollup/rollup-linux-arm64-gnu": "4.27.3", - "@rollup/rollup-linux-arm64-musl": "4.27.3", - "@rollup/rollup-linux-powerpc64le-gnu": "4.27.3", - "@rollup/rollup-linux-riscv64-gnu": "4.27.3", - "@rollup/rollup-linux-s390x-gnu": "4.27.3", - "@rollup/rollup-linux-x64-gnu": "4.27.3", - "@rollup/rollup-linux-x64-musl": "4.27.3", - "@rollup/rollup-win32-arm64-msvc": "4.27.3", - "@rollup/rollup-win32-ia32-msvc": "4.27.3", - "@rollup/rollup-win32-x64-msvc": "4.27.3", + "@rollup/rollup-android-arm-eabi": "4.28.1", + "@rollup/rollup-android-arm64": "4.28.1", + "@rollup/rollup-darwin-arm64": "4.28.1", + "@rollup/rollup-darwin-x64": "4.28.1", + "@rollup/rollup-freebsd-arm64": "4.28.1", + "@rollup/rollup-freebsd-x64": "4.28.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.28.1", + "@rollup/rollup-linux-arm-musleabihf": "4.28.1", + "@rollup/rollup-linux-arm64-gnu": "4.28.1", + "@rollup/rollup-linux-arm64-musl": "4.28.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.28.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1", + "@rollup/rollup-linux-riscv64-gnu": "4.28.1", + "@rollup/rollup-linux-s390x-gnu": "4.28.1", + "@rollup/rollup-linux-x64-gnu": "4.28.1", + "@rollup/rollup-linux-x64-musl": "4.28.1", + "@rollup/rollup-win32-arm64-msvc": "4.28.1", + "@rollup/rollup-win32-ia32-msvc": "4.28.1", + "@rollup/rollup-win32-x64-msvc": "4.28.1", "fsevents": "~2.3.2" } }, @@ -10774,7 +10818,9 @@ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/run-applescript": { "version": "7.0.0", @@ -10883,7 +10929,9 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/saxes": { "version": "6.0.0", @@ -10891,6 +10939,8 @@ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "xmlchars": "^2.2.0" }, @@ -11072,6 +11122,21 @@ "simple-concat": "^1.0.0" } }, + "node_modules/sirv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", + "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -11553,7 +11618,9 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/synckit": { "version": "0.9.2", @@ -11691,6 +11758,48 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", + "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinygradient": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/tinygradient/-/tinygradient-1.1.5.tgz", @@ -11737,6 +11846,8 @@ "integrity": "sha512-TF+wo3MgTLbf37keEwQD0IxvOZO8UZxnpPJDg5iFGAASGxYzbX/Q0y944ATEjrfxG/pF1TWRHCPbFp49Mz1Y1w==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "tldts-core": "^6.1.62" }, @@ -11749,7 +11860,9 @@ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.62.tgz", "integrity": "sha512-ohONqbfobpuaylhqFbtCzc0dFFeNz85FVKSesgT8DS9OV3a25Yj730pTj7/dDtCqmgoCgEj6gDiU9XxgHKQlBw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -11785,12 +11898,24 @@ "node": "*" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", "dev": true, "license": "BSD-3-Clause", + "optional": true, + "peer": true, "dependencies": { "tldts": "^6.1.32" }, @@ -11804,6 +11929,8 @@ "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "punycode": "^2.3.1" }, @@ -12261,9 +12388,9 @@ } }, "node_modules/vite-node": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.5.tgz", - "integrity": "sha512-rd0QIgx74q4S1Rd56XIiL2cYEdyWn13cunYBIuqh9mpmQr7gGS0IxXoP8R6OaZtNQQLyXSWbd4rXKYUbhFpK5w==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.8.tgz", + "integrity": "sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==", "dev": true, "license": "MIT", "dependencies": { @@ -12284,19 +12411,19 @@ } }, "node_modules/vitest": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.5.tgz", - "integrity": "sha512-P4ljsdpuzRTPI/kbND2sDZ4VmieerR2c9szEZpjc+98Z9ebvnXmM5+0tHEKqYZumXqlvnmfWsjeFOjXVriDG7A==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz", + "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "2.1.5", - "@vitest/mocker": "2.1.5", - "@vitest/pretty-format": "^2.1.5", - "@vitest/runner": "2.1.5", - "@vitest/snapshot": "2.1.5", - "@vitest/spy": "2.1.5", - "@vitest/utils": "2.1.5", + "@vitest/expect": "2.1.8", + "@vitest/mocker": "2.1.8", + "@vitest/pretty-format": "^2.1.8", + "@vitest/runner": "2.1.8", + "@vitest/snapshot": "2.1.8", + "@vitest/spy": "2.1.8", + "@vitest/utils": "2.1.8", "chai": "^5.1.2", "debug": "^4.3.7", "expect-type": "^1.1.0", @@ -12308,7 +12435,7 @@ "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.1.5", + "vite-node": "2.1.8", "why-is-node-running": "^2.3.0" }, "bin": { @@ -12323,8 +12450,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.5", - "@vitest/ui": "2.1.5", + "@vitest/browser": "2.1.8", + "@vitest/ui": "2.1.8", "happy-dom": "*", "jsdom": "*" }, @@ -12415,6 +12542,8 @@ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "xml-name-validator": "^5.0.0" }, @@ -12428,6 +12557,8 @@ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, "license": "BSD-2-Clause", + "optional": true, + "peer": true, "engines": { "node": ">=12" } @@ -12438,6 +12569,8 @@ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "iconv-lite": "0.6.3" }, @@ -12451,6 +12584,8 @@ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=18" } @@ -12461,6 +12596,8 @@ "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "tr46": "^5.0.0", "webidl-conversions": "^7.0.0" @@ -12777,6 +12914,8 @@ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { "node": ">=18" } @@ -12786,7 +12925,9 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/yallist": { "version": "3.1.1", diff --git a/package.json b/package.json index 55802c5..97e3db7 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,8 @@ "@types/react": "^18.0.32", "@typescript-eslint/eslint-plugin": "^8.12.2", "@typescript-eslint/parser": "^8.12.2", - "@vitest/coverage-istanbul": "^2.1.5", - "@vitest/coverage-v8": "^2.1.5", + "@vitest/coverage-v8": "^2.1.8", + "@vitest/ui": "^2.1.8", "chalk": "^5.2.0", "delay": "^6.0.0", "eslint": "^9.13.0", @@ -59,12 +59,11 @@ "eslint-plugin-sonarjs": "^2.0.4", "globals": "^15.11.0", "ink-testing-library": "^4.0.0", - "jsdom": "^25.0.1", "parser": "^0.1.4", "prettier": "^3.3.3", "ts-node": "^10.9.1", "typescript": "^5.6.3", "typescript-eslint": "^8.11.0", - "vitest": "^2.1.5" + "vitest": "^2.1.8" } } diff --git a/source/commands/apiKey.tsx b/source/commands/apiKey.tsx index fe4c932..c71484c 100644 --- a/source/commands/apiKey.tsx +++ b/source/commands/apiKey.tsx @@ -4,7 +4,7 @@ import zod from 'zod'; import { keyAccountOption } from '../options/keychain.js'; import { KEYSTORE_PERMIT_SERVICE_NAME } from '../config.js'; -import keytar from 'keytar'; +import * as keytar from 'keytar'; export const args = zod.tuple([ zod @@ -34,7 +34,11 @@ export default function ApiKey({ args, options }: Props) { keytar .getPassword(KEYSTORE_PERMIT_SERVICE_NAME, options.keyAccount) .then(value => setReadKey(value || '')) - .catch(reason => setReadKey(`-- Failed to read key- reason ${reason}`)); + .catch(reason => + setReadKey( + `-- Failed to read key- reason ${reason instanceof Error ? reason.message : String(reason)}`, + ), + ); } }, [action, options.keyAccount]); diff --git a/source/commands/pdp/check.tsx b/source/commands/pdp/check.tsx index fb8770f..fc111b8 100644 --- a/source/commands/pdp/check.tsx +++ b/source/commands/pdp/check.tsx @@ -162,7 +162,11 @@ export default function Check({ options }: Props) { queryPDP(apiKey); }) .catch(reason => { - setError(reason); + if (reason instanceof Error) { + setError(reason.message); + } else { + setError(String(reason)); + } }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [options.keyAccount]); diff --git a/source/components/EnvSelectionWizard.tsx b/source/components/EnvSelectionWizard.tsx deleted file mode 100644 index e1f1db1..0000000 --- a/source/components/EnvSelectionWizard.tsx +++ /dev/null @@ -1,209 +0,0 @@ -// import { Text } from 'ink'; -// import React, { useCallback, useEffect, useState } from 'react'; -// import Spinner from 'ink-spinner'; -// import SelectInput from 'ink-select-input'; -// import useAuth, { TokenType } from '../hooks/useAuth.js'; -// import { apiCall } from '../lib/api.js'; -// import WorkspaceSelector from './WorkspaceSelector.js'; -// // Import { apiCall } from '../lib/api.js'; - -// type EnvSelectionProps = { -// readonly workspace: string; -// readonly project: string; -// readonly environment: string; -// readonly onEnvSelected: ( -// workspace: Item | undefined, -// project: Item | undefined, -// environment: Item | undefined, -// ) => void; -// readonly onError: (error: string) => void; -// }; - -// export default function EnvSelectionWizard({ -// workspace, -// project, -// environment, -// onEnvSelected, -// onError, -// }: EnvSelectionProps) { -// const { -// authToken, -// authCookie, -// loadingAuth, -// type, -// saveAuthTokens, -// reloadTokens, -// } = useAuth(); -// const [activeWorkspace, setActiveWorkspace] = useState< -// Item | undefined -// >(); -// const [activeProject, setActiveProject] = useState< -// Item | undefined -// >(); -// const [activeEnvironment, setActiveEnvironment] = useState< -// Item | undefined -// >(); -// const [state, setState] = useState< -// 'workspace' | 'project' | 'environment' | 'selected' -// >('workspace'); -// const [workspaces, setWorkspaces] = useState>>([]); -// const [projects, setProjects] = useState>>([]); -// const [environments, setEnvironments] = useState>>([]); - -// const handleEnvironmentSelection = useCallback( -// (env: Item) => { -// setActiveEnvironment(env); -// setState('selected'); -// onEnvSelected(activeWorkspace, activeProject, activeEnvironment); -// }, -// [onEnvSelected, activeEnvironment, activeProject, activeWorkspace], -// ); - -// const fetchEnvironments = useCallback(async () => { -// if (!authToken) { -// return; -// } - -// const { response: items, status } = await apiCall( -// `v2/projects/${activeProject?.value}/envs`, -// authToken ?? '', -// authCookie, -// ); - -// const environments: Array> = items.map((w: any) => ({ -// label: w.name, -// value: w.id, -// })); - -// if (status === 200) { -// if (environments.length === 1 && environments[0]) { -// handleEnvironmentSelection(environments[0]); -// } else if (environment) { -// const selectedEnvironment = environments.find( -// (w: any) => w.id === environments, -// ); -// if (selectedEnvironment) { -// handleEnvironmentSelection(selectedEnvironment); -// } -// } - -// setEnvironments(environments); -// } else { -// onError('Failed to fetch organizations'); -// } -// }, [ -// activeProject?.value, -// authCookie, -// authToken, -// environment, -// handleEnvironmentSelection, -// onError, -// ]); - -// const handleProjectSelection = useCallback( -// async (p: Item) => { -// setActiveProject(p); -// setState('environment'); -// await fetchEnvironments(); -// }, -// [fetchEnvironments], -// ); - -// const fetchProjects = useCallback(async () => { -// const { response: items, status } = await apiCall( -// `v2/projects`, -// authToken ?? '', -// authCookie, -// ); - -// const projects: Array> = items.map((w: any) => ({ -// label: w.name, -// value: w.id, -// })); - -// if (status === 200) { -// if (projects.length === 1 && projects[0]) { -// handleProjectSelection(projects[0]); -// } else if (project) { -// const selectedProject = projects.find((w: any) => w.id === projects); -// if (selectedProject) { -// handleProjectSelection(selectedProject); -// } -// } - -// setProjects(projects); -// } else { -// onError('Failed to fetch projects'); -// } -// }, [authCookie, authToken, handleProjectSelection, onError, project]); - -// const handleWorkspaceSelection = useCallback( -// async (ws: Item) => { -// if (type === TokenType.AccessToken) { -// try { -// const { headers } = await apiCall( -// `auth/switch_org/${ws.value}`, -// authToken ?? '', -// authCookie, -// 'POST', -// ); -// await saveAuthTokens(authToken ?? '', headers.getSetCookie()[0]); -// } catch (error) { -// console.log(error); -// } -// } - -// setActiveWorkspace(ws); -// setState('project'); -// await fetchProjects(); -// }, -// [authCookie, authToken, saveAuthTokens, type, fetchProjects], -// ); - -// useEffect(() => { -// console.log(loadingAuth, authToken); -// if (workspace.length === 0) { -// reloadTokens(); -// } - -// if (!loadingAuth && workspaces?.length === 0) { -// fetchWorkspaces(); -// } -// }, [fetchWorkspaces, loadingAuth, workspaces, reloadTokens]); - -// return ( -// <> -// {state === 'workspace' && } -// {state === 'project' && activeWorkspace && -// (projects && projects.length > 0 ? ( -// <> -// Select a project -// -// -// ) : ( -// -// Loading Projects -// -// ))} -// {state === 'environment' && activeEnvironment && -// (environments && environments.length > 0 ? ( -// <> -// Select an environment -// -// -// ) : ( -// -// Loading Environments -// -// ))} -// {state === 'selected' && ( -// -// Selected Environment: {activeEnvironment?.label} -// -// )} -// -// ); -// } diff --git a/source/components/LoginFlow.tsx b/source/components/LoginFlow.tsx index 3b33c17..132e42f 100644 --- a/source/components/LoginFlow.tsx +++ b/source/components/LoginFlow.tsx @@ -43,7 +43,7 @@ const LoginFlow: React.FC = ({ } onSuccess(token, headers.getSetCookie()[0] ?? ''); } catch (error: unknown) { - onError(`Unexpected error during authentication. ${error}`); + onError(`Unexpected error during authentication. ${error as string}`); return; } } diff --git a/source/components/WorkspaceSelector.tsx b/source/components/WorkspaceSelector.tsx deleted file mode 100644 index 7703feb..0000000 --- a/source/components/WorkspaceSelector.tsx +++ /dev/null @@ -1,70 +0,0 @@ -// import { Text } from "ink"; -// import SelectInput from "ink-select-input"; -// import Spinner from "ink-spinner"; -// import React, { useEffect } from "react"; - -// export default function WorkspaceSelector({ -// onWorkspaceSelection: (Item) -// }) { -// const [authToken, loadingAuth, authCookie] = useAuth(); -// const [workspaces, setWorkspaces] = useState([]); - -// const fetchWorkspaces = useCallback(async () => { -// if (!authToken) { -// return; -// } - -// try { -// const { response: items, status } = await apiCall( -// `v2/orgs`, -// authToken ?? '', -// authCookie, -// ); - -// if (!items.map) { -// return; -// } - -// const workspaces: Array> = items.map((w: any) => ({ -// label: w.name, -// value: w.id, -// })); - -// if (status === 200) { -// if (workspaces.length === 1 && workspaces[0]) { -// handleWorkspaceSelection(workspaces[0]); -// } else if (workspace) { -// const selectedWorkspace = workspaces.find( -// (w: any) => w.id === workspace, -// ); -// if (selectedWorkspace) { -// handleWorkspaceSelection(selectedWorkspace); -// } -// } - -// setWorkspaces(workspaces); -// } else { -// onError('Failed to fetch workspaces'); -// } -// } catch (error) { -// onError(error as string); -// } -// }, [authCookie, authToken, workspace, handleWorkspaceSelection, onError]); - -// useEffect(() => { -// }, []); - -// return ((workspaces && workspaces.length > 0 ? ( -// <> -// Select a workspace -// -// -// ) : ( -// -// Loading Workspaces -// -// ))}) -// } diff --git a/source/lib/auth.ts b/source/lib/auth.ts index 4f2be4b..ea96fe3 100644 --- a/source/lib/auth.ts +++ b/source/lib/auth.ts @@ -1,7 +1,7 @@ import { createHash, randomBytes } from 'node:crypto'; import * as http from 'node:http'; import open from 'open'; -import pkg from 'keytar'; +import * as pkg from 'keytar'; import { DEFAULT_PERMIT_KEYSTORE_ACCOUNT, KEYSTORE_PERMIT_SERVICE_NAME, diff --git a/source/lib/gitops/utils.ts b/source/lib/gitops/utils.ts index 8f2c064..b814b75 100644 --- a/source/lib/gitops/utils.ts +++ b/source/lib/gitops/utils.ts @@ -87,7 +87,7 @@ async function configurePermit( status: gitConfigResponse.status, }; } else { - throw new Error('Invalid Configuration ' + response); + throw new Error('Invalid Configuration '); } } diff --git a/tests/EnvCopy.test.tsx b/tests/EnvCopy.test.tsx index 8724ee9..b392952 100644 --- a/tests/EnvCopy.test.tsx +++ b/tests/EnvCopy.test.tsx @@ -26,7 +26,7 @@ vi.mock('../source/components/EnvironmentSelection.js', () => ({ beforeEach(() => { vi.restoreAllMocks(); - vi.spyOn(process, 'exit').mockImplementation((code) => { + vi.spyOn(process, 'exit').mockImplementation(code => { console.warn(`Mocked process.exit(${code}) called`); }); }); @@ -38,19 +38,23 @@ afterEach(() => { describe('Copy Component', () => { it('should handle successful environment copy flow using arguments', async () => { vi.mocked(useApiKeyApi).mockReturnValue({ - validateApiKeyScope: vi.fn(() => Promise.resolve({ - valid: true, - scope: { - project_id: 'proj1', - }, - error: null, - })), + validateApiKeyScope: vi.fn(() => + Promise.resolve({ + valid: true, + scope: { + project_id: 'proj1', + }, + error: null, + }), + ), }); vi.mocked(useEnvironmentApi).mockReturnValue({ - copyEnvironment: vi.fn(() => Promise.resolve({ - error: null, - })), + copyEnvironment: vi.fn(() => + Promise.resolve({ + error: null, + }), + ), }); // @ts-ignore @@ -63,7 +67,16 @@ describe('Copy Component', () => { return null; }); - const { lastFrame } = render(); + const { lastFrame } = render( + , + ); await delay(100); // Allow name input expect(lastFrame()).toMatch(/Environment copied successfully/); @@ -71,10 +84,12 @@ describe('Copy Component', () => { it('should handle invalid API key gracefully', async () => { vi.mocked(useApiKeyApi).mockReturnValue({ - validateApiKeyScope: vi.fn(() => Promise.resolve({ - valid: false, - error: 'Invalid API Key', - })), + validateApiKeyScope: vi.fn(() => + Promise.resolve({ + valid: false, + error: 'Invalid API Key', + }), + ), }); const { lastFrame } = render(); @@ -87,19 +102,23 @@ describe('Copy Component', () => { it('should handle successful environment copy flow using the wizard', async () => { vi.mocked(useApiKeyApi).mockReturnValue({ - validateApiKeyScope: vi.fn(() => Promise.resolve({ - valid: true, - scope: { - project_id: 'proj1', - }, - error: null, - })), + validateApiKeyScope: vi.fn(() => + Promise.resolve({ + valid: true, + scope: { + project_id: 'proj1', + }, + error: null, + }), + ), }); vi.mocked(useEnvironmentApi).mockReturnValue({ - copyEnvironment: vi.fn(() => Promise.resolve({ - error: null, - })), + copyEnvironment: vi.fn(() => + Promise.resolve({ + error: null, + }), + ), }); vi.mocked(EnvironmentSelection).mockImplementation(({ onComplete }) => { @@ -107,12 +126,14 @@ describe('Copy Component', () => { { label: 'Org1', value: 'org1' }, { label: 'Proj1', value: 'proj1' }, { label: 'Env1', value: 'env1' }, - 'secret' + 'secret', ); return null; }); - const { lastFrame, stdin } = render(); + const { lastFrame, stdin } = render( + , + ); await delay(50); // Allow environment selection diff --git a/tests/EnvMember.test.tsx b/tests/EnvMember.test.tsx index 39f8efa..742deb8 100644 --- a/tests/EnvMember.test.tsx +++ b/tests/EnvMember.test.tsx @@ -26,7 +26,7 @@ vi.mock('../source/components/EnvironmentSelection.js', () => ({ beforeEach(() => { vi.restoreAllMocks(); - vi.spyOn(process, 'exit').mockImplementation((code) => { + vi.spyOn(process, 'exit').mockImplementation(code => { console.warn(`Mocked process.exit(${code}) called`); }); }); @@ -40,32 +40,38 @@ const enter = '\r'; describe('Member Component', () => { it('should handle successful member invite flow', async () => { vi.mocked(useApiKeyApi).mockReturnValue({ - validateApiKeyScope: vi.fn(() => Promise.resolve({ - valid: true, - scope: { - organization_id: 'org1', - project_id: 'proj1', - }, - error: null, - })), + validateApiKeyScope: vi.fn(() => + Promise.resolve({ + valid: true, + scope: { + organization_id: 'org1', + project_id: 'proj1', + }, + error: null, + }), + ), }); vi.mocked(useMemberApi).mockReturnValue({ - inviteNewMember: vi.fn(() => Promise.resolve({ - error: null, - })), + inviteNewMember: vi.fn(() => + Promise.resolve({ + error: null, + }), + ), }); vi.mocked(EnvironmentSelection).mockImplementation(({ onComplete }) => { onComplete( { label: 'Org1', value: 'org1' }, { label: 'Proj1', value: 'proj1' }, - { label: 'Env1', value: 'env1' } + { label: 'Env1', value: 'env1' }, ); return null; }); - const { lastFrame, stdin } = render(); + const { lastFrame, stdin } = render( + , + ); await delay(50); // Allow environment selection @@ -74,20 +80,24 @@ describe('Member Component', () => { stdin.write(enter); await delay(50); stdin.write(enter); - await delay(50); // Allow role selection + await delay(100); // Allow role selection expect(lastFrame()).toMatch(/User Invited Successfully/); }); it('should handle invalid API key gracefully', async () => { vi.mocked(useApiKeyApi).mockReturnValue({ - validateApiKeyScope: vi.fn(() => Promise.resolve({ - valid: false, - error: 'Invalid API Key', - })), + validateApiKeyScope: vi.fn(() => + Promise.resolve({ + valid: false, + error: 'Invalid API Key', + }), + ), }); - const { lastFrame } = render(); + const { lastFrame } = render( + , + ); await delay(50); // Allow async operations to complete diff --git a/tests/EnvSelect.test.tsx b/tests/EnvSelect.test.tsx index 38195d3..51c0704 100644 --- a/tests/EnvSelect.test.tsx +++ b/tests/EnvSelect.test.tsx @@ -31,7 +31,7 @@ vi.mock('../source/lib/auth.js', () => ({ beforeEach(() => { vi.restoreAllMocks(); - vi.spyOn(process, 'exit').mockImplementation((code) => { + vi.spyOn(process, 'exit').mockImplementation(code => { console.warn(`Mocked process.exit(${code}) called`); }); }); @@ -49,7 +49,9 @@ describe('Select Component', () => { it('should redirect to login when no API key is provided', async () => { // Mock the Login component - vi.mocked(Login).mockImplementation(() => Mocked Login Component); + vi.mocked(Login).mockImplementation(() => ( + Mocked Login Component + )); const { lastFrame } = render(); + const { lastFrame } = render( + ); + + await delay(100); // Allow async operations to complete + + expect(lastFrame()).toMatch(/Failed to save token/); + expect(process.exit).toHaveBeenCalledWith(1); + }); + it('handle login successs', async () => { + vi.mocked(useApiKeyApi).mockReturnValue({ + validateApiKey: vi.fn(() => true), + }); + vi.mocked(EnvironmentSelection).mockImplementation(({ onComplete }) => { + onComplete( + { label: 'Org1', value: 'org1' }, + { label: 'Proj1', value: 'proj1' }, + { label: 'Env1', value: 'env1' }, + 'secret_token', + ); + return null; + }); + const { lastFrame } = render(); + await delay(100); // Allow async operations to complete + expect(lastFrame()).toMatch(/Environment: Env1 selected successfully/); + }); }); diff --git a/tests/LoginFlow.test.tsx b/tests/LoginFlow.test.tsx index 7a0f2d3..dcfbf48 100644 --- a/tests/LoginFlow.test.tsx +++ b/tests/LoginFlow.test.tsx @@ -37,7 +37,11 @@ describe('LoginFlow Component', () => { const onError = vi.fn(); const { lastFrame } = render( - + , ); await delay(50); // Allow async operations to complete @@ -54,14 +58,18 @@ describe('LoginFlow Component', () => { const onError = vi.fn(); const { lastFrame } = render( - + , ); await delay(50); // Allow async operations to complete expect(onSuccess).not.toHaveBeenCalled(); expect(onError).toHaveBeenCalledWith( - 'Invalid API Key. Please provide a valid API Key or leave it blank to use browser authentication.' + 'Invalid API Key. Please provide a valid API Key or leave it blank to use browser authentication.', ); expect(lastFrame()).not.toMatch(/Logging in.../); }); @@ -77,14 +85,16 @@ describe('LoginFlow Component', () => { getSetCookie: () => ['cookie_value'], }, error: null, - }) + }), ), }); const onSuccess = vi.fn(); const onError = vi.fn(); - const { lastFrame } = render(); + const { lastFrame } = render( + , + ); await delay(50); // Allow async operations to complete @@ -96,17 +106,23 @@ describe('LoginFlow Component', () => { it('should handle browser authentication error and call onError', async () => { vi.mocked(browserAuth).mockResolvedValue('verifier'); - vi.mocked(authCallbackServer).mockRejectedValue(new Error('Callback failed')); + vi.mocked(authCallbackServer).mockRejectedValue( + new Error('Callback failed'), + ); const onSuccess = vi.fn(); const onError = vi.fn(); - const { lastFrame } = render(); + const { lastFrame } = render( + , + ); await delay(50); // Allow async operations to complete expect(onSuccess).not.toHaveBeenCalled(); - expect(onError).toHaveBeenCalledWith('Unexpected error during authentication. Error: Callback failed'); + expect(onError).toHaveBeenCalledWith( + 'Unexpected error during authentication. Error: Callback failed', + ); expect(lastFrame()).not.toMatch(/Logging in.../); }); @@ -119,20 +135,22 @@ describe('LoginFlow Component', () => { Promise.resolve({ headers: null, error: 'Network error', - }) + }), ), }); const onSuccess = vi.fn(); const onError = vi.fn(); - const { lastFrame } = render(); + const { lastFrame } = render( + , + ); await delay(50); // Allow async operations to complete expect(onSuccess).not.toHaveBeenCalled(); expect(onError).toHaveBeenCalledWith( - 'Login failed. Reason: Network error. Please check your network connection and try again.' + 'Login failed. Reason: Network error. Please check your network connection and try again.', ); expect(lastFrame()).not.toMatch(/Logging in.../); }); diff --git a/tests/OPAPolicy.test.tsx b/tests/OPAPolicy.test.tsx new file mode 100644 index 0000000..7b3427c --- /dev/null +++ b/tests/OPAPolicy.test.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { describe, it, expect, vi } from 'vitest'; +import { render } from 'ink-testing-library'; +import Policy from '../source/commands/opa/policy'; +import delay from 'delay'; +import * as keytar from "keytar" +global.fetch = vi.fn(); +const enter = '\r'; +vi.mock("keytar",()=>({ + getPassword: vi.fn(), + setPassword: vi.fn(), + deletePassword:vi.fn() +})) + +describe('OPA Policy Command', () => { + it('should render the policy command', async () => { + const options = { + serverUrl: 'http://localhost:8181', + keyAccount: 'testAccount', + apiKey: 'permit_key_'.concat('a'.repeat(97)), + }; + (fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => ({ + result: [ + { id: 'policy1', name: 'policyName1' }, + { id: 'policy2', name: 'policyName2' }, + ], + }), + status: 200, + }); + const { stdin, lastFrame } = render(); + expect(lastFrame()?.toString()).toMatch( + 'Listing Policies on Opa Server=http://localhost:8181', + ); + await delay(50); + expect(lastFrame()?.toString()).toMatch('Showing 2 of 2 policies:'); + expect(lastFrame()?.toString()).toMatch('policy1'); + expect(lastFrame()?.toString()).toMatch('policy2'); + stdin.write(enter); + }); + it('should render the policy command with error', async () => { + const options = { + serverUrl: 'http://localhost:8181', + keyAccount: 'testAccount', + }; + (fetch as any).mockRejectedValueOnce(new Error('Error')); + const { lastFrame } = render(); + expect(lastFrame()?.toString()).toMatch( + 'Listing Policies on Opa Server=http://localhost:8181', + ); + await delay(50); + expect(lastFrame()?.toString()).toMatch(/Request failed:/); + }); +}); diff --git a/tests/PDPCheck.test.tsx b/tests/PDPCheck.test.tsx new file mode 100644 index 0000000..cfa1d99 --- /dev/null +++ b/tests/PDPCheck.test.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { render } from 'ink-testing-library'; +import { describe, vi, it, expect, afterEach } from 'vitest'; +import delay from 'delay'; +import Check from '../source/commands/pdp/check'; +import * as keytar from 'keytar'; + +global.fetch = vi.fn(); +vi.mock('keytar', () => ({ + getPassword: vi.fn().mockResolvedValue('permit_key_a'.concat('a').repeat(97)), +})); +describe('PDP Check Component', () => { + afterEach(() => { + // Clear mock calls after each test + vi.clearAllMocks(); + }); + it('should render with the given options', async () => { + (fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => ({ allow: true }), + }); + const options = { + user: 'testUser', + resource: 'testResource', + action: 'testAction', + tenant: 'testTenant', + keyAccount: 'testKeyAccount', + }; + + const { lastFrame } = render(); + expect(lastFrame()).toContain( + `Checking user="testUser"action=testAction resource=testResourceat tenant=testTenant`, + ); + await delay(50); + console.log(lastFrame()); + expect(lastFrame()?.toString()).toContain('ALLOWED'); + }); + it('should render with the given options', async () => { + (fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => ({ allow: false }), + }); + const options = { + user: 'testUser', + resource: 'testResource', + action: 'testAction', + tenant: 'testTenant', + keyAccount: 'testKeyAccount', + }; + + const { lastFrame } = render(); + expect(lastFrame()).toContain( + `Checking user="testUser"action=testAction resource=testResourceat tenant=testTenant`, + ); + await delay(50); + expect(lastFrame()?.toString()).toContain('DENIED'); + }); + it('should render with the given options', async () => { + (fetch as any).mockResolvedValueOnce({ + ok: false, + text: async () => 'Error', + }); + const options = { + user: 'testUser', + resource: 'testResource', + action: 'testAction', + tenant: 'testTenant', + keyAccount: 'testKeyAccount', + }; + + const { lastFrame } = render(); + expect(lastFrame()).toContain( + `Checking user="testUser"action=testAction resource=testResourceat tenant=testTenant`, + ); + await delay(50); + expect(lastFrame()?.toString()).toContain('Error'); + }); + it('should render with the given options with multiple resource', async () => { + (fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => ({ allow: true }), + }); + const options = { + user: 'testUser', + resource: 'testResourceType: testRecsourceKey', + action: 'testAction', + tenant: 'testTenant', + keyAccount: 'testKeyAccount', + }; + + const { lastFrame } = render(); + expect(lastFrame()) + .toContain(`Checking user="testUser"action=testAction resource=testResourceType: testRecsourceKeyat +tenant=testTenant`); + await delay(50); + expect(lastFrame()?.toString()).toContain('ALLOWED'); + }); +}); diff --git a/tests/PDPRun.test.tsx b/tests/PDPRun.test.tsx new file mode 100644 index 0000000..2723c8d --- /dev/null +++ b/tests/PDPRun.test.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import {vi, describe, expect, it } from 'vitest'; +import { render } from 'ink-testing-library'; +import Run from '../source/commands/pdp/run'; +import * as keytar from "keytar" + +vi.mock("keytar",()=>({ + getPassword: vi.fn(), + setPassword: vi.fn(), + deletePassword:vi.fn() +})) + +describe('PDP Run', () => { + it('Should render the PDP Run command', () => { + const {getPassword} = keytar; + (getPassword as any).mockResolvedValueOnce("permit_key_".concat("a".repeat(97))) + const { lastFrame } = render(); + expect(lastFrame()?.toString()).toMatch(/Loading Token/); + }); +}); diff --git a/tests/SelectEnvironment.test.tsx b/tests/SelectEnvironment.test.tsx index fcd8028..5151c61 100644 --- a/tests/SelectEnvironment.test.tsx +++ b/tests/SelectEnvironment.test.tsx @@ -36,7 +36,7 @@ describe('SelectEnvironment Component', () => { activeProject={activeProject} onComplete={vi.fn()} onError={vi.fn()} - /> + />, ); expect(lastFrame()).toMatch(/Loading Environments.../); @@ -66,7 +66,7 @@ describe('SelectEnvironment Component', () => { activeProject={activeProject} onComplete={onComplete} onError={vi.fn()} - /> + />, ); await delay(50); // Allow async operation to complete @@ -82,7 +82,10 @@ describe('SelectEnvironment Component', () => { await delay(50); expect(onComplete).toHaveBeenCalledOnce(); - expect(onComplete).toHaveBeenCalledWith({ label: 'Environment 2', value: 'env2' }); + expect(onComplete).toHaveBeenCalledWith({ + label: 'Environment 2', + value: 'env2', + }); }); it('should handle errors when fetching environments fails', async () => { @@ -105,14 +108,15 @@ describe('SelectEnvironment Component', () => { cookie="test_cookie" activeProject={activeProject} onComplete={vi.fn()} - onError={onError} />, + onError={onError} + />, ); await delay(50); // Allow async operation to complete expect(onError).toHaveBeenCalledOnce(); expect(onError).toHaveBeenCalledWith( - 'Failed to load environments for project "Project 1". Reason: Network error. Please check your network connection or credentials and try again.' + 'Failed to load environments for project "Project 1". Reason: Network error. Please check your network connection or credentials and try again.', ); }); @@ -137,7 +141,7 @@ describe('SelectEnvironment Component', () => { activeProject={activeProject} onComplete={vi.fn()} onError={onError} - /> + />, ); await delay(50); // Allow async operation to complete diff --git a/tests/SelectOrganisation.test.tsx b/tests/SelectOrganisation.test.tsx index ffacf56..937aee9 100644 --- a/tests/SelectOrganisation.test.tsx +++ b/tests/SelectOrganisation.test.tsx @@ -21,9 +21,11 @@ describe('SelectOrganization Component', () => { { id: 'org2', name: 'Organization 2' }, ], error: null, - }) + }), ); - (useOrganisationApi as ReturnType).mockReturnValue({ getOrgs: mockGetOrgs }); + (useOrganisationApi as ReturnType).mockReturnValue({ + getOrgs: mockGetOrgs, + }); const { lastFrame } = render( { cookie="test_cookie" onComplete={vi.fn()} onError={vi.fn()} - /> + />, ); expect(lastFrame()).toMatch(/Loading Organizations.../); @@ -45,9 +47,11 @@ describe('SelectOrganization Component', () => { { id: 'org2', name: 'Organization 2' }, ], error: null, - }) + }), ); - (useOrganisationApi as ReturnType).mockReturnValue({ getOrgs: mockGetOrgs }); + (useOrganisationApi as ReturnType).mockReturnValue({ + getOrgs: mockGetOrgs, + }); const onComplete = vi.fn(); const { stdin, lastFrame } = render( @@ -56,7 +60,7 @@ describe('SelectOrganization Component', () => { cookie="test_cookie" onComplete={onComplete} onError={vi.fn()} - /> + />, ); await delay(50); // Allow async operation to complete @@ -72,7 +76,10 @@ describe('SelectOrganization Component', () => { await delay(50); expect(onComplete).toHaveBeenCalledOnce(); - expect(onComplete).toHaveBeenCalledWith({ label: 'Organization 2', value: 'org2' }); + expect(onComplete).toHaveBeenCalledWith({ + label: 'Organization 2', + value: 'org2', + }); }); it('should handle errors when fetching organizations fails', async () => { @@ -80,9 +87,11 @@ describe('SelectOrganization Component', () => { Promise.resolve({ response: null, error: 'Network error', - }) + }), ); - (useOrganisationApi as ReturnType).mockReturnValue({ getOrgs: mockGetOrgs }); + (useOrganisationApi as ReturnType).mockReturnValue({ + getOrgs: mockGetOrgs, + }); const onError = vi.fn(); render( @@ -90,14 +99,15 @@ describe('SelectOrganization Component', () => { accessToken="test_token" cookie="test_cookie" onComplete={vi.fn()} - onError={onError} />, + onError={onError} + />, ); await delay(50); // Allow async operation to complete expect(onError).toHaveBeenCalledOnce(); expect(onError).toHaveBeenCalledWith( - 'Failed to load organizations. Reason: Network error. Please check your network connection or credentials and try again.' + 'Failed to load organizations. Reason: Network error. Please check your network connection or credentials and try again.', ); }); @@ -109,9 +119,11 @@ describe('SelectOrganization Component', () => { { id: 'org2', name: 'Organization 2' }, ], error: null, - }) + }), ); - (useOrganisationApi as ReturnType).mockReturnValue({ getOrgs: mockGetOrgs }); + (useOrganisationApi as ReturnType).mockReturnValue({ + getOrgs: mockGetOrgs, + }); const onComplete = vi.fn(); const { lastFrame } = render( @@ -121,13 +133,16 @@ describe('SelectOrganization Component', () => { workspace="Organization 1" onComplete={onComplete} onError={vi.fn()} - /> + />, ); await delay(50); // Allow async operation to complete expect(onComplete).toHaveBeenCalledOnce(); - expect(onComplete).toHaveBeenCalledWith({ label: 'Organization 1', value: 'org1' }); + expect(onComplete).toHaveBeenCalledWith({ + label: 'Organization 1', + value: 'org1', + }); expect(lastFrame()).not.toMatch(/Select an organization/); }); @@ -139,9 +154,11 @@ describe('SelectOrganization Component', () => { { id: 'org2', name: 'Organization 2' }, ], error: null, - }) + }), ); - (useOrganisationApi as ReturnType).mockReturnValue({ getOrgs: mockGetOrgs }); + (useOrganisationApi as ReturnType).mockReturnValue({ + getOrgs: mockGetOrgs, + }); const onError = vi.fn(); const { lastFrame } = render( @@ -151,14 +168,14 @@ describe('SelectOrganization Component', () => { workspace="Unknown Organization" onComplete={vi.fn()} onError={onError} - /> + />, ); await delay(50); // Allow async operation to complete expect(onError).toHaveBeenCalledOnce(); expect(onError).toHaveBeenCalledWith( - 'Organization "Unknown Organization" not found. Please ensure the name is correct and try again.' + 'Organization "Unknown Organization" not found. Please ensure the name is correct and try again.', ); expect(lastFrame()).not.toMatch(/Select an organization/); }); diff --git a/tests/SelectProject.test.tsx b/tests/SelectProject.test.tsx index f8310cd..ed39c41 100644 --- a/tests/SelectProject.test.tsx +++ b/tests/SelectProject.test.tsx @@ -33,7 +33,7 @@ describe('SelectProject Component', () => { cookie="test_cookie" onComplete={vi.fn()} onError={vi.fn()} - /> + />, ); expect(lastFrame()).toMatch(/Loading Projects.../); @@ -60,7 +60,7 @@ describe('SelectProject Component', () => { cookie="test_cookie" onComplete={onComplete} onError={vi.fn()} - /> + />, ); await delay(50); // Allow async operation to complete @@ -76,7 +76,10 @@ describe('SelectProject Component', () => { await delay(50); expect(onComplete).toHaveBeenCalledOnce(); - expect(onComplete).toHaveBeenCalledWith({ label: 'Project 2', value: 'proj2' }); + expect(onComplete).toHaveBeenCalledWith({ + label: 'Project 2', + value: 'proj2', + }); }); it('should handle errors when fetching projects fails', async () => { @@ -96,14 +99,15 @@ describe('SelectProject Component', () => { accessToken="test_token" cookie="test_cookie" onComplete={vi.fn()} - onError={onError} />, + onError={onError} + />, ); await delay(50); // Allow async operation to complete expect(onError).toHaveBeenCalledOnce(); expect(onError).toHaveBeenCalledWith( - 'Failed to load projects. Reason: Network error. Please check your network connection or credentials and try again.' + 'Failed to load projects. Reason: Network error. Please check your network connection or credentials and try again.', ); }); @@ -125,7 +129,7 @@ describe('SelectProject Component', () => { cookie="test_cookie" onComplete={vi.fn()} onError={onError} - /> + />, ); await delay(50); // Allow async operation to complete diff --git a/tests/apiKey.test.tsx b/tests/apiKey.test.tsx new file mode 100644 index 0000000..ea87b1d --- /dev/null +++ b/tests/apiKey.test.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { render } from 'ink-testing-library'; +import ApiKey from '../source/commands/apiKey'; +import { vi, describe, it, expect } from 'vitest'; +import delay from 'delay'; +import * as keytar from 'keytar'; + +vi.mock('keytar', () => ({ + setPassword: vi.fn(), + getPassword: vi.fn(), + deletePassword: vi.fn(), +})); + +describe('ApiKey', () => { + it('Should save the key', () => { + const permitKey = 'permit_key_'.concat('a'.repeat(97)); + const { lastFrame } = render( + , + ); + expect(lastFrame()).toMatch(/Key saved to secure key store./); + }); + it('Should validate the key', () => { + const { getPassword } = keytar; + const permitKey = 'permit_key_'.concat('a'.repeat(97)); + (getPassword as any).mockResolvedValueOnce(permitKey); + const { lastFrame } = render( + , + ); + expect(lastFrame()).toMatch(/Key is valid./); + }); + it('Should read the key', async () => { + const permitKey = 'permit_key_'.concat('a'.repeat(97)); + const { getPassword } = keytar; + (getPassword as any).mockResolvedValueOnce(permitKey); + const { lastFrame } = render( + , + ); + await delay(100); + expect(lastFrame()).toMatch(/permit_key_aaaaaaa/); + }); + it('Invalid Key', async () => { + const permitKey = 'permit_key'.concat('a'.repeat(97)); + const { lastFrame } = render( + , + ); + await delay(50); + expect(lastFrame()).toMatch(/Key is not valid./); + }); +}); diff --git a/tests/cli.test.tsx b/tests/cli.test.tsx new file mode 100644 index 0000000..78033e3 --- /dev/null +++ b/tests/cli.test.tsx @@ -0,0 +1,18 @@ +import { describe, vi, expect, it } from 'vitest'; + +vi.mock('pastel', () => ({ + default: vi.fn().mockImplementation(() => ({ + run: vi.fn(() => Promise.resolve()), + })), +})); + +import Pastel from 'pastel'; + +describe('Cli script', () => { + it('Should run the pastel app', async () => { + await import('../source/cli'); + expect(Pastel).toHaveBeenCalled(); + const pastelInstance = Pastel.mock.results[0].value; + expect(pastelInstance.run).toHaveBeenCalled(); + }); +}); diff --git a/tests/components/AuthProvider.test.tsx b/tests/components/AuthProvider.test.tsx new file mode 100644 index 0000000..bd6e285 --- /dev/null +++ b/tests/components/AuthProvider.test.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { render } from 'ink-testing-library'; +import { AuthProvider, useAuth } from '../../source/components/AuthProvider.js'; +import { loadAuthToken } from '../../source/lib/auth.js'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { Text } from 'ink'; +import delay from 'delay'; + +vi.mock('../../source/lib/auth.js', () => ({ + loadAuthToken: vi.fn(), +})); + +describe('AuthProvider', () => { + it('should display loading text while loading token', async () => { + (loadAuthToken as any).mockResolvedValueOnce(new Promise(() => {})); + + const { lastFrame } = render( + + Child Component + , + ); + + expect(lastFrame()).toContain('Loading Token'); + }); + it('should display error message if loading token fails', async () => { + (loadAuthToken as any).mockRejectedValueOnce( + new Error('Failed to load token'), + ); + + const { lastFrame } = render( + + Child Component + , + ); + + await delay(50); + expect(lastFrame()).toContain('Failed to load token'); + }); + + it('should display children when token is loaded successfully', async () => { + (loadAuthToken as any).mockResolvedValueOnce('mocked-token'); + + const { lastFrame } = render( + + Child Component + , + ); + + await delay(50); + expect(lastFrame()).toContain('Child Component'); + }); + it('should use the auth context successfully', async () => { + const ChildComponent = () => { + const { authToken } = useAuth(); + return {authToken || 'No token'}; + }; + + (loadAuthToken as any).mockResolvedValueOnce('mocked-token'); + + const { lastFrame } = render( + + + , + ); + + await delay(100); + expect(lastFrame()).toContain('mocked-token'); + }); + + it('should throw an error when useAuth is called outside of AuthProvider', () => { + const ChildComponent = () => { + let apiKey: string; + try { + const { authToken } = useAuth(); + apiKey = authToken; + } catch (error) { + return useAuth must be used within an AuthProvider; + } + return {apiKey || 'No token'}; + }; + const { lastFrame } = render(); + expect(lastFrame()).toContain( + 'useAuth must be used within an AuthProvider', + ); + }); +}); diff --git a/tests/components/EnvironmentSelection.test.tsx b/tests/components/EnvironmentSelection.test.tsx new file mode 100644 index 0000000..0dbac26 --- /dev/null +++ b/tests/components/EnvironmentSelection.test.tsx @@ -0,0 +1,95 @@ +import { vi, it, describe, expect, beforeEach } from 'vitest'; +import React from 'react'; +import { render } from 'ink-testing-library'; +import EnvironmentSelection from '../../source/components/EnvironmentSelection'; +import delay from 'delay'; +import { useAuthApi } from '../../source/hooks/useAuthApi'; +import { useApiKeyApi } from '../../source/hooks/useApiKeyApi'; +import { useEnvironmentApi } from '../../source/hooks/useEnvironmentApi'; +import { useOrganisationApi } from '../../source/hooks/useOrganisationApi'; +import { apiCall } from '../../source/lib/api'; +import { g } from 'vitest/dist/chunks/suite.B2jumIFP.js'; +vi.mock('../../source/lib/api', () => ({ + apiCall: vi.fn(), +})); + +vi.mock('../../source/hooks/useAuthApi', () => ({ + useAuthApi: () => ({ + authSwitchOrgs: vi.fn().mockResolvedValue({ + headers: { + getSetCookie: () => ['new-cookie'], + }, + error: null, + }), + }), +})); + +vi.mock('../../source/hooks/useEnvironmentApi', () => ({ + useEnvironmentApi: () => ({ + getEnvironment: vi.fn().mockResolvedValue({ + response: { + name: 'Test Env', + id: 'env1', + project_id: 'proj1', + }, + }), + }), +})); + +vi.mock('../../source/hooks/useOrganisationApi', () => ({ + useOrganisationApi: () => ({ + getOrg: vi.fn().mockResolvedValue({ + response: { + name: 'Test Org', + id: 'org1', + }, + }), + }), +})); + +vi.mock('../../source/hooks/useApiKeyApi', () => ({ + useApiKeyApi: () => ({ + getApiKeyScope: vi.fn().mockResolvedValue({ + response: { + environment_id: 'env1', + project_id: 'proj1', + organization_id: 'org1', + }, + error: null, + status: 200, + }), + getProjectEnvironmentApiKey: vi.fn().mockResolvedValue({ + response: { secret: 'test-secret' }, + error: null, + }), + }), +})); + +describe('EnvironmentSelection', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should handle the where the scope has environment_id and project_id', async () => { + await delay(100); + const onComplete = vi.fn(); + const onError = vi.fn(); + + render( + , + ); + + await delay(100); + expect(onError).not.toHaveBeenCalled(); + expect(onComplete).toHaveBeenCalledWith( + { label: 'Test Org', value: 'org1' }, + { label: '', value: 'proj1' }, + { label: 'Test Env', value: 'env1' }, + 'test-token', + ); + }); +}); diff --git a/tests/components/PDPCommand.test.tsx b/tests/components/PDPCommand.test.tsx new file mode 100644 index 0000000..81deba9 --- /dev/null +++ b/tests/components/PDPCommand.test.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { describe, it, expect, vi } from 'vitest'; +import { render } from 'ink-testing-library'; +import PDPCommand from '../../source/components/PDPCommand'; +import { AuthProvider } from '../../source/components/AuthProvider'; +import delay from 'delay'; +import { loadAuthToken } from '../../source/lib/auth'; +vi.mock('../../source/lib/auth', () => ({ + loadAuthToken: vi.fn(), +})); +describe('PDP Component', () => { + it('should render the PDP component with auth token', async () => { + (loadAuthToken as any).mockResolvedValueOnce( + 'permit_key_'.concat('a'.repeat(97)), + ); + const { lastFrame } = render( + + + , + ); + expect(lastFrame()?.toString()).toMatch('Loading Token'); + await delay(50); + expect(lastFrame()?.toString()).toMatch( + 'Run the following command from your terminal:', + ); + }); + it('should render the Spinner', async () => { + const { lastFrame } = render( + + + , + ); + expect(lastFrame()?.toString()).toMatch('Loading Token'); + await delay(50); + expect(lastFrame()?.toString()).toMatch('Loading command'); + }); +}); diff --git a/tests/e2e/check.test.ts b/tests/e2e/check.test.ts deleted file mode 100644 index ef5f610..0000000 --- a/tests/e2e/check.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -// tests/e2e/check.test.ts -import { exec } from 'child_process'; -import { promisify } from 'util'; -import { describe, it, expect } from 'vitest'; - -const execAsync = promisify(exec); -const CLI_COMMAND = 'npx tsx ./source/cli pdp check'; - -describe('pdp check command e2e', () => { - // Test original functionality remains intact - describe('backwards compatibility', () => { - it('should work with basic required parameters', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r testResource -a read` - ); - expect(stdout).toContain('user="testUser"'); - expect(stdout).toContain('action=read'); - expect(stdout).toContain('resource=testResource'); - },10000); - - it('should work with optional tenant parameter', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r testResource -a read "tenant" "customTenant"` - ); - expect(stdout).toContain('DENIED'); - }); - - it('should work with resource type:key format', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r "document:doc123" -a read` - ); - expect(stdout).toContain('resource=document:doc123'); - }); - }); - - // Test new attribute functionality - describe('user attributes', () => { - it('should handle single user attribute', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r testResource -a read -ua "role:admin"` - ); - expect(stdout).toContain('DENIED'); - }); - - it('should handle multiple user attributes', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r testResource -a read -ua "role:admin,department:IT,level:5"` - ); - expect(stdout).toContain('DENIED'); - }); - - it('should handle user attributes with different types', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r testResource -a read -ua "isAdmin:true,age:25,name:john"` - ); - expect(stdout).toContain('DENIED'); - }); - }); - - describe('resource attributes', () => { - it('should handle single resource attribute', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r testResource -a read -ra "owner:john"` - ); - expect(stdout).toContain('DENIED'); - }); - - it('should handle multiple resource attributes', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r testResource -a read -ra "owner:john,status:active,priority:high"` - ); - expect(stdout).toContain('DENIED'); - }); - - it('should handle resource attributes with different types', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r testResource -a read -ra "isPublic:true,size:1024,type:document"` - ); - expect(stdout).toContain('DENIED'); - }); - }); - - describe('combined scenarios', () => { - it('should handle both user and resource attributes', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r testResource -a read -ua "role:admin" -ra "status:active"` - ); - expect(stdout).toContain('DENIED'); - }); - - it('should work with all parameters combined', async () => { - const { stdout } = await execAsync( - `${CLI_COMMAND} -u testUser -r "document:doc123" -a write "tenant" customTenant -ua "role:admin,dept:IT" -ra "status:active,size:1024"` - ); - expect(stdout).toContain('DENIED'); - }); - }); - - describe('error handling', () => { - it('should handle invalid user attribute format', async () => { - try{ - const { stderr } = await execAsync( - `${CLI_COMMAND} -u johnexample.com -r "document"`, - { encoding: 'utf8' } - );} - catch (error) { - expect(error.stderr).toContain(''); - } - }); - - it('should handle invalid resource attribute format', async () => { - try { - await execAsync( - `${CLI_COMMAND} -u johnexample.com -r "document"`, - - ); - } catch (error) { - expect(error.stderr).toContain(''); - } - },10000); - }); -}); \ No newline at end of file diff --git a/tests/github.test.tsx b/tests/github.test.tsx index 6d29cc4..cbb20cc 100644 --- a/tests/github.test.tsx +++ b/tests/github.test.tsx @@ -16,6 +16,11 @@ import { import { loadAuthToken } from '../source/lib/auth.js'; const demoPermitKey = 'permit_key_'.concat('a'.repeat(97)); +vi.mock('clipboardy', () => ({ + default: { + writeSync: vi.fn(), + }, +})); vi.mock('../source/lib/auth.js', () => ({ loadAuthToken: vi.fn(() => demoPermitKey), })); @@ -385,4 +390,81 @@ describe('GiHub Complete Flow', () => { const frameString = lastFrame()?.toString() ?? ''; expect(frameString).toMatch(/GitOps Configuration Wizard - GitHub/); }); + it('should display Error message for invalid status of the repo', async () => { + (configurePermit as any).mockResolvedValueOnce({ + id: '1', + status: 'invalid', + key: 'repo3', + }); + const { stdin, lastFrame } = render( + , + ); + const frameString = lastFrame()?.toString() ?? ''; + expect(frameString).toMatch(/Loading Token/); + await delay(50); + expect(lastFrame()?.toString()).toMatch( + /GitOps Configuration Wizard - GitHub/, + ); + await delay(50); + stdin.write(arrowDown); + await delay(50); + stdin.write(enter); + await delay(50); + expect(lastFrame()?.toString()).toMatch(/Enter Your RepositoryKey :/); + await delay(50); + stdin.write('repo3'); + await delay(50); + stdin.write(enter); + await delay(50); + expect(lastFrame()?.toString()).toMatch(/SSH Key Generated./); + await delay(50); + stdin.write('git@github.com:user/repository.git'); + await delay(50); + stdin.write(enter); + expect(lastFrame()?.toString()).toMatch(/Enter the Branch Name:/); + await delay(50); + stdin.write('main'); + await delay(50); + stdin.write(enter); + await delay(50); + expect(lastFrame()?.toString()).toMatch( + 'Invalid configuration. Please check the configuration and try again.', + ); + }); + it('should work with inactive argument', async () => { + const { stdin, lastFrame } = render( + , + ); + const frameString = lastFrame()?.toString() ?? ''; + expect(frameString).toMatch(/Loading Token/); + await delay(100); + expect(lastFrame()?.toString()).toMatch( + /GitOps Configuration Wizard - GitHub/, + ); + await delay(50); + stdin.write(arrowDown); + await delay(50); + stdin.write(enter); + await delay(50); + expect(lastFrame()?.toString()).toMatch(/Enter Your RepositoryKey :/); + await delay(50); + stdin.write('repo3'); + await delay(50); + stdin.write(enter); + await delay(50); + expect(lastFrame()?.toString()).toMatch(/SSH Key Generated./); + await delay(50); + stdin.write('git@github.com:user/repository.git'); + await delay(50); + stdin.write(enter); + expect(lastFrame()?.toString()).toMatch(/Enter the Branch Name:/); + await delay(50); + stdin.write('main'); + await delay(50); + stdin.write(enter); + await delay(50); + expect(lastFrame()?.toString()).toMatch( + /Your GitOps is configured succesffuly. To complete the setup, remember to activate it later/, + ); + }); }); diff --git a/tests/hooks/useApiKeyAPI.test.tsx b/tests/hooks/useApiKeyAPI.test.tsx new file mode 100644 index 0000000..0b3d23f --- /dev/null +++ b/tests/hooks/useApiKeyAPI.test.tsx @@ -0,0 +1,246 @@ +import { useApiKeyApi } from '../../source/hooks/useApiKeyApi'; +import { apiCall } from '../../source/lib/api'; +import { TokenType, tokenType } from '../../source/lib/auth'; +import { vi, expect, it, describe, beforeEach } from 'vitest'; +import React from 'react'; +import { render } from 'ink-testing-library'; +import { Text } from 'ink'; + +vi.mock('../../source/lib/api', () => ({ + apiCall: vi.fn(), +})); + +vi.mock('../../source/lib/auth', () => ({ + tokenType: vi.fn(), + TokenType: { + APIToken: 'APIToken', + Invalid: 'Invalid', + }, +})); + +describe('useApiKeyApi', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('Calls getProjectEnvironmentApiKey and fetches the API key data', async () => { + const projectId = 'test-project-id'; + const environmentId = 'test-environment-id'; + const cookie = 'test-cookie'; + const accessToken = 'test-access-token'; + const mockResponse = { data: 'mock-data' }; + + (apiCall as any).mockResolvedValue(mockResponse); + + const TestComponent = () => { + const { getProjectEnvironmentApiKey } = useApiKeyApi(); + const [result, setResult] = React.useState(null); + + React.useEffect(() => { + const fetchData = async () => { + const data = await getProjectEnvironmentApiKey( + projectId, + environmentId, + cookie, + accessToken, + ); + setResult(JSON.stringify(data)); + }; + fetchData(); + }, [getProjectEnvironmentApiKey]); + + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + const frame = lastFrame(); + expect(frame).toBe('{"data":"mock-data"}'); + }); + + expect(apiCall).toHaveBeenCalledWith( + `v2/api-key/${projectId}/${environmentId}`, + accessToken, + cookie, + ); + }); + + it('Calls getApiKeyScope and fetches the API key scope', async () => { + const accessToken = 'test-access-token'; + const mockResponse = { + organization_id: 'org-id', + project_id: null, + environment_id: null, + }; + + (apiCall as any).mockResolvedValue(mockResponse); + + const TestComponent = () => { + const { getApiKeyScope } = useApiKeyApi(); + const [result, setResult] = React.useState(null); + + React.useEffect(() => { + const fetchData = async () => { + const data = await getApiKeyScope(accessToken); + setResult(JSON.stringify(data)); + }; + fetchData(); + }, [getApiKeyScope]); + + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + const frame = lastFrame(); + expect(frame).toBe( + '{"organization_id":"org-id","project_id":null,"environment_id":null}', + ); + }); + + expect(apiCall).toHaveBeenCalledWith('v2/api-key/scope', accessToken); + }); + + it('validates a valid API key', async () => { + (tokenType as vi.Mock).mockReturnValue(TokenType.APIToken); + + const mockScope = { + organization_id: 'org-id', + project_id: 'project-id', + environment_id: null, + }; + + (apiCall as vi.Mock).mockResolvedValue({ + response: mockScope, + error: null, + }); + + const TestComponent = () => { + const { validateApiKeyScope } = useApiKeyApi(); + const [result, setResult] = React.useState(null); + + React.useEffect(() => { + const validateScope = async () => { + const data = await validateApiKeyScope('valid-api-key', 'project'); + setResult(JSON.stringify(data)); + }; + validateScope(); + }, [validateApiKeyScope]); + + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + const frame = lastFrame(); + expect(frame) + .toBe(`{"valid":true,"scope":{"organization_id":"org-id","project_id":"project-id","environment_id":null}," +error":null}`); + }); + + expect(apiCall).toHaveBeenCalledWith('v2/api-key/scope', 'valid-api-key'); + }); + + it('invalidates an incorrect API key', async () => { + (tokenType as vi.Mock).mockReturnValue(TokenType.Invalid); + + (apiCall as vi.Mock).mockResolvedValue({ + response: null, + error: 'Invalid API Key', + }); + + const TestComponent = () => { + const { validateApiKeyScope } = useApiKeyApi(); + const [result, setResult] = React.useState(null); + + React.useEffect(() => { + const validateScope = async () => { + const data = await validateApiKeyScope('invalid-key', 'project'); + setResult(JSON.stringify(data)); + }; + validateScope(); + }, [validateApiKeyScope]); + + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + const frame = lastFrame(); + expect(frame).toBe( + '{"valid":false,"scope":null,"error":"Please provide a valid api key"}', + ); + }); + }); + + it('validates an API key scope for organization level', async () => { + const apiKey = 'valid-api-key'; + const mockScope = { + organization_id: 'org-id', + project_id: null, + environment_id: null, + }; + + (apiCall as any).mockResolvedValue({ response: mockScope, error: null }); + (tokenType as any).mockReturnValue(TokenType.APIToken); + + const TestComponent = () => { + const { validateApiKeyScope } = useApiKeyApi(); + const [result, setResult] = React.useState(null); + + React.useEffect(() => { + const validate = async () => { + const validation = await validateApiKeyScope(apiKey, 'organization'); + setResult(JSON.stringify(validation)); + }; + validate(); + }, [validateApiKeyScope]); + + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + const frame = lastFrame(); + expect(frame).toBe( + `{"valid":true,"scope":{"organization_id":"org-id","project_id":null,"environment_id":null},"error":n +ull}`, + ); + }); + + expect(apiCall).toHaveBeenCalledWith('v2/api-key/scope', apiKey); + }); + + it('handles invalid API key in validateApiKeyScope', async () => { + const apiKey = 'invalid-api-key'; + + (apiCall as any).mockResolvedValue({ + response: null, + error: 'Invalid API key', + }); + (tokenType as any).mockReturnValue(TokenType.Invalid); + + const TestComponent = () => { + const { validateApiKeyScope } = useApiKeyApi(); + const [result, setResult] = React.useState(null); + + React.useEffect(() => { + const validate = async () => { + const validation = await validateApiKeyScope(apiKey, 'organization'); + setResult(JSON.stringify(validation)); + }; + validate(); + }, [validateApiKeyScope]); + + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + const frame = lastFrame(); + expect(frame).toBe( + '{"valid":false,"scope":null,"error":"Please provide a valid api key"}', + ); + }); + }); +}); diff --git a/tests/hooks/useAuthApi.test.tsx b/tests/hooks/useAuthApi.test.tsx new file mode 100644 index 0000000..4f226b0 --- /dev/null +++ b/tests/hooks/useAuthApi.test.tsx @@ -0,0 +1,109 @@ +import { useAuthApi } from '../../source/hooks/useAuthApi'; +import { apiCall } from '../../source/lib/api'; +import { vi, expect, it, describe, beforeEach } from 'vitest'; +import React from 'react'; +import { render } from 'ink-testing-library'; +import { Text } from 'ink'; + +// Mocking the apiCall function +vi.mock('../../source/lib/api', () => ({ + apiCall: vi.fn(), +})); + +describe('useAuthApi', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should switch organization and return success', async () => { + const TestComponent = () => { + const { authSwitchOrgs } = useAuthApi(); + // Mock the apiCall to simulate a successful response + apiCall.mockResolvedValue({ success: true }); + const [result, setResult] = React.useState(null); + + const switchOrg = async () => { + const result = await authSwitchOrgs( + 'workspace-id', + 'access-token', + 'cookie', + ); + return result.success ? 'Organization switched' : 'Failed to switch'; + }; + switchOrg().then(res => setResult(res)); + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + expect(lastFrame()).toBe('Organization switched'); + }); + }); + + it('should login and return success', async () => { + const TestComponent = () => { + const { getLogin } = useAuthApi(); + // Mock the apiCall to simulate a successful login response + apiCall.mockResolvedValue({ success: true }); + const [result, setResult] = React.useState(null); + + const login = async () => { + const result = await getLogin('valid-token'); + return result.success ? 'Login successful' : 'Login failed'; + }; + login().then(res => setResult(res)); + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + expect(lastFrame()).toBe('Login successful'); + }); + }); + + it('should handle failed organization switch', async () => { + const TestComponent = () => { + const { authSwitchOrgs } = useAuthApi(); + // Mock the apiCall to simulate a failed response + apiCall.mockResolvedValue({ success: false }); + const [result, setResult] = React.useState(null); + + const switchOrg = async () => { + const result = await authSwitchOrgs( + 'workspace-id', + 'access-token', + 'cookie', + ); + return result.success ? 'Organization switched' : 'Failed to switch'; + }; + switchOrg().then(res => setResult(res)); + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + expect(lastFrame()).toBe('Failed to switch'); + }); + }); + + it('should handle failed login', async () => { + const TestComponent = () => { + const { getLogin } = useAuthApi(); + // Mock the apiCall to simulate a failed login response + apiCall.mockResolvedValue({ success: false }); + + const login = async () => { + const result = await getLogin('invalid-token'); + return result.success ? 'Login successful' : 'Login failed'; + }; + const [result, setResult] = React.useState(null); + login().then(res => setResult(res)); + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + expect(lastFrame()).toBe('Login failed'); + }); + }); +}); diff --git a/tests/hooks/useEnvironmentApi.test.tsx b/tests/hooks/useEnvironmentApi.test.tsx new file mode 100644 index 0000000..47f9081 --- /dev/null +++ b/tests/hooks/useEnvironmentApi.test.tsx @@ -0,0 +1,193 @@ +import { useEnvironmentApi } from '../../source/hooks/useEnvironmentApi'; +import { apiCall } from '../../source/lib/api'; +import { vi, expect, it, describe, beforeEach } from 'vitest'; +import React from 'react'; +import { render } from 'ink-testing-library'; +import { Text } from 'ink'; + +// Mocking the apiCall function +vi.mock('../../source/lib/api', () => ({ + apiCall: vi.fn(), +})); + +describe('useEnvironmentApi', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should fetch environments and return a list of environments', async () => { + const TestComponent = () => { + const { getEnvironments } = useEnvironmentApi(); + const projectId = 'project-id'; + const accessToken = 'access-token'; + + // Mock the apiCall to simulate a successful response + apiCall.mockResolvedValue([ + { + key: 'env-key-1', + id: 'env-id-1', + organization_id: 'org-id', + project_id: 'project-id', + created_at: '2024-12-01', + updated_at: '2024-12-02', + email_configuration: 'email-config', + name: 'Env 1', + }, + ]); + + const fetchEnvironments = async () => { + const environments = await getEnvironments(projectId, accessToken); + return environments.length > 0 + ? 'Environments fetched' + : 'No environments'; + }; + const [restult, setResult] = React.useState(null); + fetchEnvironments().then(res => setResult(res)); + return {restult}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + expect(lastFrame()).toBe('Environments fetched'); + }); + }); + + it('should fetch a single environment by id', async () => { + const TestComponent = () => { + const { getEnvironment } = useEnvironmentApi(); + const projectId = 'project-id'; + const environmentId = 'env-id-1'; + const accessToken = 'access-token'; + + // Mock the apiCall to simulate a successful response + apiCall.mockResolvedValue({ + key: 'env-key-1', + id: 'env-id-1', + organization_id: 'org-id', + project_id: 'project-id', + created_at: '2024-12-01', + updated_at: '2024-12-02', + email_configuration: 'email-config', + name: 'Env 1', + }); + + const fetchEnvironment = async () => { + const environment = await getEnvironment( + projectId, + environmentId, + accessToken, + ); + return environment ? 'Environment fetched' : 'No environment found'; + }; + const [restult, setResult] = React.useState(null); + fetchEnvironment().then(res => setResult(res)); + + return {restult}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + expect(lastFrame()).toBe('Environment fetched'); + }); + }); + + it('should handle failed fetch of environments', async () => { + const TestComponent = () => { + const { getEnvironments } = useEnvironmentApi(); + const projectId = 'project-id'; + const accessToken = 'access-token'; + + // Mock the apiCall to simulate a failed response + apiCall.mockResolvedValue([]); + + const fetchEnvironments = async () => { + const environments = await getEnvironments(projectId, accessToken); + return environments.length > 0 + ? 'Environments fetched' + : 'No environments'; + }; + const [restult, setResult] = React.useState(null); + fetchEnvironments().then(res => setResult(res)); + + return {restult}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + expect(lastFrame()).toBe('No environments'); + }); + }); + + it('should copy an environment successfully', async () => { + const TestComponent = () => { + const { copyEnvironment } = useEnvironmentApi(); + const projectId = 'project-id'; + const environmentId = 'env-id-1'; + const accessToken = 'access-token'; + const cookie = 'cookie'; + const body = { someKey: 'someValue' }; + + // Mock the apiCall to simulate a successful response + apiCall.mockResolvedValue({ success: true }); + + const copyEnv = async () => { + const result = await copyEnvironment( + projectId, + environmentId, + accessToken, + cookie, + body, + ); + return result.success + ? 'Environment copied' + : 'Failed to copy environment'; + }; + const [result, setResult] = React.useState(null); + copyEnv().then(res => setResult(res)); + + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + expect(lastFrame()).toBe('Environment copied'); + }); + }); + + it('should handle failed environment copy', async () => { + const TestComponent = () => { + const { copyEnvironment } = useEnvironmentApi(); + const projectId = 'project-id'; + const environmentId = 'env-id-1'; + const accessToken = 'access-token'; + const cookie = 'cookie'; + const body = { someKey: 'someValue' }; + + // Mock the apiCall to simulate a failed response + apiCall.mockResolvedValue({ success: false }); + + const copyEnv = async () => { + const result = await copyEnvironment( + projectId, + environmentId, + accessToken, + cookie, + body, + ); + return result.success + ? 'Environment copied' + : 'Failed to copy environment'; + }; + + const [result, setResult] = React.useState(null); + copyEnv().then(res => setResult(res)); + + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + expect(lastFrame()).toBe('Failed to copy environment'); + }); + }); +}); diff --git a/tests/hooks/useMemberAPI.test.tsx b/tests/hooks/useMemberAPI.test.tsx new file mode 100644 index 0000000..10add98 --- /dev/null +++ b/tests/hooks/useMemberAPI.test.tsx @@ -0,0 +1,73 @@ +import { useMemberApi } from '../../source/hooks/useMemberApi'; +import { apiCall } from '../../source/lib/api'; +import { vi, expect, it, describe, beforeEach } from 'vitest'; +import React from 'react'; +import { render } from 'ink-testing-library'; +import { Text } from 'ink'; + +// Mocking the apiCall function +vi.mock('../../source/lib/api', () => ({ + apiCall: vi.fn(), +})); + +describe('useMemberApi', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should invite a new member successfully', async () => { + const TestComponent = () => { + const { inviteNewMember } = useMemberApi(); + const authToken = 'auth-token'; + const body = { + email: 'newmember@example.com', + role: 'member', + }; + + // Mock the apiCall to simulate a successful response + apiCall.mockResolvedValue({ success: true }); + + const inviteMember = async () => { + const result = await inviteNewMember(authToken, body); + return result.success ? 'Member invited' : 'Failed to invite member'; + }; + const [result, setResult] = React.useState(null); + inviteMember().then(res => setResult(res)); + + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + expect(lastFrame()).toBe('Member invited'); + }); + }); + + it('should handle failed member invitation', async () => { + const TestComponent = () => { + const { inviteNewMember } = useMemberApi(); + const authToken = 'auth-token'; + const body = { + email: 'newmember@example.com', + role: 'member', + }; + + // Mock the apiCall to simulate a failed response + apiCall.mockResolvedValue({ success: false }); + + const inviteMember = async () => { + const result = await inviteNewMember(authToken, body); + return result.success ? 'Member invited' : 'Failed to invite member'; + }; + const [result, setResult] = React.useState(null); + inviteMember().then(res => setResult(res)); + + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + expect(lastFrame()).toBe('Failed to invite member'); + }); + }); +}); diff --git a/tests/hooks/useOrganizationAPI.test.tsx b/tests/hooks/useOrganizationAPI.test.tsx new file mode 100644 index 0000000..f695f1b --- /dev/null +++ b/tests/hooks/useOrganizationAPI.test.tsx @@ -0,0 +1,146 @@ +import { useOrganisationApi } from '../../source/hooks/useOrganisationApi'; +import { apiCall } from '../../source/lib/api'; +import { vi, expect, it, describe, beforeEach } from 'vitest'; +import React from 'react'; +import { render } from 'ink-testing-library'; +import { Text } from 'ink'; + +// Mocking the apiCall function +vi.mock('../../source/lib/api', () => ({ + apiCall: vi.fn(), +})); + +describe('useOrganisationApi', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should fetch all organizations', async () => { + const TestComponent = () => { + const { getOrgs } = useOrganisationApi(); + const accessToken = 'access-token'; + const cookie = 'cookie'; + + // Mock the apiCall to simulate a successful response + apiCall.mockResolvedValue([ + { + key: 'org-key', + id: 'org-id', + is_enterprise: false, + usage_limits: { mau: 100, tenants: 10, billing_tier: 'standard' }, + created_at: '2024-01-01', + updated_at: '2024-01-02', + name: 'Organization Name', + settings: {}, + }, + ]); + + const fetchOrgs = async () => { + const orgs = await getOrgs(accessToken, cookie); + return orgs.length > 0 ? orgs[0].name : 'No organizations'; + }; + const [result, setResult] = React.useState(null); + fetchOrgs().then(res => setResult(res)); + + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + expect(lastFrame()).toBe('Organization Name'); + }); + }); + + it('should handle failure to fetch organizations', async () => { + const TestComponent = () => { + const { getOrgs } = useOrganisationApi(); + const accessToken = 'access-token'; + const cookie = 'cookie'; + + // Mock the apiCall to simulate a failed response + apiCall.mockRejectedValue(new Error('Failed to fetch organizations')); + + const fetchOrgs = async () => { + try { + const orgs = await getOrgs(accessToken, cookie); + return orgs.length > 0 ? orgs[0].name : 'No organizations'; + } catch (error) { + return error.message; + } + }; + const [result, setResult] = React.useState(null); + fetchOrgs().then(res => setResult(res)); + + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + expect(lastFrame()).toBe('Failed to fetch organizations'); + }); + }); + + it('should fetch a single organization', async () => { + const TestComponent = () => { + const { getOrg } = useOrganisationApi(); + const accessToken = 'access-token'; + const cookie = 'cookie'; + const organizationId = 'org-id'; + + // Mock the apiCall to simulate a successful response for a single organization + apiCall.mockResolvedValue({ + key: 'org-key', + id: 'org-id', + is_enterprise: false, + usage_limits: { mau: 100, tenants: 10, billing_tier: 'standard' }, + created_at: '2024-01-01', + updated_at: '2024-01-02', + name: 'Organization Name', + settings: {}, + }); + + const fetchOrg = async () => { + const org = await getOrg(organizationId, accessToken, cookie); + return org.name; + }; + const [result, setResult] = React.useState(null); + fetchOrg().then(res => setResult(res)); + + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + expect(lastFrame()).toBe('Organization Name'); + }); + }); + + it('should handle failure to fetch a single organization', async () => { + const TestComponent = () => { + const { getOrg } = useOrganisationApi(); + const accessToken = 'access-token'; + const cookie = 'cookie'; + const organizationId = 'org-id'; + + // Mock the apiCall to simulate a failed response for a single organization + apiCall.mockRejectedValue(new Error('Failed to fetch organization')); + + const fetchOrg = async () => { + try { + const org = await getOrg(organizationId, accessToken, cookie); + return org.name; + } catch (error) { + return error.message; + } + }; + const [result, setResult] = React.useState(null); + fetchOrg().then(res => setResult(res)); + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + expect(lastFrame()).toBe('Failed to fetch organization'); + }); + }); +}); diff --git a/tests/hooks/useProjectAPI.test.tsx b/tests/hooks/useProjectAPI.test.tsx new file mode 100644 index 0000000..7dbe75e --- /dev/null +++ b/tests/hooks/useProjectAPI.test.tsx @@ -0,0 +1,82 @@ +import { useProjectAPI } from '../../source/hooks/useProjectAPI'; +import { apiCall } from '../../source/lib/api'; +import { vi, expect, it, describe, beforeEach } from 'vitest'; +import React from 'react'; +import { render } from 'ink-testing-library'; +import { Text } from 'ink'; + +// Mocking the apiCall function +vi.mock('../../source/lib/api', () => ({ + apiCall: vi.fn(), +})); + +describe('useProjectAPI', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should fetch all projects', async () => { + const TestComponent = () => { + const { getProjects } = useProjectAPI(); + const accessToken = 'access-token'; + const cookie = 'cookie'; + + // Mock the apiCall to simulate a successful response + apiCall.mockResolvedValue([ + { + key: 'project-key', + id: 'project-id', + organization_id: 'org-id', + created_at: '2024-01-01', + updated_at: '2024-01-02', + name: 'Project Name', + settings: {}, + active_policy_repo_id: 'policy-id', + }, + ]); + + const fetchProjects = async () => { + const projects = await getProjects(accessToken, cookie); + return projects.length > 0 ? projects[0].name : 'No projects'; + }; + const [result, setResult] = React.useState(null); + fetchProjects().then(res => setResult(res)); + + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + expect(lastFrame()).toBe('Project Name'); + }); + }); + + it('should handle failure to fetch projects', async () => { + const TestComponent = () => { + const { getProjects } = useProjectAPI(); + const accessToken = 'access-token'; + const cookie = 'cookie'; + + // Mock the apiCall to simulate a failed response + apiCall.mockRejectedValue(new Error('Failed to fetch projects')); + + const fetchProjects = async () => { + try { + const projects = await getProjects(accessToken, cookie); + return projects.length > 0 ? projects[0].name : 'No projects'; + } catch (error) { + return error.message; + } + }; + const [result, setResult] = React.useState(null); + fetchProjects().then(res => setResult(res)); + + return {result}; + }; + + const { lastFrame } = render(); + await vi.waitFor(() => { + expect(lastFrame()).toBe('Failed to fetch projects'); + }); + }); +}); diff --git a/tests/index.test.tsx b/tests/index.test.tsx new file mode 100644 index 0000000..4005379 --- /dev/null +++ b/tests/index.test.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { render } from 'ink-testing-library'; +import { describe, vi, expect, it } from 'vitest'; +import Index from '../source/commands/index'; +import delay from 'delay'; + +describe('index file', () => { + it('the index file should render', () => { + const { lastFrame } = render(); + expect(lastFrame()?.toString()).toMatch( + /Run this command with --help for more information/, + ); + }); +}); diff --git a/tests/lib/api.test.ts b/tests/lib/api.test.ts new file mode 100644 index 0000000..3ee59b8 --- /dev/null +++ b/tests/lib/api.test.ts @@ -0,0 +1,24 @@ +import { describe, vi, it, expect } from 'vitest'; +import * as api from '../../source/lib/api'; +global.fetch = vi.fn(); +describe('API', () => { + it('should call the apiCall', async () => { + (fetch as any).mockResolvedValueOnce({ + headers: {}, + ok: true, + status: 200, + json: async () => ({ id: 'testId', name: 'testName' }), + }); + const response = await api.apiCall<{ id: string; name: string }>( + 'testEndpoint', + 'testToken', + 'testCookie', + 'GET', + 'testBody', + ); + expect(response.status).toBe(200); + expect(response.response.id).toBe('testId'); + expect(response.response.name).toBe('testName'); + expect(response.headers).toEqual({}); + }); +}); diff --git a/tests/lib/auth.test.ts b/tests/lib/auth.test.ts new file mode 100644 index 0000000..947320d --- /dev/null +++ b/tests/lib/auth.test.ts @@ -0,0 +1,122 @@ +import { describe, vi, it, expect } from 'vitest'; +import * as auth from '../../source/lib/auth'; +import * as http from 'http'; +import { + KEYSTORE_PERMIT_SERVICE_NAME, + DEFAULT_PERMIT_KEYSTORE_ACCOUNT, +} from '../../source/config'; +import open from 'open'; +import * as pkg from 'keytar'; + +// Mock dependencies +vi.mock('http', () => ({ + createServer: vi.fn().mockReturnValue({ + listen: vi.fn(), + close: vi.fn(), + }), +})); +vi.mock('open', () => ({ + default: vi.fn(), +})); + +vi.mock('node:crypto', () => ({ + randomBytes: vi.fn().mockReturnValue(Buffer.from('mock-verifier')), + createHash: vi.fn().mockImplementation(() => ({ + update: vi.fn().mockReturnThis(), + digest: vi.fn(() => Buffer.from('mock-hash')), + })), +})); + +// Correct mock for 'keytar' using named exports +vi.mock('keytar', () => ({ + setPassword: vi.fn(), + getPassword: vi.fn(), // Mocked return value + deletePassword: vi.fn(), +})); + +describe('Token Type', () => { + it('Should return correct token type', async () => { + const demoToken = 'permit_key_'.concat('a'.repeat(97)); + const tokenType = auth.tokenType(demoToken); + expect(tokenType).toBe(auth.TokenType.APIToken); + }); + + it('Should return type of JWT', async () => { + const demoJwtToken = 'demo1.demo2.demo3'; + const tokenType = auth.tokenType(demoJwtToken); + expect(tokenType).toBe(auth.TokenType.AccessToken); + }); + + it('should return invalid token', async () => { + const demoInvalidToken = 'invalid token'; + const tokenType = auth.tokenType(demoInvalidToken); + expect(tokenType).toBe(auth.TokenType.Invalid); + }); +}); + +describe('Save Auth Token', () => { + it('Should save token', async () => { + const demoToken = 'permit_key_'.concat('a'.repeat(97)); + const { setPassword } = pkg; + const result = await auth.saveAuthToken(demoToken); + expect(setPassword).toBeCalledWith( + KEYSTORE_PERMIT_SERVICE_NAME, + DEFAULT_PERMIT_KEYSTORE_ACCOUNT, + demoToken, + ); + expect(result).toBe(''); // Ensure the result is empty as expected + }); + + it('Should return invalid token', async () => { + const demoToken = 'invalid token'; + const result = await auth.saveAuthToken(demoToken); + expect(result).toBe('Invalid auth token'); + }); +}); + +describe('Load Auth Token', () => { + it('Should load token', async () => { + const demoToken = 'permit_key_'.concat('a'.repeat(97)); + await auth.saveAuthToken(demoToken); // Save token first + const { getPassword } = pkg; + (getPassword as any).mockResolvedValueOnce( + 'permit_key_a'.concat('a'.repeat(97)), + ); + const result = await auth.loadAuthToken(); + expect(result).toBe('permit_key_a'.concat('a'.repeat(97))); // Mocked return value + }); + + it('Should throw error', async () => { + const { deletePassword } = await import('keytar'); + await deletePassword( + KEYSTORE_PERMIT_SERVICE_NAME, + DEFAULT_PERMIT_KEYSTORE_ACCOUNT, + ); + + try { + await auth.loadAuthToken(); + } catch (error) { + expect(error).toBeInstanceOf(Error); // Expect an error when the token is not found + } + }); +}); + +describe('Clean Auth Token', () => { + it('Should clean token', async () => { + const { getPassword } = pkg; + await auth.cleanAuthToken(); + (getPassword as any).mockResolvedValueOnce(null); + const result = await getPassword( + KEYSTORE_PERMIT_SERVICE_NAME, + DEFAULT_PERMIT_KEYSTORE_ACCOUNT, + ); + expect(result).toBeNull(); // Expect null after cleaning the token + }); +}); + +describe('Browser Auth', () => { + it('Should open browser', async () => { + await auth.browserAuth(); + expect(open).toHaveBeenCalled(); // Ensure the browser opens + }); +}); diff --git a/tests/lib/gitops_utils.test.ts b/tests/lib/gitops_utils.test.ts new file mode 100644 index 0000000..7826dd6 --- /dev/null +++ b/tests/lib/gitops_utils.test.ts @@ -0,0 +1,185 @@ +import { describe, vi, it, expect } from 'vitest'; +import * as utils from '../../source/lib/gitops/utils'; +import { apiCall } from '../../source/lib/api'; +import ssh from 'micro-key-producer/ssh.js'; +import { randomBytes } from 'micro-key-producer/utils.js'; +vi.mock('../../source/lib/api', () => ({ + apiCall: vi.fn(), +})); +vi.mock('micro-key-producer/ssh.js', () => ({ + default: vi.fn(), +})); +vi.mock('micro-key-producer/utils.js', () => ({ + randomBytes: vi.fn(), +})); + +describe('getProjectList', () => { + it('should return a list of projects', async () => { + (apiCall as any).mockResolvedValueOnce({ + status: 200, + response: [ + { + key: 'testKey', + urn_namespace: 'testNamespace', + id: 'testId', + organization_id: 'testOrgId', + created_at: 'testCreatedAt', + updated_at: 'testUpdatedAt', + name: 'testName', + }, + ], + }); + const projects = await utils.getProjectList( + 'permit_key_'.concat('a'.repeat(96)), + ); + expect(projects).toEqual([ + { + key: 'testKey', + urn_namespace: 'testNamespace', + id: 'testId', + organization_id: 'testOrgId', + created_at: 'testCreatedAt', + updated_at: 'testUpdatedAt', + name: 'testName', + }, + ]); + }); + it('should throw an error if the status is not 200', async () => { + (apiCall as any).mockResolvedValueOnce({ + status: 400, + response: 'testError', + }); + await expect( + utils.getProjectList('permit_key_'.concat('a'.repeat(96))), + ).rejects.toThrow('Failed to fetch projects: testError'); + }); +}); + +describe('getRepoList', () => { + it('should return a list of repos', async () => { + (apiCall as any).mockResolvedValueOnce({ + status: 200, + response: [ + { status: 'valid', key: 'testKey' }, + { status: 'invalid', key: 'testKey2' }, + ], + }); + const repos = await utils.getRepoList( + 'permit_key_'.concat('a'.repeat(96)), + 'testProjectKey', + ); + expect(repos).toEqual([ + { status: 'valid', key: 'testKey' }, + { status: 'invalid', key: 'testKey2' }, + ]); + }); +}); + +describe('generateSSHKey', () => { + it('should generate an SSH key', () => { + (randomBytes as any).mockReturnValueOnce(new Uint8Array(32)); + (ssh as any).mockReturnValueOnce({ + publicKeyBytes: new Uint8Array(8), + publicKey: 'publicKey', + privateKey: 'privateKey', + fingerprint: 'testFingerprint', + }); + const key = utils.generateSSHKey(); + expect(key).toStrictEqual({ + publicKeyBytes: new Uint8Array(8), + publicKey: 'publicKey', + privateKey: 'privateKey', + fingerprint: 'testFingerprint', + }); + }); +}); + +describe('Configure Permit', async () => { + it('should configure permit', async () => { + const gitconfig = { + url: 'testUrl', + mainBranchName: 'testMainBranchName', + credentials: { + authType: 'ssh', + username: 'git', + privateKey: 'privateKey', + }, + key: 'testKey', + activateWhenValidated: true, + }; + (apiCall as any).mockResolvedValueOnce({ + status: 200, + response: { + id: 'testId', + key: 'testKey', + status: 'valid', + }, + }); + const response = await utils.configurePermit( + 'permit_key_'.concat('a'.repeat(96)), + 'testProjectKey', + gitconfig, + ); + expect(response).toStrictEqual({ + id: 'testId', + key: 'testKey', + status: 'valid', + }); + }); + it('should throw an error if the status is 422', async () => { + const gitconfig = { + url: 'testUrl', + mainBranchName: 'testMainBranchName', + credentials: { + authType: 'ssh', + username: 'git', + privateKey: 'privateKey', + }, + key: 'testKey', + activateWhenValidated: true, + }; + (apiCall as any).mockResolvedValueOnce({ + status: 422, + response: { + id: 'testId', + key: 'testKey', + status: 'valid', + }, + }); + await expect( + utils.configurePermit( + 'permit_key_'.concat('a'.repeat(96)), + 'testProjectKey', + gitconfig, + ), + ).rejects.toThrow('Validation Error in Configuring Permit'); + }); + it('should throw an error if the status is not 200', async () => { + const gitconfig = { + url: 'testUrl', + mainBranchName: 'testMainBranchName', + credentials: { + authType: 'ssh', + username: 'git', + privateKey: 'privateKey', + }, + key: 'testKey', + activateWhenValidated: true, + }; + (apiCall as any).mockResolvedValueOnce({ + status: 400, + response: { + id: 'testId', + key: 'testKey', + status: 'valid', + }, + }); + await expect( + utils.configurePermit( + 'permit_key_'.concat('a'.repeat(96)), + 'testProjectKey', + gitconfig, + ), + ).rejects.toThrow('Invalid Configuration '); + }); +}); diff --git a/tests/login.test.tsx b/tests/login.test.tsx new file mode 100644 index 0000000..635cc9f --- /dev/null +++ b/tests/login.test.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { vi, expect, it, describe } from 'vitest'; +import { render } from 'ink-testing-library'; +import Login from '../source/commands/login'; +import delay from 'delay'; +import * as keytar from "keytar" + +vi.mock("keytar",()=>({ + getPassword: vi.fn(), + setPassword: vi.fn(), + deletePassword:vi.fn() +})) + +describe('Login Component', () => { + it('Should render the login component', async () => { + const { lastFrame } = render( + , + ); + expect(lastFrame()?.toString()).toMatch('Logging in'); + }); +}); diff --git a/tests/logout.test.tsx b/tests/logout.test.tsx new file mode 100644 index 0000000..5b14f22 --- /dev/null +++ b/tests/logout.test.tsx @@ -0,0 +1,29 @@ +import { vi, expect, it, describe, beforeEach } from 'vitest'; +import React from 'react'; +import { render } from 'ink-testing-library'; +import Logout from '../source/commands/logout'; +import delay from 'delay'; +import * as keytar from 'keytar'; + +vi.mock('keytar', () => ({ + setPassword: vi.fn(), + getPassword: vi.fn(), + deletePassword: vi.fn(), +})); + +describe('Logout', () => { + beforeEach(() => { + vi.spyOn(process, 'exit').mockImplementation(code => { + console.warn(`Mocked process.exit(${code})`); + }); + }); + + it('should render the logout component and call process.exit', async () => { + const { lastFrame } = render(); + // Ensure initial loading text is displayed + expect(lastFrame()).toMatch(/Cleaning session.../); + await delay(50); + // Ensure process.exit was called with 0 + expect(process.exit).toHaveBeenCalledWith(0); + }); +}); diff --git a/tests/utils/attributes.test.ts b/tests/utils/attributes.test.ts index aaa832a..2aa8cad 100644 --- a/tests/utils/attributes.test.ts +++ b/tests/utils/attributes.test.ts @@ -3,86 +3,100 @@ import { parseAttributes } from '../../source/utils/attributes'; import { describe, it, expect } from 'vitest'; describe('parseAttributes', () => { - it('should parse string attributes', () => { - const result = parseAttributes('name:john,role:admin'); - expect(result).toEqual({ - name: 'john', - role: 'admin' - }); - }); + it('should parse string attributes', () => { + const result = parseAttributes('name:john,role:admin'); + expect(result).toEqual({ + name: 'john', + role: 'admin', + }); + }); - it('should parse number attributes', () => { - const result = parseAttributes('age:25,score:98.5'); - expect(result).toEqual({ - age: 25, - score: 98.5 - }); - }); + it('should parse number attributes', () => { + const result = parseAttributes('age:25,score:98.5'); + expect(result).toEqual({ + age: 25, + score: 98.5, + }); + }); - it('should parse boolean attributes', () => { - const result = parseAttributes('active:true,verified:false'); - expect(result).toEqual({ - active: true, - verified: false - }); - }); + it('should parse boolean attributes', () => { + const result = parseAttributes('active:true,verified:false'); + expect(result).toEqual({ + active: true, + verified: false, + }); + }); - it('should handle mixed types', () => { - const result = parseAttributes('name:john,age:30,active:true'); - expect(result).toEqual({ - name: 'john', - age: 30, - active: true - }); - }); + it('should handle mixed types', () => { + const result = parseAttributes('name:john,age:30,active:true'); + expect(result).toEqual({ + name: 'john', + age: 30, + active: true, + }); + }); - it('should handle whitespace', () => { - const result = parseAttributes(' name : john , age : 30 '); - expect(result).toEqual({ - name: 'john', - age: 30 - }); - }); + it('should handle whitespace', () => { + const result = parseAttributes(' name : john , age : 30 '); + expect(result).toEqual({ + name: 'john', + age: 30, + }); + }); - it('should return empty object for empty string', () => { - const result = parseAttributes(''); - expect(result).toEqual({}); - }); + it('should return empty object for empty string', () => { + const result = parseAttributes(''); + expect(result).toEqual({}); + }); - it('should return empty object for whitespace string', () => { - const result = parseAttributes(' '); - expect(result).toEqual({}); - }); + it('should return empty object for whitespace string', () => { + const result = parseAttributes(' '); + expect(result).toEqual({}); + }); - // Error cases - it('should throw error for invalid format', () => { - expect(() => parseAttributes('invalid')).toThrow('Invalid attribute format'); - expect(() => parseAttributes('key1:value1,invalid')).toThrow('Invalid attribute format'); - }); + // Error cases + it('should throw error for invalid format', () => { + expect(() => parseAttributes('invalid')).toThrow( + 'Invalid attribute format', + ); + expect(() => parseAttributes('key1:value1,invalid')).toThrow( + 'Invalid attribute format', + ); + }); - it('should throw error for empty key', () => { - expect(() => parseAttributes(':value')).toThrow('Attribute key cannot be empty'); - expect(() => parseAttributes('key1:value1,:value2')).toThrow('Attribute key cannot be empty'); - }); + it('should throw error for empty key', () => { + expect(() => parseAttributes(':value')).toThrow( + 'Attribute key cannot be empty', + ); + expect(() => parseAttributes('key1:value1,:value2')).toThrow( + 'Attribute key cannot be empty', + ); + }); - it('should throw error for empty value', () => { - expect(() => parseAttributes('key:')).toThrow('Value for key "key" cannot be empty'); - expect(() => parseAttributes('key1:value1,key2:')).toThrow('Value for key "key2" cannot be empty'); - }); + it('should throw error for empty value', () => { + expect(() => parseAttributes('key:')).toThrow( + 'Value for key "key" cannot be empty', + ); + expect(() => parseAttributes('key1:value1,key2:')).toThrow( + 'Value for key "key2" cannot be empty', + ); + }); - it('should handle special characters in values', () => { - const result = parseAttributes('email:user@example.com,path:/home/user/doc'); - expect(result).toEqual({ - email: 'user@example.com', - path: '/home/user/doc' - }); - }); + it('should handle special characters in values', () => { + const result = parseAttributes( + 'email:user@example.com,path:/home/user/doc', + ); + expect(result).toEqual({ + email: 'user@example.com', + path: '/home/user/doc', + }); + }); - it('should preserve case sensitivity in values', () => { - const result = parseAttributes('name:John,role:Admin'); - expect(result).toEqual({ - name: 'John', - role: 'Admin' - }); - }); -}); \ No newline at end of file + it('should preserve case sensitivity in values', () => { + const result = parseAttributes('name:John,role:Admin'); + expect(result).toEqual({ + name: 'John', + role: 'Admin', + }); + }); +});