diff --git a/README.md b/README.md index 0989a48..6ed1e65 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ export class AppComponent implements OnInit { } login() { - this.oidcSecurityService.authorize(); + this.oidcSecurityService.authorize().subscribe(); } logout() { diff --git a/package.json b/package.json index 71fbacb..5917f1d 100644 --- a/package.json +++ b/package.json @@ -26,15 +26,14 @@ "scripts": { "build": "rslib build", "dev": "rslib build --watch", - "test": "vitest --browser.headless", - "test-ci": "vitest --watch=false --browser.headless --coverage", + "test": "vitest --coverage", + "test-ci": "vitest --watch=false --coverage", "pack": "npm run build && npm pack ./dist", "publish": "npm run build && npm publish ./dist", "coverage": "vitest run --coverage", "lint": "ultracite lint", "format": "ultracite format", - "cli": "tsx scripts/cli.ts", - "test:browser": "vitest --workspace=vitest.workspace.ts" + "cli": "tsx scripts/cli.ts" }, "dependencies": { "@ngify/http": "^2.0.4", @@ -51,11 +50,14 @@ "@evilmartians/lefthook": "^1.0.3", "@playwright/test": "^1.49.1", "@rslib/core": "^0.3.1", + "@swc/core": "^1.10.12", + "@types/jsdom": "^21.1.7", "@types/lodash-es": "^4.17.12", "@types/node": "^22.12.0", "@vitest/browser": "^3.0.4", "@vitest/coverage-v8": "^3.0.4", "commander": "^13.1.0", + "jsdom": "^26.0.0", "lodash-es": "^4.17.21", "oxc-parser": "^0.48.1", "oxc-walker": "^0.2.2", @@ -65,9 +67,9 @@ "tsx": "^4.19.2", "typescript": "^5.7.3", "ultracite": "^4.1.15", + "unplugin-swc": "^1.5.1", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^3.0.4", - "webdriverio": "^9.7.2" + "vitest": "^3.0.4" }, "keywords": [ "rxjs", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 998f0e5..3ee9173 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,12 @@ importers: '@rslib/core': specifier: ^0.3.1 version: 0.3.1(typescript@5.7.3) + '@swc/core': + specifier: ^1.10.12 + version: 1.10.12(@swc/helpers@0.5.15) + '@types/jsdom': + specifier: ^21.1.7 + version: 21.1.7 '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 @@ -51,9 +57,15 @@ importers: commander: specifier: ^13.1.0 version: 13.1.0 + jsdom: + specifier: ^26.0.0 + version: 26.0.0 lodash-es: specifier: ^4.17.21 version: 4.17.21 + mock-local-storage: + specifier: ^1.1.24 + version: 1.1.24 oxc-parser: specifier: ^0.48.1 version: 0.48.1 @@ -78,15 +90,15 @@ importers: ultracite: specifier: ^4.1.15 version: 4.1.15 + unplugin-swc: + specifier: ^1.5.1 + version: 1.5.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(rollup@4.30.1) vite-tsconfig-paths: specifier: ^5.1.4 version: 5.1.4(typescript@5.7.3)(vite@6.0.7(@types/node@22.12.0)(tsx@4.19.2)) vitest: specifier: ^3.0.4 - version: 3.0.4(@types/node@22.12.0)(@vitest/browser@3.0.4)(msw@2.7.0(@types/node@22.12.0)(typescript@5.7.3))(tsx@4.19.2) - webdriverio: - specifier: ^9.7.2 - version: 9.7.2 + version: 3.0.4(@types/node@22.12.0)(@vitest/browser@3.0.4)(jsdom@26.0.0)(msw@2.7.0(@types/node@22.12.0)(typescript@5.7.3))(tsx@4.19.2) packages: @@ -94,6 +106,9 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@asamuzakjp/css-color@2.8.3': + resolution: {integrity: sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==} + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -202,6 +217,34 @@ packages: '@bundled-es-modules/tough-cookie@0.1.6': resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} + '@csstools/color-helpers@5.0.1': + resolution: {integrity: sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.1': + resolution: {integrity: sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-color-parser@3.0.7': + resolution: {integrity: sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.4 + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-parser-algorithms@3.0.4': + resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.3 + + '@csstools/css-tokenizer@3.0.3': + resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} + engines: {node: '>=18'} + '@esbuild/aix-ppc64@0.23.1': resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} engines: {node: '>=18'} @@ -646,6 +689,15 @@ packages: engines: {node: '>=18'} hasBin: true + '@rollup/pluginutils@5.1.4': + resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.30.1': resolution: {integrity: sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==} cpu: [arm] @@ -820,9 +872,84 @@ packages: resolution: {integrity: sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w==} engines: {node: '>=16.0.0'} + '@swc/core-darwin-arm64@1.10.12': + resolution: {integrity: sha512-pOANQegUTAriW7jq3SSMZGM5l89yLVMs48R0F2UG6UZsH04SiViCnDctOGlA/Sa++25C+rL9MGMYM1jDLylBbg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.10.12': + resolution: {integrity: sha512-m4kbpIDDsN1FrwfNQMU+FTrss356xsXvatLbearwR+V0lqOkjLBP0VmRvQfHEg+uy13VPyrT9gj4HLoztlci7w==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.10.12': + resolution: {integrity: sha512-OY9LcupgqEu8zVK+rJPes6LDJJwPDmwaShU96beTaxX2K6VrXbpwm5WbPS/8FfQTsmpnuA7dCcMPUKhNgmzTrQ==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.10.12': + resolution: {integrity: sha512-nJD587rO0N4y4VZszz3xzVr7JIiCzSMhEMWnPjuh+xmPxDBz0Qccpr8xCr1cSxpl1uY7ERkqAGlKr6CwoV5kVg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.10.12': + resolution: {integrity: sha512-oqhSmV+XauSf0C//MoQnVErNUB/5OzmSiUzuazyLsD5pwqKNN+leC3JtRQ/QVzaCpr65jv9bKexT9+I2Tt3xDw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-x64-gnu@1.10.12': + resolution: {integrity: sha512-XldSIHyjD7m1Gh+/8rxV3Ok711ENLI420CU2EGEqSe3VSGZ7pHJvJn9ZFbYpWhsLxPqBYMFjp3Qw+J6OXCPXCA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.10.12': + resolution: {integrity: sha512-wvPXzJxzPgTqhyp1UskOx1hRTtdWxlyFD1cGWOxgLsMik0V9xKRgqKnMPv16Nk7L9xl6quQ6DuUHj9ID7L3oVw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.10.12': + resolution: {integrity: sha512-TUYzWuu1O7uyIcRfxdm6Wh1u+gNnrW5M1DUgDOGZLsyQzgc2Zjwfh2llLhuAIilvCVg5QiGbJlpibRYJ/8QGsg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.10.12': + resolution: {integrity: sha512-4Qrw+0Xt+Fe2rz4OJ/dEPMeUf/rtuFWWAj/e0vL7J5laUHirzxawLRE5DCJLQTarOiYR6mWnmadt9o3EKzV6Xg==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.10.12': + resolution: {integrity: sha512-YiloZXLW7rUxJpALwHXaGjVaAEn+ChoblG7/3esque+Y7QCyheoBUJp2DVM1EeVA43jBfZ8tvYF0liWd9Tpz1A==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.10.12': + resolution: {integrity: sha512-+iUL0PYpPm6N9AdV1wvafakvCqFegQus1aoEDxgFsv3/uNVNIyRaupf/v/Zkp5hbep2EzhtoJR0aiJIzDbXWHg==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '*' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/types@0.1.17': + resolution: {integrity: sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==} + '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -845,6 +972,9 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/jsdom@21.1.7': + resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} + '@types/lodash-es@4.17.12': resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} @@ -1014,6 +1144,9 @@ packages: async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + b4a@1.6.7: resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} @@ -1116,6 +1249,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -1171,6 +1308,10 @@ packages: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} + cssstyle@4.2.1: + resolution: {integrity: sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==} + engines: {node: '>=18'} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -1179,6 +1320,10 @@ packages: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -1192,6 +1337,9 @@ packages: resolution: {integrity: sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + decimal.js@10.5.0: + resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -1204,6 +1352,10 @@ packages: resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} engines: {node: '>= 14'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1214,6 +1366,9 @@ packages: dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dom-walk@0.1.2: + resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} + domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} @@ -1283,6 +1438,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -1336,6 +1494,10 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -1378,6 +1540,9 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + global@4.4.0: + resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} @@ -1398,6 +1563,10 @@ packages: headers-polyfill@4.0.3: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -1451,6 +1620,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -1494,6 +1666,15 @@ packages: jsbn@1.1.0: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + jsdom@26.0.0: + resolution: {integrity: sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} @@ -1504,6 +1685,10 @@ packages: lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + locate-app@2.5.0: resolution: {integrity: sha512-xIqbzPMBYArJRmPGUZD9CzV9wOqmVtQnaAn3wrj3s6WYW0bQvPI7x+sPYUGmDTYMHefVK//zc6HEYZ1qnxIK+Q==} @@ -1553,6 +1738,17 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + min-document@2.19.0: + resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==} + minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} @@ -1568,6 +1764,9 @@ packages: mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + mock-local-storage@1.1.24: + resolution: {integrity: sha512-NEfmw+yEK9oe6xCfOnTaJ6Dz+L3eu6vsZopJlxflXYxr7Mg3EV+S0NXKUQlY9AAeDEdaPZDSUGq1Gi6kLSa5PA==} + mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} @@ -1613,6 +1812,9 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nwsapi@2.2.16: + resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1793,6 +1995,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + rsbuild-plugin-dts@0.3.1: resolution: {integrity: sha512-oeD8ztSn0LBSNhUbIkYIxJKRMjd1Td2Dfhc1RefP0eouxkpnbQ+Dln3V5AVUGfDGK+BlubqEAF1gk3xAzLWA9w==} engines: {node: '>=16.0.0'} @@ -1822,6 +2027,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + semver@7.6.3: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} @@ -1928,6 +2137,9 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tar-fs@3.0.8: resolution: {integrity: sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==} @@ -1966,6 +2178,13 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tldts-core@6.1.75: + resolution: {integrity: sha512-AOvV5YYIAFFBfransBzSTyztkc3IMfz5Eq3YluaRiEu55nn43Fzaufx70UqEKYr8BoLCach4q8g/bg6e5+/aFw==} + + tldts@6.1.75: + resolution: {integrity: sha512-+lFzEXhpl7JXgWYaXcB6DqTYXbUArvrWAE/5ioq/X3CdWLbDjpPP4XTrQBmEJ91y3xbe4Fkw7Lxv4P3GWeJaNg==} + hasBin: true + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -1974,6 +2193,14 @@ packages: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} + tough-cookie@5.1.0: + resolution: {integrity: sha512-rvZUv+7MoBYTiDmFPBrhL7Ujx9Sk+q9wwm22x8c8T5IJaR+Wsyc7TNxbVxo84kZoRJZZMazowFLqpankBEQrGg==} + engines: {node: '>=16'} + + tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + tsconfck@3.1.4: resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==} engines: {node: ^18 || >=20} @@ -2040,6 +2267,11 @@ packages: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} + unplugin-swc@1.5.1: + resolution: {integrity: sha512-/ZLrPNjChhGx3Z95pxJ4tQgfI6rWqukgYHKflrNB4zAV1izOQuDhkTn55JWeivpBxDCoK7M/TStb2aS/14PS/g==} + peerDependencies: + '@swc/core': ^1.2.108 + unplugin@1.16.1: resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} engines: {node: '>=14.0.0'} @@ -2138,6 +2370,10 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + wait-port@1.1.0: resolution: {integrity: sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==} engines: {node: '>=10'} @@ -2160,6 +2396,10 @@ packages: puppeteer-core: optional: true + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -2171,6 +2411,10 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-url@14.1.0: + resolution: {integrity: sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==} + engines: {node: '>=18'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2213,6 +2457,13 @@ packages: utf-8-validate: optional: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -2243,6 +2494,14 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 + '@asamuzakjp/css-color@2.8.3': + dependencies: + '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-color-parser': 3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + lru-cache: 10.4.3 + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.25.9 @@ -2322,6 +2581,26 @@ snapshots: '@types/tough-cookie': 4.0.5 tough-cookie: 4.1.4 + '@csstools/color-helpers@5.0.1': {} + + '@csstools/css-calc@2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-color-parser@3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/color-helpers': 5.0.1 + '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) + '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': + dependencies: + '@csstools/css-tokenizer': 3.0.3 + + '@csstools/css-tokenizer@3.0.3': {} + '@esbuild/aix-ppc64@0.23.1': optional: true @@ -2611,6 +2890,7 @@ snapshots: '@promptbook/utils@0.69.5': dependencies: spacetrim: 0.11.59 + optional: true '@puppeteer/browsers@2.7.0': dependencies: @@ -2625,6 +2905,15 @@ snapshots: transitivePeerDependencies: - bare-buffer - supports-color + optional: true + + '@rollup/pluginutils@5.1.4(rollup@4.30.1)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.30.1 '@rollup/rollup-android-arm-eabi@4.30.1': optional: true @@ -2748,10 +3037,63 @@ snapshots: '@rspack/lite-tapable@1.0.1': {} + '@swc/core-darwin-arm64@1.10.12': + optional: true + + '@swc/core-darwin-x64@1.10.12': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.10.12': + optional: true + + '@swc/core-linux-arm64-gnu@1.10.12': + optional: true + + '@swc/core-linux-arm64-musl@1.10.12': + optional: true + + '@swc/core-linux-x64-gnu@1.10.12': + optional: true + + '@swc/core-linux-x64-musl@1.10.12': + optional: true + + '@swc/core-win32-arm64-msvc@1.10.12': + optional: true + + '@swc/core-win32-ia32-msvc@1.10.12': + optional: true + + '@swc/core-win32-x64-msvc@1.10.12': + optional: true + + '@swc/core@1.10.12(@swc/helpers@0.5.15)': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.17 + optionalDependencies: + '@swc/core-darwin-arm64': 1.10.12 + '@swc/core-darwin-x64': 1.10.12 + '@swc/core-linux-arm-gnueabihf': 1.10.12 + '@swc/core-linux-arm64-gnu': 1.10.12 + '@swc/core-linux-arm64-musl': 1.10.12 + '@swc/core-linux-x64-gnu': 1.10.12 + '@swc/core-linux-x64-musl': 1.10.12 + '@swc/core-win32-arm64-msvc': 1.10.12 + '@swc/core-win32-ia32-msvc': 1.10.12 + '@swc/core-win32-x64-msvc': 1.10.12 + '@swc/helpers': 0.5.15 + + '@swc/counter@0.1.3': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 + '@swc/types@0.1.17': + dependencies: + '@swc/counter': 0.1.3 + '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.26.2 @@ -2767,7 +3109,8 @@ snapshots: dependencies: '@testing-library/dom': 10.4.0 - '@tootallnate/quickjs-emscripten@0.23.0': {} + '@tootallnate/quickjs-emscripten@0.23.0': + optional: true '@types/aria-query@5.0.4': {} @@ -2775,6 +3118,12 @@ snapshots: '@types/estree@1.0.6': {} + '@types/jsdom@21.1.7': + dependencies: + '@types/node': 22.12.0 + '@types/tough-cookie': 4.0.5 + parse5: 7.2.1 + '@types/lodash-es@4.17.12': dependencies: '@types/lodash': 4.17.15 @@ -2784,22 +3133,26 @@ snapshots: '@types/node@20.17.16': dependencies: undici-types: 6.19.8 + optional: true '@types/node@22.12.0': dependencies: undici-types: 6.20.0 - '@types/sinonjs__fake-timers@8.1.5': {} + '@types/sinonjs__fake-timers@8.1.5': + optional: true '@types/statuses@2.0.5': {} '@types/tough-cookie@4.0.5': {} - '@types/which@2.0.2': {} + '@types/which@2.0.2': + optional: true '@types/ws@8.5.14': dependencies: '@types/node': 22.12.0 + optional: true '@types/yauzl@2.10.3': dependencies: @@ -2816,7 +3169,7 @@ snapshots: msw: 2.7.0(@types/node@22.12.0)(typescript@5.7.3) sirv: 3.0.0 tinyrainbow: 2.0.0 - vitest: 3.0.4(@types/node@22.12.0)(@vitest/browser@3.0.4)(msw@2.7.0(@types/node@22.12.0)(typescript@5.7.3))(tsx@4.19.2) + vitest: 3.0.4(@types/node@22.12.0)(@vitest/browser@3.0.4)(jsdom@26.0.0)(msw@2.7.0(@types/node@22.12.0)(typescript@5.7.3))(tsx@4.19.2) ws: 8.18.0 optionalDependencies: playwright: 1.50.0 @@ -2842,7 +3195,7 @@ snapshots: std-env: 3.8.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.0.4(@types/node@22.12.0)(@vitest/browser@3.0.4)(msw@2.7.0(@types/node@22.12.0)(typescript@5.7.3))(tsx@4.19.2) + vitest: 3.0.4(@types/node@22.12.0)(@vitest/browser@3.0.4)(jsdom@26.0.0)(msw@2.7.0(@types/node@22.12.0)(typescript@5.7.3))(tsx@4.19.2) optionalDependencies: '@vitest/browser': 3.0.4(@types/node@22.12.0)(playwright@1.50.0)(typescript@5.7.3)(vite@6.0.7(@types/node@22.12.0)(tsx@4.19.2))(vitest@3.0.4)(webdriverio@9.7.2) transitivePeerDependencies: @@ -2900,6 +3253,7 @@ snapshots: transitivePeerDependencies: - bare-buffer - supports-color + optional: true '@wdio/logger@9.4.4': dependencies: @@ -2907,16 +3261,20 @@ snapshots: loglevel: 1.9.2 loglevel-plugin-prefix: 0.8.4 strip-ansi: 7.1.0 + optional: true - '@wdio/protocols@9.7.0': {} + '@wdio/protocols@9.7.0': + optional: true '@wdio/repl@9.4.4': dependencies: '@types/node': 20.17.16 + optional: true '@wdio/types@9.6.3': dependencies: '@types/node': 20.17.16 + optional: true '@wdio/utils@9.7.2': dependencies: @@ -2936,12 +3294,15 @@ snapshots: transitivePeerDependencies: - bare-buffer - supports-color + optional: true - '@zip.js/zip.js@2.7.56': {} + '@zip.js/zip.js@2.7.56': + optional: true abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 + optional: true acorn@8.14.0: {} @@ -2972,6 +3333,7 @@ snapshots: lodash: 4.17.21 normalize-path: 3.0.0 readable-stream: 4.7.0 + optional: true archiver@7.0.1: dependencies: @@ -2982,6 +3344,7 @@ snapshots: readdir-glob: 1.1.3 tar-stream: 3.1.7 zip-stream: 6.0.1 + optional: true aria-query@5.3.0: dependencies: @@ -2992,10 +3355,15 @@ snapshots: ast-types@0.13.4: dependencies: tslib: 2.8.1 + optional: true - async@3.2.6: {} + async@3.2.6: + optional: true - b4a@1.6.7: {} + asynckit@0.4.0: {} + + b4a@1.6.7: + optional: true balanced-match@1.0.2: {} @@ -3026,29 +3394,36 @@ snapshots: bare-events: 2.5.4 optional: true - base64-js@1.5.1: {} + base64-js@1.5.1: + optional: true - basic-ftp@5.0.5: {} + basic-ftp@5.0.5: + optional: true - boolbase@1.0.0: {} + boolbase@1.0.0: + optional: true brace-expansion@2.0.1: dependencies: balanced-match: 1.0.2 - buffer-crc32@0.2.13: {} + buffer-crc32@0.2.13: + optional: true - buffer-crc32@1.0.0: {} + buffer-crc32@1.0.0: + optional: true buffer@5.7.1: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 + optional: true buffer@6.0.3: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 + optional: true cac@6.7.14: {} @@ -3067,7 +3442,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.4.1: {} + chalk@5.4.1: + optional: true check-error@2.1.1: {} @@ -3079,6 +3455,7 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 domutils: 3.2.2 + optional: true cheerio@1.0.0: dependencies: @@ -3093,6 +3470,7 @@ snapshots: parse5-parser-stream: 7.1.2 undici: 6.21.1 whatwg-mimetype: 4.0.0 + optional: true cli-width@4.1.0: {} @@ -3108,11 +3486,16 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@12.1.0: {} commander@13.1.0: {} - commander@9.5.0: {} + commander@9.5.0: + optional: true compress-commons@6.0.2: dependencies: @@ -3121,6 +3504,7 @@ snapshots: is-stream: 2.0.1 normalize-path: 3.0.0 readable-stream: 4.7.0 + optional: true confbox@0.1.8: {} @@ -3128,14 +3512,17 @@ snapshots: core-js@3.39.0: {} - core-util-is@1.0.3: {} + core-util-is@1.0.3: + optional: true - crc-32@1.2.2: {} + crc-32@1.2.2: + optional: true crc32-stream@6.0.0: dependencies: crc-32: 1.2.2 readable-stream: 4.7.0 + optional: true cross-spawn@7.0.6: dependencies: @@ -3150,32 +3537,55 @@ snapshots: domhandler: 5.0.3 domutils: 3.2.2 nth-check: 2.1.1 + optional: true - css-shorthand-properties@1.1.2: {} + css-shorthand-properties@1.1.2: + optional: true - css-value@0.0.1: {} + css-value@0.0.1: + optional: true - css-what@6.1.0: {} + css-what@6.1.0: + optional: true - data-uri-to-buffer@4.0.1: {} + cssstyle@4.2.1: + dependencies: + '@asamuzakjp/css-color': 2.8.3 + rrweb-cssom: 0.8.0 - data-uri-to-buffer@6.0.2: {} + data-uri-to-buffer@4.0.1: + optional: true + + data-uri-to-buffer@6.0.2: + optional: true + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.1.0 debug@4.4.0: dependencies: ms: 2.1.3 - decamelize@6.0.0: {} + decamelize@6.0.0: + optional: true + + decimal.js@10.5.0: {} deep-eql@5.0.2: {} - deepmerge-ts@7.1.4: {} + deepmerge-ts@7.1.4: + optional: true degenerator@5.0.1: dependencies: ast-types: 0.13.4 escodegen: 2.1.0 esprima: 4.0.1 + optional: true + + delayed-stream@1.0.0: {} dequal@2.0.3: {} @@ -3186,18 +3596,24 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 entities: 4.5.0 + optional: true - domelementtype@2.3.0: {} + dom-walk@0.1.2: {} + + domelementtype@2.3.0: + optional: true domhandler@5.0.3: dependencies: domelementtype: 2.3.0 + optional: true domutils@3.2.2: dependencies: dom-serializer: 2.0.0 domelementtype: 2.3.0 domhandler: 5.0.3 + optional: true eastasianwidth@0.2.0: {} @@ -3205,6 +3621,7 @@ snapshots: dependencies: '@types/which': 2.0.2 which: 2.0.2 + optional: true edgedriver@6.1.1: dependencies: @@ -3219,6 +3636,7 @@ snapshots: which: 5.0.0 transitivePeerDependencies: - supports-color + optional: true emoji-regex@8.0.0: {} @@ -3228,10 +3646,12 @@ snapshots: dependencies: iconv-lite: 0.6.3 whatwg-encoding: 3.1.1 + optional: true end-of-stream@1.4.4: dependencies: once: 1.4.0 + optional: true entities@4.5.0: {} @@ -3301,20 +3721,28 @@ snapshots: esutils: 2.0.3 optionalDependencies: source-map: 0.6.1 + optional: true - esprima@4.0.1: {} + esprima@4.0.1: + optional: true - estraverse@5.3.0: {} + estraverse@5.3.0: + optional: true + + estree-walker@2.0.2: {} estree-walker@3.0.3: dependencies: '@types/estree': 1.0.6 - esutils@2.0.3: {} + esutils@2.0.3: + optional: true - event-target-shim@5.0.1: {} + event-target-shim@5.0.1: + optional: true - events@3.3.0: {} + events@3.3.0: + optional: true expect-type@1.1.0: {} @@ -3327,18 +3755,23 @@ snapshots: '@types/yauzl': 2.10.3 transitivePeerDependencies: - supports-color + optional: true - fast-deep-equal@2.0.1: {} + fast-deep-equal@2.0.1: + optional: true - fast-fifo@1.3.2: {} + fast-fifo@1.3.2: + optional: true fast-xml-parser@4.5.1: dependencies: strnum: 1.0.5 + optional: true fd-slicer@1.1.0: dependencies: pend: 1.2.0 + optional: true fdir@6.4.2(picomatch@4.0.2): optionalDependencies: @@ -3348,15 +3781,23 @@ snapshots: dependencies: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + optional: true foreground-child@3.3.0: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 + optional: true fsevents@2.3.2: optional: true @@ -3377,14 +3818,17 @@ snapshots: transitivePeerDependencies: - bare-buffer - supports-color + optional: true get-caller-file@2.0.5: {} - get-port@7.1.0: {} + get-port@7.1.0: + optional: true get-stream@5.2.0: dependencies: pump: 3.0.2 + optional: true get-tsconfig@4.10.0: dependencies: @@ -3397,6 +3841,7 @@ snapshots: debug: 4.4.0 transitivePeerDependencies: - supports-color + optional: true glob@10.4.5: dependencies: @@ -3407,11 +3852,18 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + global@4.4.0: + dependencies: + min-document: 2.19.0 + process: 0.11.10 + globrex@0.1.2: {} - graceful-fs@4.2.11: {} + graceful-fs@4.2.11: + optional: true - grapheme-splitter@1.0.4: {} + grapheme-splitter@1.0.4: + optional: true graphql@16.10.0: {} @@ -3419,9 +3871,14 @@ snapshots: headers-polyfill@4.0.3: {} + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + html-escaper@2.0.2: {} - htmlfy@0.6.0: {} + htmlfy@0.6.0: + optional: true htmlparser2@9.1.0: dependencies: @@ -3429,6 +3886,7 @@ snapshots: domhandler: 5.0.3 domutils: 3.2.2 entities: 4.5.0 + optional: true http-proxy-agent@7.0.2: dependencies: @@ -3448,13 +3906,17 @@ snapshots: dependencies: safer-buffer: 2.1.2 - ieee754@1.2.1: {} + ieee754@1.2.1: + optional: true - immediate@3.0.6: {} + immediate@3.0.6: + optional: true - import-meta-resolve@4.1.0: {} + import-meta-resolve@4.1.0: + optional: true - inherits@2.0.4: {} + inherits@2.0.4: + optional: true injection-js@https://codeload.github.com/mgechev/injection-js/tar.gz/81a10e0: dependencies: @@ -3464,20 +3926,27 @@ snapshots: dependencies: jsbn: 1.1.0 sprintf-js: 1.1.3 + optional: true is-fullwidth-code-point@3.0.0: {} is-node-process@1.2.0: {} - is-plain-obj@4.1.0: {} + is-plain-obj@4.1.0: + optional: true - is-stream@2.0.1: {} + is-potential-custom-element-name@1.0.1: {} - isarray@1.0.0: {} + is-stream@2.0.1: + optional: true + + isarray@1.0.0: + optional: true isexe@2.0.0: {} - isexe@3.1.1: {} + isexe@3.1.1: + optional: true isomorphic-rslog@0.0.6: {} @@ -3510,7 +3979,36 @@ snapshots: js-tokens@4.0.0: {} - jsbn@1.1.0: {} + jsbn@1.1.0: + optional: true + + jsdom@26.0.0: + dependencies: + cssstyle: 4.2.1 + data-urls: 5.0.0 + decimal.js: 10.5.0 + form-data: 4.0.1 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.16 + parse5: 7.2.1 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.1.0 + ws: 8.18.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate jszip@3.10.1: dependencies: @@ -3518,38 +4016,50 @@ snapshots: pako: 1.0.11 readable-stream: 2.3.8 setimmediate: 1.0.5 + optional: true lazystream@1.0.1: dependencies: readable-stream: 2.3.8 + optional: true lie@3.3.0: dependencies: immediate: 3.0.6 + optional: true + + load-tsconfig@0.2.5: {} locate-app@2.5.0: dependencies: '@promptbook/utils': 0.69.5 type-fest: 4.26.0 userhome: 1.0.1 + optional: true lodash-es@4.17.21: {} - lodash.clonedeep@4.5.0: {} + lodash.clonedeep@4.5.0: + optional: true - lodash.zip@4.2.0: {} + lodash.zip@4.2.0: + optional: true - lodash@4.17.21: {} + lodash@4.17.21: + optional: true - loglevel-plugin-prefix@0.8.4: {} + loglevel-plugin-prefix@0.8.4: + optional: true - loglevel@1.9.2: {} + loglevel@1.9.2: + optional: true loupe@3.1.2: {} lru-cache@10.4.3: {} - lru-cache@7.18.3: {} + lru-cache@7.18.3: + optional: true lz-string@1.5.0: {} @@ -3577,9 +4087,20 @@ snapshots: dependencies: semver: 7.6.3 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + min-document@2.19.0: + dependencies: + dom-walk: 0.1.2 + minimatch@5.1.6: dependencies: brace-expansion: 2.0.1 + optional: true minimatch@9.0.5: dependencies: @@ -3594,6 +4115,11 @@ snapshots: pkg-types: 1.3.1 ufo: 1.5.4 + mock-local-storage@1.1.24: + dependencies: + core-js: 3.39.0 + global: 4.4.0 + mrmime@2.0.0: {} ms@2.1.3: {} @@ -3627,25 +4153,33 @@ snapshots: nanoid@3.3.8: {} - netmask@2.0.2: {} + netmask@2.0.2: + optional: true - node-domexception@1.0.0: {} + node-domexception@1.0.0: + optional: true node-fetch@3.3.2: dependencies: data-uri-to-buffer: 4.0.1 fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + optional: true - normalize-path@3.0.0: {} + normalize-path@3.0.0: + optional: true nth-check@2.1.1: dependencies: boolbase: 1.0.0 + optional: true + + nwsapi@2.2.16: {} once@1.4.0: dependencies: wrappy: 1.0.2 + optional: true outvariant@1.4.3: {} @@ -3680,24 +4214,29 @@ snapshots: socks-proxy-agent: 8.0.5 transitivePeerDependencies: - supports-color + optional: true pac-resolver@7.0.1: dependencies: degenerator: 5.0.1 netmask: 2.0.2 + optional: true package-json-from-dist@1.0.1: {} - pako@1.0.11: {} + pako@1.0.11: + optional: true parse5-htmlparser2-tree-adapter@7.1.0: dependencies: domhandler: 5.0.3 parse5: 7.2.1 + optional: true parse5-parser-stream@7.1.2: dependencies: parse5: 7.2.1 + optional: true parse5@7.2.1: dependencies: @@ -3718,7 +4257,8 @@ snapshots: pathval@2.0.0: {} - pend@1.2.0: {} + pend@1.2.0: + optional: true picocolors@1.1.1: {} @@ -3758,11 +4298,13 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 - process-nextick-args@2.0.1: {} + process-nextick-args@2.0.1: + optional: true process@0.11.10: {} - progress@2.0.3: {} + progress@2.0.3: + optional: true proxy-agent@6.5.0: dependencies: @@ -3776,8 +4318,10 @@ snapshots: socks-proxy-agent: 8.0.5 transitivePeerDependencies: - supports-color + optional: true - proxy-from-env@1.1.0: {} + proxy-from-env@1.1.0: + optional: true psl@1.15.0: dependencies: @@ -3787,10 +4331,12 @@ snapshots: dependencies: end-of-stream: 1.4.4 once: 1.4.0 + optional: true punycode@2.3.1: {} - query-selector-shadow-dom@1.0.1: {} + query-selector-shadow-dom@1.0.1: + optional: true querystringify@2.2.0: {} @@ -3805,6 +4351,7 @@ snapshots: safe-buffer: 5.1.2 string_decoder: 1.1.1 util-deprecate: 1.0.2 + optional: true readable-stream@4.7.0: dependencies: @@ -3813,10 +4360,12 @@ snapshots: events: 3.3.0 process: 0.11.10 string_decoder: 1.3.0 + optional: true readdir-glob@1.1.3: dependencies: minimatch: 5.1.6 + optional: true reflect-metadata@0.2.2: {} @@ -3833,10 +4382,12 @@ snapshots: resq@1.11.0: dependencies: fast-deep-equal: 2.0.1 + optional: true rfc4648@1.5.4: {} - rgb2hex@0.2.5: {} + rgb2hex@0.2.5: + optional: true rollup@4.30.1: dependencies: @@ -3863,6 +4414,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.30.1 fsevents: 2.3.3 + rrweb-cssom@0.8.0: {} + rsbuild-plugin-dts@0.3.1(@rsbuild/core@1.2.0-beta.0)(typescript@5.7.3): dependencies: '@rsbuild/core': 1.2.0-beta.0 @@ -3876,21 +4429,30 @@ snapshots: dependencies: tslib: 2.8.1 - safaridriver@1.0.0: {} + safaridriver@1.0.0: + optional: true - safe-buffer@5.1.2: {} + safe-buffer@5.1.2: + optional: true - safe-buffer@5.2.1: {} + safe-buffer@5.2.1: + optional: true safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + semver@7.6.3: {} serialize-error@11.0.3: dependencies: type-fest: 2.19.0 + optional: true - setimmediate@1.0.5: {} + setimmediate@1.0.5: + optional: true shebang-command@2.0.0: dependencies: @@ -3908,7 +4470,8 @@ snapshots: mrmime: 2.0.0 totalist: 3.0.1 - smart-buffer@4.2.0: {} + smart-buffer@4.2.0: + optional: true socks-proxy-agent@8.0.5: dependencies: @@ -3917,22 +4480,27 @@ snapshots: socks: 2.8.3 transitivePeerDependencies: - supports-color + optional: true socks@2.8.3: dependencies: ip-address: 9.0.5 smart-buffer: 4.2.0 + optional: true source-map-js@1.2.1: {} source-map@0.6.1: optional: true - spacetrim@0.11.59: {} + spacetrim@0.11.59: + optional: true - split2@4.2.0: {} + split2@4.2.0: + optional: true - sprintf-js@1.1.3: {} + sprintf-js@1.1.3: + optional: true stackback@0.0.2: {} @@ -3946,6 +4514,7 @@ snapshots: text-decoder: 1.2.3 optionalDependencies: bare-events: 2.5.4 + optional: true strict-event-emitter@0.5.1: {} @@ -3964,10 +4533,12 @@ snapshots: string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 + optional: true string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 + optional: true strip-ansi@6.0.1: dependencies: @@ -3977,12 +4548,15 @@ snapshots: dependencies: ansi-regex: 6.1.0 - strnum@1.0.5: {} + strnum@1.0.5: + optional: true supports-color@7.2.0: dependencies: has-flag: 4.0.0 + symbol-tree@3.2.4: {} + tar-fs@3.0.8: dependencies: pump: 3.0.2 @@ -3992,12 +4566,14 @@ snapshots: bare-path: 3.0.0 transitivePeerDependencies: - bare-buffer + optional: true tar-stream@3.1.7: dependencies: b4a: 1.6.7 fast-fifo: 1.3.2 streamx: 2.22.0 + optional: true test-exclude@7.0.1: dependencies: @@ -4008,8 +4584,10 @@ snapshots: text-decoder@1.2.3: dependencies: b4a: 1.6.7 + optional: true - through@2.3.8: {} + through@2.3.8: + optional: true tinybench@2.9.0: {} @@ -4026,6 +4604,12 @@ snapshots: tinyspy@3.0.2: {} + tldts-core@6.1.75: {} + + tldts@6.1.75: + dependencies: + tldts-core: 6.1.75 + totalist@3.0.1: {} tough-cookie@4.1.4: @@ -4035,6 +4619,14 @@ snapshots: universalify: 0.2.0 url-parse: 1.5.10 + tough-cookie@5.1.0: + dependencies: + tldts: 6.1.75 + + tr46@5.0.0: + dependencies: + punycode: 2.3.1 + tsconfck@3.1.4(typescript@5.7.3): optionalDependencies: typescript: 5.7.3 @@ -4050,9 +4642,11 @@ snapshots: type-fest@0.21.3: {} - type-fest@2.19.0: {} + type-fest@2.19.0: + optional: true - type-fest@4.26.0: {} + type-fest@4.26.0: + optional: true type-fest@4.33.0: {} @@ -4070,15 +4664,27 @@ snapshots: dependencies: buffer: 5.7.1 through: 2.3.8 + optional: true - undici-types@6.19.8: {} + undici-types@6.19.8: + optional: true undici-types@6.20.0: {} - undici@6.21.1: {} + undici@6.21.1: + optional: true universalify@0.2.0: {} + unplugin-swc@1.5.1(@swc/core@1.10.12(@swc/helpers@0.5.15))(rollup@4.30.1): + dependencies: + '@rollup/pluginutils': 5.1.4(rollup@4.30.1) + '@swc/core': 1.10.12(@swc/helpers@0.5.15) + load-tsconfig: 0.2.5 + unplugin: 1.16.1 + transitivePeerDependencies: + - rollup + unplugin@1.16.1: dependencies: acorn: 8.14.0 @@ -4089,11 +4695,14 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 - urlpattern-polyfill@10.0.0: {} + urlpattern-polyfill@10.0.0: + optional: true - userhome@1.0.1: {} + userhome@1.0.1: + optional: true - util-deprecate@1.0.2: {} + util-deprecate@1.0.2: + optional: true vite-node@3.0.4(@types/node@22.12.0)(tsx@4.19.2): dependencies: @@ -4137,7 +4746,7 @@ snapshots: fsevents: 2.3.3 tsx: 4.19.2 - vitest@3.0.4(@types/node@22.12.0)(@vitest/browser@3.0.4)(msw@2.7.0(@types/node@22.12.0)(typescript@5.7.3))(tsx@4.19.2): + vitest@3.0.4(@types/node@22.12.0)(@vitest/browser@3.0.4)(jsdom@26.0.0)(msw@2.7.0(@types/node@22.12.0)(typescript@5.7.3))(tsx@4.19.2): dependencies: '@vitest/expect': 3.0.4 '@vitest/mocker': 3.0.4(msw@2.7.0(@types/node@22.12.0)(typescript@5.7.3))(vite@6.0.7(@types/node@22.12.0)(tsx@4.19.2)) @@ -4162,6 +4771,7 @@ snapshots: optionalDependencies: '@types/node': 22.12.0 '@vitest/browser': 3.0.4(@types/node@22.12.0)(playwright@1.50.0)(typescript@5.7.3)(vite@6.0.7(@types/node@22.12.0)(tsx@4.19.2))(vitest@3.0.4)(webdriverio@9.7.2) + jsdom: 26.0.0 transitivePeerDependencies: - jiti - less @@ -4176,6 +4786,10 @@ snapshots: - tsx - yaml + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + wait-port@1.1.0: dependencies: chalk: 4.1.2 @@ -4183,8 +4797,10 @@ snapshots: debug: 4.4.0 transitivePeerDependencies: - supports-color + optional: true - web-streams-polyfill@3.3.3: {} + web-streams-polyfill@3.3.3: + optional: true webdriver@9.7.2: dependencies: @@ -4203,6 +4819,7 @@ snapshots: - bufferutil - supports-color - utf-8-validate + optional: true webdriverio@9.7.2: dependencies: @@ -4238,6 +4855,9 @@ snapshots: - bufferutil - supports-color - utf-8-validate + optional: true + + webidl-conversions@7.0.0: {} webpack-virtual-modules@0.6.2: {} @@ -4247,6 +4867,11 @@ snapshots: whatwg-mimetype@4.0.0: {} + whatwg-url@14.1.0: + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -4254,6 +4879,7 @@ snapshots: which@5.0.0: dependencies: isexe: 3.1.1 + optional: true why-is-node-running@2.3.0: dependencies: @@ -4278,10 +4904,15 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 - wrappy@1.0.2: {} + wrappy@1.0.2: + optional: true ws@8.18.0: {} + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + y18n@5.0.8: {} yargs-parser@21.1.1: {} @@ -4300,6 +4931,7 @@ snapshots: dependencies: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 + optional: true yoctocolors-cjs@2.1.2: {} @@ -4308,3 +4940,4 @@ snapshots: archiver-utils: 5.0.2 compress-commons: 6.0.2 readable-stream: 4.7.0 + optional: true diff --git a/scripts/cli.ts b/scripts/cli.ts index 3f257c8..ef77210 100644 --- a/scripts/cli.ts +++ b/scripts/cli.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import { Command } from 'commander'; -import { rewriteAllObservableSubscribeToLastValueFrom } from './code-transform'; +import { rewriteAllObservableSubscribeTofirstValueFrom } from './code-transform'; const program = new Command(); @@ -13,7 +13,7 @@ program .command('rewrite ') .description('Rewrite files matching the given glob pattern') .action(async (pattern: string) => { - await rewriteAllObservableSubscribeToLastValueFrom(pattern); + await rewriteAllObservableSubscribeTofirstValueFrom(pattern); }); program.parse(process.argv); diff --git a/scripts/code-transform.spec.ts b/scripts/code-transform.spec.ts index aac88fc..a4fed4a 100644 --- a/scripts/code-transform.spec.ts +++ b/scripts/code-transform.spec.ts @@ -1,11 +1,11 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { Biome, Distribution } from '@biomejs/js-api'; -import { rewriteObservableSubscribeToLastValueFrom } from './code-transform'; +import { rewriteObservableSubscribeTofirstValueFrom } from './code-transform'; -describe('rewriteSpecObservableSubscribeToLastValueFrom', () => { +describe('rewriteSpecObservableSubscribeTofirstValueFrom', () => { it('should transform simple example valid string', async () => { - const actual = await rewriteObservableSubscribeToLastValueFrom( + const actual = await rewriteObservableSubscribeTofirstValueFrom( 'index.ts', `refreshSessionIframeService .refreshSessionWithIframe(allConfigs[0]!, allConfigs) @@ -20,7 +20,7 @@ describe('rewriteSpecObservableSubscribeToLastValueFrom', () => { });` ); - const expect = `const result = await lastValueFrom(refreshSessionIframeService.refreshSessionWithIframe(allConfigs[0]!, allConfigs)); + const expect = `const result = await firstValueFrom(refreshSessionIframeService.refreshSessionWithIframe(allConfigs[0]!, allConfigs)); expect(result).toHaveBeenCalledExactlyOnceWith('a-url',allConfigs[0]!,allConfigs);`; const biome = await Biome.create({ @@ -34,7 +34,7 @@ describe('rewriteSpecObservableSubscribeToLastValueFrom', () => { }); it('should rewrite complex exmaple to valid string', async () => { - const actual = await rewriteObservableSubscribeToLastValueFrom( + const actual = await rewriteObservableSubscribeTofirstValueFrom( 'index.ts', `codeFlowCallbackService .authenticatedCallbackWithCode('some-url4', config, [config]) @@ -56,7 +56,7 @@ describe('rewriteSpecObservableSubscribeToLastValueFrom', () => { const expect = ` try { - const abc = await lastValueFrom(codeFlowCallbackService.authenticatedCallbackWithCode('some-url4', config, [config])); + const abc = await firstValueFrom(codeFlowCallbackService.authenticatedCallbackWithCode('some-url4', config, [config])); expect(abc).toBeTruthy(); } catch (err: any) { if (err instanceof EmptyError) { diff --git a/scripts/code-transform.ts b/scripts/code-transform.ts index 0f5a357..86a81ae 100644 --- a/scripts/code-transform.ts +++ b/scripts/code-transform.ts @@ -21,7 +21,7 @@ function sourceTextFromNode( return magicString.getSourceText(start, end); } -export async function rewriteObservableSubscribeToLastValueFrom( +export async function rewriteObservableSubscribeTofirstValueFrom( filename: string, content?: string ) { @@ -83,7 +83,7 @@ export async function rewriteObservableSubscribeToLastValueFrom( error = args[1]; complete = args[2]; } - let newContent = `await lastValueFrom(${sourceTextFromNode(context, child.expression.callee.object)});`; + let newContent = `await firstValueFrom(${sourceTextFromNode(context, child.expression.callee.object)});`; if (next) { const nextParam = @@ -161,12 +161,12 @@ export async function rewriteObservableSubscribeToLastValueFrom( return result; } -export async function rewriteAllObservableSubscribeToLastValueFrom( +export async function rewriteAllObservableSubscribeTofirstValueFrom( pattern: string | string[] ) { const files = fsp.glob(pattern); for await (const file of files) { - const result = await rewriteObservableSubscribeToLastValueFrom(file); + const result = await rewriteObservableSubscribeTofirstValueFrom(file); await fsp.writeFile(file, result, 'utf-8'); } diff --git a/src/api/data.service.spec.ts b/src/api/data.service.spec.ts index 8caadb5..8961a48 100644 --- a/src/api/data.service.spec.ts +++ b/src/api/data.service.spec.ts @@ -3,7 +3,7 @@ import { provideHttpClientTesting } from '@/testing/http'; import { HttpHeaders } from '@ngify/http'; import { HttpTestingController } from '@ngify/http/testing'; import { provideHttpClient, withInterceptorsFromDi } from 'oidc-client-rx'; -import { lastValueFrom } from 'rxjs'; +import { firstValueFrom } from 'rxjs'; import { DataService } from './data.service'; import { HttpBaseService } from './http-base.service'; @@ -33,7 +33,7 @@ describe('Data Service', () => { it('get call sets the accept header', async () => { const url = 'testurl'; - const data = await lastValueFrom( + const data = await firstValueFrom( dataService.get(url, { configId: 'configId1' }) ); expect(data).toBe('bodyData'); @@ -51,7 +51,7 @@ describe('Data Service', () => { const url = 'testurl'; const token = 'token'; - const data = await lastValueFrom( + const data = await firstValueFrom( dataService.get(url, { configId: 'configId1' }, token) ); expect(data).toBe('bodyData'); @@ -69,7 +69,7 @@ describe('Data Service', () => { it('call without ngsw-bypass param by default', async () => { const url = 'testurl'; - const data = await lastValueFrom( + const data = await firstValueFrom( dataService.get(url, { configId: 'configId1' }) ); expect(data).toBe('bodyData'); @@ -87,7 +87,7 @@ describe('Data Service', () => { it('call with ngsw-bypass param', async () => { const url = 'testurl'; - const data = await lastValueFrom( + const data = await firstValueFrom( dataService.get(url, { configId: 'configId1', ngswBypass: true }) ); expect(data).toBe('bodyData'); @@ -107,8 +107,9 @@ describe('Data Service', () => { it('call sets the accept header when no other params given', async () => { const url = 'testurl'; - await lastValueFrom(dataService - .post(url, { some: 'thing' }, { configId: 'configId1' })); + await firstValueFrom( + dataService.post(url, { some: 'thing' }, { configId: 'configId1' }) + ); const req = httpMock.expectOne(url); expect(req.request.method).toBe('POST'); @@ -125,7 +126,7 @@ describe('Data Service', () => { headers = headers.set('X-MyHeader', 'Genesis'); - await lastValueFrom( + await firstValueFrom( dataService.post( url, { some: 'thing' }, @@ -147,7 +148,7 @@ describe('Data Service', () => { it('call without ngsw-bypass param by default', async () => { const url = 'testurl'; - await lastValueFrom( + await firstValueFrom( dataService.post(url, { some: 'thing' }, { configId: 'configId1' }) ); const req = httpMock.expectOne(url); @@ -164,7 +165,7 @@ describe('Data Service', () => { it('call with ngsw-bypass param', async () => { const url = 'testurl'; - await lastValueFrom( + await firstValueFrom( dataService.post( url, { some: 'thing' }, diff --git a/src/api/data.service.ts b/src/api/data.service.ts index 6e92abc..cfb7297 100644 --- a/src/api/data.service.ts +++ b/src/api/data.service.ts @@ -1,7 +1,8 @@ -import { HttpHeaders, HttpParams } from '@ngify/http'; +import { HttpHeaders } from '@ngify/http'; import { Injectable, inject } from 'injection-js'; import type { Observable } from 'rxjs'; import type { OpenIdConfiguration } from '../config/openid-configuration'; +import { HttpParams } from '../http'; import { HttpBaseService } from './http-base.service'; const NGSW_CUSTOM_PARAM = 'ngsw-bypass'; diff --git a/src/auth-state/auth-state.service.spec.ts b/src/auth-state/auth-state.service.spec.ts index 84826d2..f4a42c2 100644 --- a/src/auth-state/auth-state.service.spec.ts +++ b/src/auth-state/auth-state.service.spec.ts @@ -257,7 +257,7 @@ describe('Auth State Service', () => { [{ configId: 'configId1' }] ); expect(spy).toHaveBeenCalledTimes(2); - expect(spy).toHaveBeenCalledWith([ + expect(spy.mock.calls).toEqual([ ['authzData', 'accesstoken', { configId: 'configId1' }], [ 'access_token_expires_at', diff --git a/src/auth-state/check-auth.service.spec.ts b/src/auth-state/check-auth.service.spec.ts index ec3ec73..1e9d58f 100644 --- a/src/auth-state/check-auth.service.spec.ts +++ b/src/auth-state/check-auth.service.spec.ts @@ -4,7 +4,7 @@ import { mockRouterProvider, spyOnProperty, } from '@/testing'; -import { lastValueFrom, of, throwError } from 'rxjs'; +import { firstValueFrom, of, throwError } from 'rxjs'; import { vi } from 'vitest'; import { AutoLoginService } from '../auto-login/auto-login.service'; import { CallbackService } from '../callback/callback.service'; @@ -49,6 +49,8 @@ describe('CheckAuthService', () => { TestBed.configureTestingModule({ imports: [], providers: [ + CheckAuthService, + StoragePersistenceService, mockRouterProvider(), mockProvider(CheckSessionService), mockProvider(SilentRenewService), @@ -109,7 +111,7 @@ describe('CheckAuthService', () => { ); const spy = vi.spyOn(checkAuthService as any, 'checkAuthWithConfig'); - await lastValueFrom( + await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); expect(spy).toHaveBeenCalledExactlyOnceWith( @@ -136,7 +138,7 @@ describe('CheckAuthService', () => { const spy = vi.spyOn(checkAuthService as any, 'checkAuthWithConfig'); try { - await lastValueFrom( + await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); } catch (err: any) { @@ -155,7 +157,7 @@ describe('CheckAuthService', () => { ]; const spy = vi.spyOn(checkAuthService as any, 'checkAuthWithConfig'); - await lastValueFrom( + await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); expect(spy).toHaveBeenCalledExactlyOnceWith( @@ -184,7 +186,7 @@ describe('CheckAuthService', () => { vi.spyOn(popUpService, 'isCurrentlyInPopup').mockReturnValue(true); const popupSpy = vi.spyOn(popUpService, 'sendMessageToMainWindow'); - const result = await lastValueFrom( + const result = await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); expect(result).toEqual({ @@ -216,7 +218,7 @@ describe('CheckAuthService', () => { 'http://localhost:4200' ); - const result = await lastValueFrom( + const result = await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); expect(result).toEqual({ @@ -249,7 +251,7 @@ describe('CheckAuthService', () => { .spyOn(callBackService, 'handleCallbackAndFireEvents') .mockReturnValue(of({} as CallbackContext)); - const result = await lastValueFrom( + const result = await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); expect(result).toEqual({ @@ -282,7 +284,7 @@ describe('CheckAuthService', () => { vi.spyOn(authStateService, 'getAccessToken').mockReturnValue('at'); vi.spyOn(authStateService, 'getIdToken').mockReturnValue('idt'); - const result = await lastValueFrom( + const result = await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); expect(result).toEqual({ @@ -322,7 +324,7 @@ describe('CheckAuthService', () => { ); const userServiceSpy = vi.spyOn(userService, 'publishUserDataIfExists'); - const result = await lastValueFrom( + const result = await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); expect(result).toEqual({ @@ -362,7 +364,7 @@ describe('CheckAuthService', () => { ); const userServiceSpy = vi.spyOn(userService, 'publishUserDataIfExists'); - const result = await lastValueFrom( + const result = await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); expect(result).toEqual({ @@ -393,7 +395,7 @@ describe('CheckAuthService', () => { true ); - const result = await lastValueFrom( + const result = await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); expect(result).toEqual({ @@ -420,7 +422,7 @@ describe('CheckAuthService', () => { const spy = vi.spyOn(authStateService, 'setAuthenticatedAndFireEvent'); - await lastValueFrom( + await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); expect(spy).toHaveBeenCalled(); @@ -443,7 +445,7 @@ describe('CheckAuthService', () => { const spy = vi.spyOn(userService, 'publishUserDataIfExists'); - await lastValueFrom( + await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); expect(spy).toHaveBeenCalled(); @@ -470,7 +472,7 @@ describe('CheckAuthService', () => { 'startTokenValidationPeriodically' ); - await lastValueFrom( + await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); expect(spy).toHaveBeenCalled(); @@ -495,7 +497,7 @@ describe('CheckAuthService', () => { ); const spy = vi.spyOn(checkSessionService, 'start'); - await lastValueFrom( + await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); expect(spy).toHaveBeenCalled(); @@ -520,7 +522,7 @@ describe('CheckAuthService', () => { ); const spy = vi.spyOn(silentRenewService, 'getOrCreateIframe'); - await lastValueFrom( + await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); expect(spy).toHaveBeenCalled(); @@ -545,7 +547,7 @@ describe('CheckAuthService', () => { 'checkSavedRedirectRouteAndNavigate' ); - await lastValueFrom( + await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); expect(spy).toHaveBeenCalledTimes(1); @@ -568,7 +570,7 @@ describe('CheckAuthService', () => { 'checkSavedRedirectRouteAndNavigate' ); - await lastValueFrom( + await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); expect(spy).toHaveBeenCalledTimes(0); @@ -588,10 +590,10 @@ describe('CheckAuthService', () => { const fireEventSpy = vi.spyOn(publicEventsService, 'fireEvent'); - await lastValueFrom( + await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); - expect(fireEventSpy).toHaveBeenCalledWith([ + expect(fireEventSpy.mock.calls).toEqual([ [EventTypes.CheckingAuth], [EventTypes.CheckingAuthFinished], ]); @@ -611,10 +613,10 @@ describe('CheckAuthService', () => { 'http://localhost:4200' ); - await lastValueFrom( + await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); - expect(fireEventSpy).toHaveBeenCalledWith([ + expect(fireEventSpy.mock.calls).toEqual([ [EventTypes.CheckingAuth], [EventTypes.CheckingAuthFinishedWithError, 'ERROR'], ]); @@ -634,10 +636,10 @@ describe('CheckAuthService', () => { const fireEventSpy = vi.spyOn(publicEventsService, 'fireEvent'); - await lastValueFrom( + await firstValueFrom( checkAuthService.checkAuth(allConfigs[0]!, allConfigs) ); - expect(fireEventSpy).toBeCalledWith([ + expect(fireEventSpy.mock.calls).toEqual([ [EventTypes.CheckingAuth], [EventTypes.CheckingAuthFinished], ]); @@ -665,7 +667,7 @@ describe('CheckAuthService', () => { ); const spy = vi.spyOn(silentRenewService, 'getOrCreateIframe'); - await lastValueFrom( + await firstValueFrom( checkAuthService.checkAuthIncludingServer(allConfigs[0]!, allConfigs) ); expect(spy).toHaveBeenCalled(); @@ -694,7 +696,7 @@ describe('CheckAuthService', () => { }) ); - const result = await lastValueFrom( + const result = await firstValueFrom( checkAuthService.checkAuthIncludingServer(allConfigs[0]!, allConfigs) ); expect(result).toBeTruthy(); @@ -742,7 +744,7 @@ describe('CheckAuthService', () => { }) ); - await lastValueFrom( + await firstValueFrom( checkAuthService.checkAuthIncludingServer(allConfigs[0]!, allConfigs) ); expect(checkSessionServiceStartSpy).toHaveBeenCalledExactlyOnceWith( @@ -796,7 +798,7 @@ describe('CheckAuthService', () => { }) ); - await lastValueFrom( + await firstValueFrom( checkAuthService.checkAuthIncludingServer(allConfigs[0]!, allConfigs) ); expect(checkSessionServiceStartSpy).toHaveBeenCalledExactlyOnceWith( @@ -825,7 +827,7 @@ describe('CheckAuthService', () => { ); const spy = vi.spyOn(checkAuthService as any, 'checkAuthWithConfig'); - const result = await lastValueFrom( + const result = await firstValueFrom( checkAuthService.checkAuthMultiple(allConfigs) ); expect(Array.isArray(result)).toBe(true); @@ -855,11 +857,11 @@ describe('CheckAuthService', () => { const spy = vi.spyOn(checkAuthService as any, 'checkAuthWithConfig'); - const result = await lastValueFrom( + const result = await firstValueFrom( checkAuthService.checkAuthMultiple(allConfigs) ); expect(Array.isArray(result)).toBe(true); - expect(spy).toBeCalledWith([ + expect(spy.mock.calls).toEqual([ [ { configId: 'configId1', authority: 'some-authority1' }, allConfigs, @@ -886,7 +888,7 @@ describe('CheckAuthService', () => { const spy = vi.spyOn(checkAuthService as any, 'checkAuthWithConfig'); - const result = await lastValueFrom( + const result = await firstValueFrom( checkAuthService.checkAuthMultiple(allConfigs) ); expect(Array.isArray(result)).toBe(true); @@ -912,7 +914,7 @@ describe('CheckAuthService', () => { const allConfigs: OpenIdConfiguration[] = []; try { - await lastValueFrom(checkAuthService.checkAuthMultiple(allConfigs)); + await firstValueFrom(checkAuthService.checkAuthMultiple(allConfigs)); } catch (error: any) { expect(error.message).toEqual( 'could not find matching config for state the-state-param' diff --git a/src/auto-login/auto-login-partial-routes.guard.spec.ts b/src/auto-login/auto-login-partial-routes.guard.spec.ts index 35cd4c0..4557bf7 100644 --- a/src/auto-login/auto-login-partial-routes.guard.spec.ts +++ b/src/auto-login/auto-login-partial-routes.guard.spec.ts @@ -4,7 +4,7 @@ import { type ActivatedRouteSnapshot, type RouterStateSnapshot, } from 'oidc-client-rx'; -import { lastValueFrom, of } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { vi } from 'vitest'; import { AuthStateService } from '../auth-state/auth-state.service'; import { CheckAuthService } from '../auth-state/check-auth.service'; @@ -24,6 +24,7 @@ describe('AutoLoginPartialRoutesGuard', () => { TestBed.configureTestingModule({ imports: [], providers: [ + AutoLoginPartialRoutesGuard, mockRouterProvider(), AutoLoginService, mockProvider(AuthStateService), @@ -83,21 +84,20 @@ describe('AutoLoginPartialRoutesGuard', () => { ); const loginSpy = vi.spyOn(loginService, 'login'); - await lastValueFrom(guard - .canActivate( + await firstValueFrom( + guard.canActivate( {} as ActivatedRouteSnapshot, { url: 'some-url1' } as RouterStateSnapshot - )); -expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( - { configId: 'configId1' }, - 'some-url1' - );; -expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ - configId: 'configId1', - });; -expect( - checkSavedRedirectRouteAndNavigateSpy - ).not.toHaveBeenCalled(); + ) + ); + expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( + { configId: 'configId1' }, + 'some-url1' + ); + expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ + configId: 'configId1', + }); + expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); }); it('should save current route and call `login` if not authenticated already and add custom params', async () => { @@ -114,22 +114,21 @@ expect( ); const loginSpy = vi.spyOn(loginService, 'login'); - await lastValueFrom(guard - .canActivate( + await firstValueFrom( + guard.canActivate( { data: { custom: 'param' } } as unknown as ActivatedRouteSnapshot, { url: 'some-url1' } as RouterStateSnapshot - )); -expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( - { configId: 'configId1' }, - 'some-url1' - );; -expect(loginSpy).toHaveBeenCalledExactlyOnceWith( - { configId: 'configId1' }, - { customParams: { custom: 'param' } } - );; -expect( - checkSavedRedirectRouteAndNavigateSpy - ).not.toHaveBeenCalled(); + ) + ); + expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( + { configId: 'configId1' }, + 'some-url1' + ); + expect(loginSpy).toHaveBeenCalledExactlyOnceWith( + { configId: 'configId1' }, + { customParams: { custom: 'param' } } + ); + expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); }); it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => { @@ -146,16 +145,17 @@ expect( ); const loginSpy = vi.spyOn(loginService, 'login'); - await lastValueFrom(guard - .canActivate( + await firstValueFrom( + guard.canActivate( {} as ActivatedRouteSnapshot, { url: 'some-url1' } as RouterStateSnapshot - )); -expect(saveRedirectRouteSpy).not.toHaveBeenCalled();; -expect(loginSpy).not.toHaveBeenCalled();; -expect( - checkSavedRedirectRouteAndNavigateSpy - ).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' }); + ) + ); + expect(saveRedirectRouteSpy).not.toHaveBeenCalled(); + expect(loginSpy).not.toHaveBeenCalled(); + expect( + checkSavedRedirectRouteAndNavigateSpy + ).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' }); }); }); @@ -174,21 +174,20 @@ expect( ); const loginSpy = vi.spyOn(loginService, 'login'); - await lastValueFrom(guard - .canActivateChild( + await firstValueFrom( + guard.canActivateChild( {} as ActivatedRouteSnapshot, { url: 'some-url1' } as RouterStateSnapshot - )); -expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( - { configId: 'configId1' }, - 'some-url1' - );; -expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ - configId: 'configId1', - });; -expect( - checkSavedRedirectRouteAndNavigateSpy - ).not.toHaveBeenCalled(); + ) + ); + expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( + { configId: 'configId1' }, + 'some-url1' + ); + expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ + configId: 'configId1', + }); + expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); }); it('should save current route and call `login` if not authenticated already with custom params', async () => { @@ -205,22 +204,21 @@ expect( ); const loginSpy = vi.spyOn(loginService, 'login'); - await lastValueFrom(guard - .canActivateChild( + await firstValueFrom( + guard.canActivateChild( { data: { custom: 'param' } } as unknown as ActivatedRouteSnapshot, { url: 'some-url1' } as RouterStateSnapshot - )); -expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( - { configId: 'configId1' }, - 'some-url1' - );; -expect(loginSpy).toHaveBeenCalledExactlyOnceWith( - { configId: 'configId1' }, - { customParams: { custom: 'param' } } - );; -expect( - checkSavedRedirectRouteAndNavigateSpy - ).not.toHaveBeenCalled(); + ) + ); + expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( + { configId: 'configId1' }, + 'some-url1' + ); + expect(loginSpy).toHaveBeenCalledExactlyOnceWith( + { configId: 'configId1' }, + { customParams: { custom: 'param' } } + ); + expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); }); it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => { @@ -237,16 +235,17 @@ expect( ); const loginSpy = vi.spyOn(loginService, 'login'); - await lastValueFrom(guard - .canActivateChild( + await firstValueFrom( + guard.canActivateChild( {} as ActivatedRouteSnapshot, { url: 'some-url1' } as RouterStateSnapshot - )); -expect(saveRedirectRouteSpy).not.toHaveBeenCalled();; -expect(loginSpy).not.toHaveBeenCalled();; -expect( - checkSavedRedirectRouteAndNavigateSpy - ).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' }); + ) + ); + expect(saveRedirectRouteSpy).not.toHaveBeenCalled(); + expect(loginSpy).not.toHaveBeenCalled(); + expect( + checkSavedRedirectRouteAndNavigateSpy + ).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' }); }); }); @@ -265,15 +264,15 @@ expect( ); const loginSpy = vi.spyOn(loginService, 'login'); - await lastValueFrom(guard.canLoad()); -expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( - { configId: 'configId1' }, - '' - );; -expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ - configId: 'configId1', - });; -expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); + await firstValueFrom(guard.canLoad()); + expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( + { configId: 'configId1' }, + '' + ); + expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ + configId: 'configId1', + }); + expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); }); it('should save current route (with router extractedUrl) and call `login` if not authenticated already', async () => { @@ -301,15 +300,15 @@ expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); trigger: 'imperative', }); - await lastValueFrom(guard.canLoad()); -expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( - { configId: 'configId1' }, - 'some-url12/with/some-param?queryParam=true' - );; -expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ - configId: 'configId1', - });; -expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); + await firstValueFrom(guard.canLoad()); + expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( + { configId: 'configId1' }, + 'some-url12/with/some-param?queryParam=true' + ); + expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ + configId: 'configId1', + }); + expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); }); it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => { @@ -326,12 +325,12 @@ expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); ); const loginSpy = vi.spyOn(loginService, 'login'); - await lastValueFrom(guard.canLoad()); -expect(saveRedirectRouteSpy).not.toHaveBeenCalled();; -expect(loginSpy).not.toHaveBeenCalled();; -expect( - checkSavedRedirectRouteAndNavigateSpy - ).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' }); + await firstValueFrom(guard.canLoad()); + expect(saveRedirectRouteSpy).not.toHaveBeenCalled(); + expect(loginSpy).not.toHaveBeenCalled(); + expect( + checkSavedRedirectRouteAndNavigateSpy + ).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' }); }); }); }); @@ -383,15 +382,15 @@ expect( autoLoginPartialRoutesGuard ); - await lastValueFrom(guard$); -expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( - { configId: 'configId1' }, - '' - );; -expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ - configId: 'configId1', - });; -expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); + await firstValueFrom(guard$); + expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( + { configId: 'configId1' }, + '' + ); + expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ + configId: 'configId1', + }); + expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); }); it('should save current route (with router extractedUrl) and call `login` if not authenticated already', async () => { @@ -423,15 +422,15 @@ expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); autoLoginPartialRoutesGuard ); - await lastValueFrom(guard$); -expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( - { configId: 'configId1' }, - 'some-url12/with/some-param?queryParam=true' - );; -expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ - configId: 'configId1', - });; -expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); + await firstValueFrom(guard$); + expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( + { configId: 'configId1' }, + 'some-url12/with/some-param?queryParam=true' + ); + expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ + configId: 'configId1', + }); + expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); }); it('should save current route and call `login` if not authenticated already and add custom params', async () => { @@ -454,16 +453,16 @@ expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); } as unknown as ActivatedRouteSnapshot) ); - await lastValueFrom(guard$); -expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( - { configId: 'configId1' }, - '' - );; -expect(loginSpy).toHaveBeenCalledExactlyOnceWith( - { configId: 'configId1' }, - { customParams: { custom: 'param' } } - );; -expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); + await firstValueFrom(guard$); + expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( + { configId: 'configId1' }, + '' + ); + expect(loginSpy).toHaveBeenCalledExactlyOnceWith( + { configId: 'configId1' }, + { customParams: { custom: 'param' } } + ); + expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); }); it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => { @@ -484,12 +483,12 @@ expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); autoLoginPartialRoutesGuard ); - await lastValueFrom(guard$); -expect(saveRedirectRouteSpy).not.toHaveBeenCalled();; -expect(loginSpy).not.toHaveBeenCalled();; -expect( - checkSavedRedirectRouteAndNavigateSpy - ).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' }); + await firstValueFrom(guard$); + expect(saveRedirectRouteSpy).not.toHaveBeenCalled(); + expect(loginSpy).not.toHaveBeenCalled(); + expect( + checkSavedRedirectRouteAndNavigateSpy + ).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' }); }); }); @@ -537,15 +536,15 @@ expect( autoLoginPartialRoutesGuardWithConfig('configId1') ); - await lastValueFrom(guard$); -expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( - { configId: 'configId1' }, - '' - );; -expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ - configId: 'configId1', - });; -expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); + await firstValueFrom(guard$); + expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( + { configId: 'configId1' }, + '' + ); + expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ + configId: 'configId1', + }); + expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); }); }); }); diff --git a/src/callback/callback.service.spec.ts b/src/callback/callback.service.spec.ts index 68fbda9..604421a 100644 --- a/src/callback/callback.service.spec.ts +++ b/src/callback/callback.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@/testing'; -import { Observable, lastValueFrom, of } from 'rxjs'; +import { Observable, firstValueFrom, of } from 'rxjs'; import { vi } from 'vitest'; import type { CallbackContext } from '../flows/callback-context'; import { mockProvider } from '../testing/mock'; @@ -59,7 +59,7 @@ describe('CallbackService ', () => { .spyOn(codeFlowCallbackService, 'authenticatedCallbackWithCode') .mockReturnValue(of({} as CallbackContext)); - await lastValueFrom( + await firstValueFrom( callbackService.handleCallbackAndFireEvents( 'anyUrl', { configId: 'configId1' }, @@ -83,17 +83,16 @@ describe('CallbackService ', () => { .spyOn(implicitFlowCallbackService, 'authenticatedImplicitFlowCallback') .mockReturnValue(of({} as CallbackContext)); - await lastValueFrom( + await firstValueFrom( callbackService.handleCallbackAndFireEvents( 'anyUrl', { configId: 'configId1' }, [{ configId: 'configId1' }] ) ); - expect(authorizedCallbackWithCodeSpy).toHaveBeenCalledWith( - { configId: 'configId1' }, - [{ configId: 'configId1' }] - ); + expect(authorizedCallbackWithCodeSpy.mock.calls).toEqual([ + [{ configId: 'configId1' }, [{ configId: 'configId1' }]], + ]); }); it('calls authorizedImplicitFlowCallback with hash if current flow is implicit flow and callbackurl does include a hash', async () => { @@ -105,7 +104,7 @@ describe('CallbackService ', () => { .spyOn(implicitFlowCallbackService, 'authenticatedImplicitFlowCallback') .mockReturnValue(of({} as CallbackContext)); - await lastValueFrom( + await firstValueFrom( callbackService.handleCallbackAndFireEvents( 'anyUrlWithAHash#some-string', { configId: 'configId1' }, @@ -113,11 +112,9 @@ describe('CallbackService ', () => { ) ); - expect(authorizedCallbackWithCodeSpy).toHaveBeenCalledWith( - { configId: 'configId1' }, - [{ configId: 'configId1' }], - 'some-string' - ); + expect(authorizedCallbackWithCodeSpy.mock.calls).toEqual([ + [{ configId: 'configId1' }, [{ configId: 'configId1' }], 'some-string'], + ]); }); it('emits callbackinternal no matter which flow it is', async () => { @@ -131,7 +128,7 @@ describe('CallbackService ', () => { .spyOn(codeFlowCallbackService, 'authenticatedCallbackWithCode') .mockReturnValue(of({} as CallbackContext)); - await lastValueFrom( + await firstValueFrom( callbackService.handleCallbackAndFireEvents( 'anyUrl', { configId: 'configId1' }, diff --git a/src/callback/code-flow-callback.service.spec.ts b/src/callback/code-flow-callback.service.spec.ts index cd89000..1c943e9 100644 --- a/src/callback/code-flow-callback.service.spec.ts +++ b/src/callback/code-flow-callback.service.spec.ts @@ -1,6 +1,6 @@ import { TestBed, mockRouterProvider } from '@/testing'; import { AbstractRouter } from 'oidc-client-rx'; -import { lastValueFrom, of, throwError } from 'rxjs'; +import { firstValueFrom, of, throwError } from 'rxjs'; import { vi } from 'vitest'; import type { CallbackContext } from '../flows/callback-context'; import { FlowsDataService } from '../flows/flows-data.service'; @@ -85,7 +85,7 @@ describe('CodeFlowCallbackService ', () => { triggerAuthorizationResultEvent: true, }; - await lastValueFrom( + await firstValueFrom( codeFlowCallbackService.authenticatedCallbackWithCode( 'some-url2', config, @@ -125,7 +125,7 @@ describe('CodeFlowCallbackService ', () => { postLoginRoute: 'postLoginRoute', }; - await lastValueFrom( + await firstValueFrom( codeFlowCallbackService.authenticatedCallbackWithCode( 'some-url3', config, @@ -163,7 +163,7 @@ describe('CodeFlowCallbackService ', () => { }; try { - await lastValueFrom( + await firstValueFrom( codeFlowCallbackService.authenticatedCallbackWithCode( 'some-url4', config, @@ -201,7 +201,7 @@ describe('CodeFlowCallbackService ', () => { }; try { - await lastValueFrom( + await firstValueFrom( codeFlowCallbackService.authenticatedCallbackWithCode( 'some-url5', config, diff --git a/src/callback/implicit-flow-callback.service.spec.ts b/src/callback/implicit-flow-callback.service.spec.ts index a2bfe5b..84863ce 100644 --- a/src/callback/implicit-flow-callback.service.spec.ts +++ b/src/callback/implicit-flow-callback.service.spec.ts @@ -1,6 +1,6 @@ import { TestBed, mockRouterProvider } from '@/testing'; import { AbstractRouter } from 'oidc-client-rx'; -import { lastValueFrom, of, throwError } from 'rxjs'; +import { firstValueFrom, of, throwError } from 'rxjs'; import { vi } from 'vitest'; import type { CallbackContext } from '../flows/callback-context'; import { FlowsDataService } from '../flows/flows-data.service'; @@ -20,6 +20,7 @@ describe('ImplicitFlowCallbackService ', () => { TestBed.configureTestingModule({ imports: [], providers: [ + ImplicitFlowCallbackService, mockRouterProvider(), mockProvider(FlowsService), mockProvider(FlowsDataService), @@ -81,7 +82,7 @@ describe('ImplicitFlowCallbackService ', () => { triggerAuthorizationResultEvent: true, }; - await lastValueFrom( + await firstValueFrom( implicitFlowCallbackService.authenticatedImplicitFlowCallback( config, [config], @@ -118,7 +119,7 @@ describe('ImplicitFlowCallbackService ', () => { postLoginRoute: 'postLoginRoute', }; - await lastValueFrom( + await firstValueFrom( implicitFlowCallbackService.authenticatedImplicitFlowCallback( config, [config], @@ -152,7 +153,7 @@ describe('ImplicitFlowCallbackService ', () => { }; try { - await lastValueFrom( + await firstValueFrom( implicitFlowCallbackService.authenticatedImplicitFlowCallback( config, [config], @@ -188,7 +189,7 @@ describe('ImplicitFlowCallbackService ', () => { }; try { - await lastValueFrom( + await firstValueFrom( implicitFlowCallbackService.authenticatedImplicitFlowCallback( config, [config], diff --git a/src/callback/interval.service.spec.ts b/src/callback/interval.service.spec.ts index 5710e50..340cbeb 100644 --- a/src/callback/interval.service.spec.ts +++ b/src/callback/interval.service.spec.ts @@ -7,8 +7,10 @@ describe('IntervalService', () => { let intervalService: IntervalService; beforeEach(() => { + vi.useFakeTimers(); TestBed.configureTestingModule({ providers: [ + IntervalService, { provide: Document, useValue: { @@ -22,6 +24,11 @@ describe('IntervalService', () => { intervalService = TestBed.inject(IntervalService); }); + // biome-ignore lint/correctness/noUndeclaredVariables: + afterEach(() => { + vi.useRealTimers(); + }); + it('should create', () => { expect(intervalService).toBeTruthy(); }); diff --git a/src/callback/periodically-token-check.service.spec.ts b/src/callback/periodically-token-check.service.spec.ts index 20efef3..c037f06 100644 --- a/src/callback/periodically-token-check.service.spec.ts +++ b/src/callback/periodically-token-check.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@/testing'; -import { lastValueFrom, of, throwError } from 'rxjs'; +import { ReplaySubject, firstValueFrom, of, share, throwError } from 'rxjs'; import { vi } from 'vitest'; import { AuthStateService } from '../auth-state/auth-state.service'; import { ConfigurationService } from '../config/config.service'; @@ -33,9 +33,11 @@ describe('PeriodicallyTokenCheckService', () => { let publicEventsService: PublicEventsService; beforeEach(() => { + vi.useFakeTimers(); TestBed.configureTestingModule({ imports: [], providers: [ + PeriodicallyTokenCheckService, mockProvider(ResetAuthDataService), FlowHelper, mockProvider(FlowsDataService), @@ -73,10 +75,11 @@ describe('PeriodicallyTokenCheckService', () => { // biome-ignore lint/correctness/noUndeclaredVariables: afterEach(() => { - if (intervalService.runTokenValidationRunning?.unsubscribe) { + if (intervalService?.runTokenValidationRunning?.unsubscribe) { intervalService.runTokenValidationRunning.unsubscribe(); intervalService.runTokenValidationRunning = null; } + vi.useRealTimers(); }); it('should create', () => { @@ -84,13 +87,22 @@ describe('PeriodicallyTokenCheckService', () => { }); describe('startTokenValidationPeriodically', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + // biome-ignore lint/correctness/noUndeclaredVariables: + afterEach(() => { + vi.useRealTimers(); + }); + it('returns if no config has silentrenew enabled', async () => { const configs = [ { silentRenew: false, configId: 'configId1' }, { silentRenew: false, configId: 'configId2' }, ]; - const result = await lastValueFrom( + const result = await firstValueFrom( periodicallyTokenCheckService.startTokenValidationPeriodically( configs, configs[0]! @@ -107,7 +119,7 @@ describe('PeriodicallyTokenCheckService', () => { true ); - const result = await lastValueFrom( + const result = await firstValueFrom( periodicallyTokenCheckService.startTokenValidationPeriodically( configs, configs[0]! @@ -181,19 +193,29 @@ describe('PeriodicallyTokenCheckService', () => { of(configs[0]!) ); - periodicallyTokenCheckService.startTokenValidationPeriodically( - configs, - configs[0]! - ); + try { + const test$ = periodicallyTokenCheckService + .startTokenValidationPeriodically(configs, configs[0]!) + .pipe( + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: true, + }) + ); - await vi.advanceTimersByTimeAsync(1000); + test$.subscribe(); - expect( - periodicallyTokenCheckService.startTokenValidationPeriodically - ).toThrowError(); - expect(resetSilentRenewRunning).toHaveBeenCalledExactlyOnceWith( - configs[0] - ); + await vi.advanceTimersByTimeAsync(1000); + + await firstValueFrom(test$); + expect.fail('should throw errror'); + } catch { + expect(resetSilentRenewRunning).toHaveBeenCalledExactlyOnceWith( + configs[0] + ); + } }); it('interval throws silent renew failed event with data in case of an error', async () => { @@ -220,20 +242,29 @@ describe('PeriodicallyTokenCheckService', () => { of(configs[0]!) ); - periodicallyTokenCheckService.startTokenValidationPeriodically( - configs, - configs[0]! - ); + try { + const test$ = periodicallyTokenCheckService + .startTokenValidationPeriodically(configs, configs[0]!) + .pipe( + share({ + connector: () => new ReplaySubject(1), + resetOnComplete: false, + resetOnError: false, + resetOnRefCountZero: false, + }) + ); - await vi.advanceTimersByTimeAsync(1000); + test$.subscribe(); - expect( - periodicallyTokenCheckService.startTokenValidationPeriodically - ).toThrowError(); - expect(publicEventsServiceSpy).toBeCalledWith([ - [EventTypes.SilentRenewStarted], - [EventTypes.SilentRenewFailed, new Error('error')], - ]); + await vi.advanceTimersByTimeAsync(1000); + + await firstValueFrom(test$); + } catch { + expect(publicEventsServiceSpy.mock.calls).toEqual([ + [EventTypes.SilentRenewStarted], + [EventTypes.SilentRenewFailed, new Error('error')], + ]); + } }); it('calls resetAuthorizationData and returns if no silent renew is configured', async () => { diff --git a/src/callback/periodically-token-check.service.ts b/src/callback/periodically-token-check.service.ts index 1f30de6..bc73068 100644 --- a/src/callback/periodically-token-check.service.ts +++ b/src/callback/periodically-token-check.service.ts @@ -1,6 +1,6 @@ import { Injectable, inject } from 'injection-js'; -import { type Observable, forkJoin, of, throwError } from 'rxjs'; -import { catchError, map, shareReplay, switchMap } from 'rxjs/operators'; +import { type Observable, ReplaySubject, forkJoin, of, throwError } from 'rxjs'; +import { catchError, map, share, switchMap } from 'rxjs/operators'; import { AuthStateService } from '../auth-state/auth-state.service'; import { ConfigurationService } from '../config/config.service'; import type { OpenIdConfiguration } from '../config/openid-configuration'; @@ -52,16 +52,16 @@ export class PeriodicallyTokenCheckService { startTokenValidationPeriodically( allConfigs: OpenIdConfiguration[], currentConfig: OpenIdConfiguration - ): Observable { + ): Observable { const configsWithSilentRenewEnabled = this.getConfigsWithSilentRenewEnabled(allConfigs); if (configsWithSilentRenewEnabled.length <= 0) { - return; + return of(undefined); } if (this.intervalService.isTokenValidationRunning()) { - return; + return of(undefined); } const refreshTimeInSeconds = this.getSmallestRefreshTimeFromConfigs( @@ -87,7 +87,14 @@ export class PeriodicallyTokenCheckService { ); const o$ = periodicallyCheck$.pipe( - catchError((error) => throwError(() => new Error(error))), + catchError((error) => { + this.loggerService.logError( + currentConfig, + 'silent renew failed!', + error + ); + return throwError(() => error); + }), map((objectWithConfigIds) => { for (const [configId, _] of Object.entries(objectWithConfigIds)) { this.configurationService @@ -104,20 +111,18 @@ export class PeriodicallyTokenCheckService { this.flowsDataService.resetSilentRenewRunning(config); } }); + return undefined; } }), - catchError((error) => { - this.loggerService.logError( - currentConfig, - 'silent renew failed!', - error - ); - return throwError(() => error); - }), - shareReplay(1) + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: false, + }) ); - this.intervalService.runTokenValidationRunning = o$.subscribe(); + this.intervalService.runTokenValidationRunning = o$.subscribe({}); return o$; } diff --git a/src/callback/refresh-session-refresh-token.service.spec.ts b/src/callback/refresh-session-refresh-token.service.spec.ts index b411b3a..6430fba 100644 --- a/src/callback/refresh-session-refresh-token.service.spec.ts +++ b/src/callback/refresh-session-refresh-token.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@/testing'; -import { lastValueFrom, of, throwError } from 'rxjs'; +import { firstValueFrom, of, throwError } from 'rxjs'; import { vi } from 'vitest'; import type { CallbackContext } from '../flows/callback-context'; import { FlowsService } from '../flows/flows.service'; @@ -16,6 +16,7 @@ describe('RefreshSessionRefreshTokenService', () => { let flowsService: FlowsService; beforeEach(() => { + vi.useFakeTimers(); TestBed.configureTestingModule({ imports: [], providers: [ @@ -34,6 +35,11 @@ describe('RefreshSessionRefreshTokenService', () => { resetAuthDataService = TestBed.inject(ResetAuthDataService); }); + // biome-ignore lint/correctness/noUndeclaredVariables: + afterEach(() => { + vi.useRealTimers(); + }); + it('should create', () => { expect(refreshSessionRefreshTokenService).toBeTruthy(); }); @@ -44,7 +50,7 @@ describe('RefreshSessionRefreshTokenService', () => { .spyOn(flowsService, 'processRefreshToken') .mockReturnValue(of({} as CallbackContext)); - await lastValueFrom( + await firstValueFrom( refreshSessionRefreshTokenService.refreshSessionWithRefreshTokens( { configId: 'configId1' }, [{ configId: 'configId1' }] @@ -63,7 +69,7 @@ describe('RefreshSessionRefreshTokenService', () => { ); try { - await lastValueFrom( + await firstValueFrom( refreshSessionRefreshTokenService.refreshSessionWithRefreshTokens( { configId: 'configId1' }, [{ configId: 'configId1' }] @@ -85,7 +91,7 @@ describe('RefreshSessionRefreshTokenService', () => { ); try { - await lastValueFrom( + await firstValueFrom( refreshSessionRefreshTokenService.refreshSessionWithRefreshTokens( { configId: 'configId1' }, [{ configId: 'configId1' }] diff --git a/src/callback/refresh-session.service.spec.ts b/src/callback/refresh-session.service.spec.ts index ceb35d1..e93ee35 100644 --- a/src/callback/refresh-session.service.spec.ts +++ b/src/callback/refresh-session.service.spec.ts @@ -1,6 +1,12 @@ import { TestBed, spyOnProperty } from '@/testing'; -import { EmptyError, lastValueFrom, of, throwError } from 'rxjs'; -import { delay } from 'rxjs/operators'; +import { + EmptyError, + ReplaySubject, + firstValueFrom, + of, + throwError, +} from 'rxjs'; +import { delay, share } from 'rxjs/operators'; import { vi } from 'vitest'; import { AuthStateService } from '../auth-state/auth-state.service'; import { AuthWellKnownService } from '../config/auth-well-known/auth-well-known.service'; @@ -22,6 +28,7 @@ import { } from './refresh-session.service'; describe('RefreshSessionService ', () => { + vi.useFakeTimers(); let refreshSessionService: RefreshSessionService; let flowHelper: FlowHelper; let authStateService: AuthStateService; @@ -63,6 +70,11 @@ describe('RefreshSessionService ', () => { storagePersistenceService = TestBed.inject(StoragePersistenceService); }); + // biome-ignore lint/correctness/noUndeclaredVariables: + afterEach(() => { + vi.useRealTimers(); + }); + it('should create', () => { expect(refreshSessionService).toBeTruthy(); }); @@ -91,7 +103,7 @@ describe('RefreshSessionService ', () => { const extraCustomParams = { extra: 'custom' }; - await lastValueFrom( + await firstValueFrom( refreshSessionService.userForceRefreshSession( allConfigs[0]!, allConfigs, @@ -128,7 +140,7 @@ describe('RefreshSessionService ', () => { const extraCustomParams = { extra: 'custom' }; - await lastValueFrom( + await firstValueFrom( refreshSessionService.userForceRefreshSession( allConfigs[0]!, allConfigs, @@ -163,7 +175,7 @@ describe('RefreshSessionService ', () => { ]; const writeSpy = vi.spyOn(storagePersistenceService, 'write'); - await lastValueFrom( + await firstValueFrom( refreshSessionService.userForceRefreshSession( allConfigs[0]!, allConfigs @@ -186,7 +198,7 @@ describe('RefreshSessionService ', () => { ]; try { - const result = await lastValueFrom( + const result = await firstValueFrom( refreshSessionService.userForceRefreshSession( allConfigs[0]!, allConfigs @@ -217,7 +229,7 @@ describe('RefreshSessionService ', () => { ]; try { - await lastValueFrom( + await firstValueFrom( refreshSessionService.userForceRefreshSession( allConfigs[0]!, allConfigs @@ -259,7 +271,7 @@ describe('RefreshSessionService ', () => { }, ]; - const result = await lastValueFrom( + const result = await firstValueFrom( refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs) ); expect(result.idToken).toEqual('id-token'); @@ -285,7 +297,7 @@ describe('RefreshSessionService ', () => { }, ]; - const result = await lastValueFrom( + const result = await firstValueFrom( refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs) ); expect(result).toEqual({ @@ -328,7 +340,7 @@ describe('RefreshSessionService ', () => { }, ]; - const result = await lastValueFrom( + const result = await firstValueFrom( refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs) ); expect(result.idToken).toBeDefined(); @@ -358,7 +370,7 @@ describe('RefreshSessionService ', () => { }, ]; - const result = await lastValueFrom( + const result = await firstValueFrom( refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs) ); expect(result).toEqual({ @@ -372,6 +384,8 @@ describe('RefreshSessionService ', () => { }); it('occurs timeout error and retry mechanism exhausted max retry count throws error', async () => { + vi.useRealTimers(); + vi.useFakeTimers(); vi.spyOn( flowHelper, 'isCurrentFlowCodeFlowWithRefreshTokens' @@ -402,10 +416,25 @@ describe('RefreshSessionService ', () => { const expectedInvokeCount = MAX_RETRY_ATTEMPTS; try { - const result = await lastValueFrom( - refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs) + const o$ = refreshSessionService + .forceRefreshSession(allConfigs[0]!, allConfigs) + .pipe( + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: true, + }) + ); + + o$.subscribe(); + + await vi.advanceTimersByTimeAsync( + allConfigs[0]!.silentRenewTimeoutInSeconds * 10000 ); + const result = await firstValueFrom(o$); + if (result) { expect.fail('It should not return any result.'); } @@ -415,10 +444,6 @@ describe('RefreshSessionService ', () => { expectedInvokeCount ); } - - await vi.advanceTimersByTimeAsync( - allConfigs[0]!.silentRenewTimeoutInSeconds * 10000 - ); }); it('occurs unknown error throws it to subscriber', async () => { @@ -453,7 +478,7 @@ describe('RefreshSessionService ', () => { ); try { - await lastValueFrom( + await firstValueFrom( refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs) ); expect.fail('It should not return any result.'); @@ -489,7 +514,7 @@ describe('RefreshSessionService ', () => { 'refreshSessionWithIFrameCompleted$' ).mockReturnValue(of(null)); - const result = await lastValueFrom( + const result = await firstValueFrom( refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs) ); expect(result).toEqual({ @@ -533,7 +558,7 @@ describe('RefreshSessionService ', () => { .spyOn(authStateService, 'areAuthStorageTokensValid') .mockReturnValue(true); - const result = await lastValueFrom( + const result = await firstValueFrom( refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs) ); expect(result).toEqual({ @@ -552,7 +577,7 @@ describe('RefreshSessionService ', () => { it('returns null if no auth well known endpoint defined', async () => { vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(true); - const result = await lastValueFrom( + const result = await firstValueFrom( (refreshSessionService as any).startRefreshSession() ); expect(result).toBe(null); @@ -561,7 +586,7 @@ describe('RefreshSessionService ', () => { it('returns null if silent renew Is running', async () => { vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(true); - const result = await lastValueFrom( + const result = await firstValueFrom( (refreshSessionService as any).startRefreshSession() ); expect(result).toBe(null); @@ -594,7 +619,7 @@ describe('RefreshSessionService ', () => { 'refreshSessionWithRefreshTokens' ).mockReturnValue(of({} as CallbackContext)); - await lastValueFrom( + await firstValueFrom( (refreshSessionService as any).startRefreshSession( allConfigs[0]!, allConfigs @@ -629,7 +654,7 @@ describe('RefreshSessionService ', () => { ) .mockReturnValue(of({} as CallbackContext)); - await lastValueFrom( + await firstValueFrom( (refreshSessionService as any).startRefreshSession( allConfigs[0]!, allConfigs @@ -668,7 +693,7 @@ describe('RefreshSessionService ', () => { .spyOn(refreshSessionIframeService, 'refreshSessionWithIframe') .mockReturnValue(of(false)); - await lastValueFrom( + await firstValueFrom( (refreshSessionService as any).startRefreshSession( allConfigs[0]!, allConfigs diff --git a/src/config/auth-well-known/auth-well-known-data.service.spec.ts b/src/config/auth-well-known/auth-well-known-data.service.spec.ts index 38eed06..446a3cb 100644 --- a/src/config/auth-well-known/auth-well-known-data.service.spec.ts +++ b/src/config/auth-well-known/auth-well-known-data.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@/testing'; -import { lastValueFrom, of, throwError } from 'rxjs'; +import { firstValueFrom, of, throwError } from 'rxjs'; import { vi } from 'vitest'; import { DataService } from '../../api/data.service'; import { LoggerService } from '../../logging/logger.service'; @@ -56,7 +56,7 @@ describe('AuthWellKnownDataService', () => { const urlWithoutSuffix = 'myUrl'; const urlWithSuffix = `${urlWithoutSuffix}/.well-known/openid-configuration`; - await lastValueFrom( + await firstValueFrom( (service as any).getWellKnownDocument(urlWithoutSuffix, { configId: 'configId1', }) @@ -72,7 +72,7 @@ describe('AuthWellKnownDataService', () => { .mockReturnValue(of(null)); const urlWithSuffix = 'myUrl/.well-known/openid-configuration'; - await lastValueFrom( + await firstValueFrom( (service as any).getWellKnownDocument(urlWithSuffix, { configId: 'configId1', }) @@ -89,7 +89,7 @@ describe('AuthWellKnownDataService', () => { const urlWithSuffix = 'myUrl/.well-known/openid-configuration/and/some/more/stuff'; - await lastValueFrom( + await firstValueFrom( (service as any).getWellKnownDocument(urlWithSuffix, { configId: 'configId1', }) @@ -106,7 +106,7 @@ describe('AuthWellKnownDataService', () => { const urlWithoutSuffix = 'myUrl'; const urlWithSuffix = `${urlWithoutSuffix}/.well-known/test-openid-configuration`; - await lastValueFrom( + await firstValueFrom( (service as any).getWellKnownDocument(urlWithoutSuffix, { configId: 'configId1', authWellknownUrlSuffix: '/.well-known/test-openid-configuration', @@ -126,7 +126,7 @@ describe('AuthWellKnownDataService', () => { ) ); - const res: unknown = await lastValueFrom( + const res: unknown = await firstValueFrom( (service as any).getWellKnownDocument('anyurl', { configId: 'configId1', }) @@ -144,7 +144,7 @@ describe('AuthWellKnownDataService', () => { ) ); - const res: any = await lastValueFrom( + const res: any = await firstValueFrom( (service as any).getWellKnownDocument('anyurl', { configId: 'configId1', }) @@ -164,7 +164,7 @@ describe('AuthWellKnownDataService', () => { ); try { - await lastValueFrom( + await firstValueFrom( (service as any).getWellKnownDocument('anyurl', 'configId') ); } catch (err: unknown) { @@ -181,7 +181,7 @@ describe('AuthWellKnownDataService', () => { const spy = vi.spyOn(service as any, 'getWellKnownDocument'); - const result = await lastValueFrom( + const result = await firstValueFrom( service.getWellKnownEndPointsForConfig({ configId: 'configId1', authWellknownEndpointUrl: 'any-url', @@ -200,7 +200,7 @@ describe('AuthWellKnownDataService', () => { }; try { - await lastValueFrom(service.getWellKnownEndPointsForConfig(config)); + await firstValueFrom(service.getWellKnownEndPointsForConfig(config)); } catch (error: any) { expect(loggerSpy).toHaveBeenCalledExactlyOnceWith( config, @@ -221,7 +221,7 @@ describe('AuthWellKnownDataService', () => { jwksUri: DUMMY_WELL_KNOWN_DOCUMENT.jwks_uri, }; - const result = await lastValueFrom( + const result = await firstValueFrom( service.getWellKnownEndPointsForConfig({ configId: 'configId1', authWellknownEndpointUrl: 'any-url', diff --git a/src/config/auth-well-known/auth-well-known.service.spec.ts b/src/config/auth-well-known/auth-well-known.service.spec.ts index 146c2e9..2399670 100644 --- a/src/config/auth-well-known/auth-well-known.service.spec.ts +++ b/src/config/auth-well-known/auth-well-known.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; -import { lastValueFrom, of, throwError } from 'rxjs'; +import { firstValueFrom, of, throwError } from 'rxjs'; import { vi } from 'vitest'; import { EventTypes } from '../../public-events/event-types'; import { PublicEventsService } from '../../public-events/public-events.service'; @@ -36,7 +36,7 @@ describe('AuthWellKnownService', () => { describe('getAuthWellKnownEndPoints', () => { it('getAuthWellKnownEndPoints throws an error if not config provided', async () => { try { - await lastValueFrom(service.queryAndStoreAuthWellKnownEndPoints(null)); + await firstValueFrom(service.queryAndStoreAuthWellKnownEndPoints(null)); } catch (error) { expect(error).toEqual( new Error( @@ -57,7 +57,7 @@ describe('AuthWellKnownService', () => { () => ({ issuer: 'anything' }) ); - const result = await lastValueFrom( + const result = await firstValueFrom( service.queryAndStoreAuthWellKnownEndPoints({ configId: 'configId1' }) ); expect(storagePersistenceService.read).not.toHaveBeenCalled(); @@ -77,7 +77,7 @@ describe('AuthWellKnownService', () => { ); const storeSpy = vi.spyOn(service, 'storeWellKnownEndpoints'); - const result = await lastValueFrom( + const result = await firstValueFrom( service.queryAndStoreAuthWellKnownEndPoints({ configId: 'configId1' }) ); expect(dataServiceSpy).toHaveBeenCalled(); @@ -92,7 +92,7 @@ describe('AuthWellKnownService', () => { const publicEventsServiceSpy = vi.spyOn(publicEventsService, 'fireEvent'); try { - await lastValueFrom( + await firstValueFrom( service.queryAndStoreAuthWellKnownEndPoints({ configId: 'configId1' }) ); } catch (err: any) { diff --git a/src/config/config.service.spec.ts b/src/config/config.service.spec.ts index 5fd4124..3ab4d9a 100644 --- a/src/config/config.service.spec.ts +++ b/src/config/config.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@/testing'; -import { lastValueFrom, of } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { vi } from 'vitest'; import { LoggerService } from '../logging/logger.service'; import { EventTypes } from '../public-events/event-types'; @@ -93,7 +93,7 @@ describe('Configuration Service', () => { }; const spy = vi.spyOn(configService as any, 'loadConfigs'); - const config = await lastValueFrom( + const config = await firstValueFrom( configService.getOpenIDConfiguration('configId1') ); expect(config).toBeTruthy(); @@ -108,7 +108,7 @@ describe('Configuration Service', () => { vi.spyOn(configValidationService, 'validateConfig').mockReturnValue(true); - const config = await lastValueFrom( + const config = await firstValueFrom( configService.getOpenIDConfiguration('configId1') ); expect(config).toBeTruthy(); @@ -126,7 +126,7 @@ describe('Configuration Service', () => { ); const consoleSpy = vi.spyOn(console, 'warn'); - const config = await lastValueFrom( + const config = await firstValueFrom( configService.getOpenIDConfiguration('configId1') ); expect(config).toBeNull(); @@ -141,7 +141,7 @@ describe('Configuration Service', () => { configId2: { configId: 'configId2' }, }; - const config = await lastValueFrom( + const config = await firstValueFrom( configService.getOpenIDConfiguration('notExisting') ); expect(config).toBeNull(); @@ -160,7 +160,7 @@ describe('Configuration Service', () => { issuer: 'auth-well-known', }); - const config = await lastValueFrom( + const config = await firstValueFrom( configService.getOpenIDConfiguration('configId1') ); expect(config?.authWellknownEndpoints).toEqual({ @@ -182,7 +182,7 @@ describe('Configuration Service', () => { const spy = vi.spyOn(publicEventsService, 'fireEvent'); - await lastValueFrom(configService.getOpenIDConfiguration('configId1')); + await firstValueFrom(configService.getOpenIDConfiguration('configId1')); expect(spy).toHaveBeenCalledExactlyOnceWith( EventTypes.ConfigLoaded, expect.anything() @@ -209,7 +209,7 @@ describe('Configuration Service', () => { 'storeWellKnownEndpoints' ); - const config = await lastValueFrom( + const config = await firstValueFrom( configService.getOpenIDConfiguration('configId1') ); expect(config).toBeTruthy(); @@ -237,7 +237,7 @@ describe('Configuration Service', () => { vi.spyOn(configValidationService, 'validateConfig').mockReturnValue(true); - const result = await lastValueFrom( + const result = await firstValueFrom( configService.getOpenIDConfigurations('configId1') ); expect(result.allConfigs.length).toEqual(2); @@ -254,7 +254,7 @@ describe('Configuration Service', () => { vi.spyOn(configValidationService, 'validateConfig').mockReturnValue(true); - const result = await lastValueFrom( + const result = await firstValueFrom( configService.getOpenIDConfigurations() ); expect(result.allConfigs.length).toEqual(2); @@ -276,7 +276,7 @@ describe('Configuration Service', () => { false ); - const { allConfigs, currentConfig } = await lastValueFrom( + const { allConfigs, currentConfig } = await firstValueFrom( configService.getOpenIDConfigurations() ); expect(allConfigs).toEqual([]); diff --git a/src/config/loader/config-loader.spec.ts b/src/config/loader/config-loader.spec.ts index 688006a..b189a53 100644 --- a/src/config/loader/config-loader.spec.ts +++ b/src/config/loader/config-loader.spec.ts @@ -1,4 +1,4 @@ -import { lastValueFrom, of } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import type { OpenIdConfiguration } from '../openid-configuration'; import { StsConfigHttpLoader, StsConfigStaticLoader } from './config-loader'; @@ -15,7 +15,7 @@ describe('ConfigLoader', () => { const result$ = loader.loadConfigs(); - const result = await lastValueFrom(result$); + const result = await firstValueFrom(result$); expect(Array.isArray(result)).toBeTruthy(); }); @@ -26,7 +26,7 @@ describe('ConfigLoader', () => { const result$ = loader.loadConfigs(); - const result = await lastValueFrom(result$); + const result = await firstValueFrom(result$); expect(Array.isArray(result)).toBeTruthy(); }); }); @@ -43,7 +43,7 @@ describe('ConfigLoader', () => { const result$ = loader.loadConfigs(); - const result = await lastValueFrom(result$); + const result = await firstValueFrom(result$); expect(Array.isArray(result)).toBeTruthy(); expect(result[0]!.configId).toBe('configId1'); expect(result[1]!.configId).toBe('configId2'); @@ -58,7 +58,7 @@ describe('ConfigLoader', () => { const result$ = loader.loadConfigs(); - const result = await lastValueFrom(result$); + const result = await firstValueFrom(result$); expect(Array.isArray(result)).toBeTruthy(); expect(result[0]!.configId).toBe('configId1'); expect(result[1]!.configId).toBe('configId2'); @@ -71,7 +71,7 @@ describe('ConfigLoader', () => { const result$ = loader.loadConfigs(); - const result = await lastValueFrom(result$); + const result = await firstValueFrom(result$); expect(Array.isArray(result)).toBeTruthy(); expect(result[0]!.configId).toBe('configId1'); }); diff --git a/src/config/loader/config-loader.ts b/src/config/loader/config-loader.ts index 6e4d190..59c804a 100644 --- a/src/config/loader/config-loader.ts +++ b/src/config/loader/config-loader.ts @@ -13,7 +13,6 @@ export abstract class StsConfigLoader { export class StsConfigStaticLoader implements StsConfigLoader { constructor( - // biome-ignore lint/style/noParameterProperties: private readonly passedConfigs: OpenIdConfiguration | OpenIdConfiguration[] ) {} diff --git a/src/config/validation/config-validation.service.spec.ts b/src/config/validation/config-validation.service.spec.ts index a6e2953..39a872e 100644 --- a/src/config/validation/config-validation.service.spec.ts +++ b/src/config/validation/config-validation.service.spec.ts @@ -1,4 +1,5 @@ import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; +import { mockImplementationWhenArgs, spyOnWithOrigin } from '@/testing/spy'; import { vi } from 'vitest'; import { LogLevel } from '../../logging/log-level'; import { LoggerService } from '../../logging/logger.service'; @@ -51,12 +52,12 @@ describe('Config Validation Service', () => { it('calls `logWarning` if one rule has warning level', () => { const loggerWarningSpy = vi.spyOn(loggerService, 'logWarning'); - const messageTypeSpy = vi.spyOn( - configValidationService as any, - 'getAllMessagesOfType' + const messageTypeSpy = spyOnWithOrigin( + configValidationService, + 'getAllMessagesOfType' as any ); - mockImplementationWhenArgsEqual( + mockImplementationWhenArgs( messageTypeSpy, (arg1: any, arg2: any) => arg1 === 'warning' && Array.isArray(arg2), () => ['A warning message'] diff --git a/src/config/validation/config-validation.service.ts b/src/config/validation/config-validation.service.ts index c82aeee..19aa266 100644 --- a/src/config/validation/config-validation.service.ts +++ b/src/config/validation/config-validation.service.ts @@ -85,7 +85,7 @@ export class ConfigValidationService { return allErrorMessages.length; } - private getAllMessagesOfType( + protected getAllMessagesOfType( type: Level, results: RuleValidationResult[] ): string[] { diff --git a/src/extractors/jwk.extractor.spec.ts b/src/extractors/jwk.extractor.spec.ts index 44a62c3..1a4c0e1 100644 --- a/src/extractors/jwk.extractor.spec.ts +++ b/src/extractors/jwk.extractor.spec.ts @@ -102,21 +102,30 @@ describe('JwkExtractor', () => { describe('extractJwk', () => { it('throws error if no keys are present in array', () => { - expect(() => { + try { service.extractJwk([]); - }).toThrow(JwkExtractorInvalidArgumentError); + expect.fail('should error'); + } catch (error: any) { + expect(error).toBe(JwkExtractorInvalidArgumentError); + } }); it('throws error if spec.kid is present, but no key was matching', () => { - expect(() => { + try { service.extractJwk(keys, { kid: 'doot' }); - }).toThrow(JwkExtractorNoMatchingKeysError); + expect.fail('should error'); + } catch (error: any) { + expect(error).toBe(JwkExtractorNoMatchingKeysError); + } }); it('throws error if spec.use is present, but no key was matching', () => { - expect(() => { + try { service.extractJwk(keys, { use: 'blorp' }); - }).toThrow(JwkExtractorNoMatchingKeysError); + expect.fail('should error'); + } catch (error: any) { + expect(error).toBe(JwkExtractorNoMatchingKeysError); + } }); it('does not throw error if no key is matching when throwOnEmpty is false', () => { @@ -126,9 +135,12 @@ describe('JwkExtractor', () => { }); it('throws error if multiple keys are present, and spec is not present', () => { - expect(() => { + try { service.extractJwk(keys); - }).toThrow(JwkExtractorSeveralMatchingKeysError); + expect.fail('should error'); + } catch (error: any) { + expect(error).toBe(JwkExtractorSeveralMatchingKeysError); + } }); it('returns array of keys matching spec.kid', () => { diff --git a/src/flows/callback-handling/code-flow-callback-handler.service.spec.ts b/src/flows/callback-handling/code-flow-callback-handler.service.spec.ts index b92e8e3..b43f814 100644 --- a/src/flows/callback-handling/code-flow-callback-handler.service.spec.ts +++ b/src/flows/callback-handling/code-flow-callback-handler.service.spec.ts @@ -1,6 +1,6 @@ import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { HttpErrorResponse, HttpHeaders } from '@ngify/http'; -import { lastValueFrom, of, throwError } from 'rxjs'; +import { firstValueFrom, of, throwError } from 'rxjs'; import { vi } from 'vitest'; import { DataService } from '../../api/data.service'; import { LoggerService } from '../../logging/logger.service'; @@ -56,7 +56,7 @@ describe('CodeFlowCallbackHandlerService', () => { ); try { - await lastValueFrom( + await firstValueFrom( service.codeFlowCallback('test-url', { configId: 'configId1' }) ); } catch (err: any) { @@ -76,7 +76,7 @@ describe('CodeFlowCallbackHandlerService', () => { ); try { - await lastValueFrom( + await firstValueFrom( service.codeFlowCallback('test-url', { configId: 'configId1' }) ); } catch (err: any) { @@ -99,7 +99,7 @@ describe('CodeFlowCallbackHandlerService', () => { existingIdToken: null, } as CallbackContext; - const callbackContext = await lastValueFrom( + const callbackContext = await firstValueFrom( service.codeFlowCallback('test-url', { configId: 'configId1' }) ); expect(callbackContext).toEqual(expectedCallbackContext); @@ -122,7 +122,7 @@ describe('CodeFlowCallbackHandlerService', () => { ).mockReturnValue(false); try { - await lastValueFrom( + await firstValueFrom( service.codeFlowCodeRequest({} as CallbackContext, { configId: 'configId1', }) @@ -144,7 +144,7 @@ describe('CodeFlowCallbackHandlerService', () => { ); try { - await lastValueFrom( + await firstValueFrom( service.codeFlowCodeRequest({} as CallbackContext, { configId: 'configId1', }) @@ -166,7 +166,7 @@ describe('CodeFlowCallbackHandlerService', () => { ); try { - await lastValueFrom( + await firstValueFrom( service.codeFlowCodeRequest({} as CallbackContext, { configId: 'configId1', }) @@ -190,7 +190,7 @@ describe('CodeFlowCallbackHandlerService', () => { 'validateStateFromHashCallback' ).mockReturnValue(true); - await lastValueFrom( + await firstValueFrom( service.codeFlowCodeRequest({} as CallbackContext, { configId: 'configId1', }) @@ -226,7 +226,7 @@ describe('CodeFlowCallbackHandlerService', () => { const postSpy = vi.spyOn(dataService, 'post').mockReturnValue(of({})); - await lastValueFrom( + await firstValueFrom( service.codeFlowCodeRequest({ code: 'foo' } as CallbackContext, config) ); expect(urlServiceSpy).toHaveBeenCalledExactlyOnceWith('foo', config, { @@ -253,7 +253,7 @@ describe('CodeFlowCallbackHandlerService', () => { 'validateStateFromHashCallback' ).mockReturnValue(true); - await lastValueFrom( + await firstValueFrom( service.codeFlowCodeRequest({} as CallbackContext, config) ); const httpHeaders = postSpy.mock.calls.at(-1)?.[3] as HttpHeaders; @@ -280,7 +280,7 @@ describe('CodeFlowCallbackHandlerService', () => { ); try { - await lastValueFrom( + await firstValueFrom( service.codeFlowCodeRequest({} as CallbackContext, config) ); } catch (err: any) { @@ -313,7 +313,7 @@ describe('CodeFlowCallbackHandlerService', () => { ).mockReturnValue(true); try { - const res = await lastValueFrom( + const res = await firstValueFrom( service.codeFlowCodeRequest({} as CallbackContext, config) ); expect(res).toBeTruthy(); @@ -348,7 +348,7 @@ describe('CodeFlowCallbackHandlerService', () => { ).mockReturnValue(true); try { - const res = await lastValueFrom( + const res = await firstValueFrom( service.codeFlowCodeRequest({} as CallbackContext, config) ); expect(res).toBeFalsy(); diff --git a/src/flows/callback-handling/code-flow-callback-handler.service.ts b/src/flows/callback-handling/code-flow-callback-handler.service.ts index 2df5334..a7ef847 100644 --- a/src/flows/callback-handling/code-flow-callback-handler.service.ts +++ b/src/flows/callback-handling/code-flow-callback-handler.service.ts @@ -1,5 +1,5 @@ import { HttpHeaders } from '@ngify/http'; -import { inject, Injectable } from 'injection-js'; +import { Injectable, inject } from 'injection-js'; import { type Observable, of, throwError, timer } from 'rxjs'; import { catchError, mergeMap, retryWhen, switchMap } from 'rxjs/operators'; import { DataService } from '../../api/data.service'; @@ -116,7 +116,7 @@ export class CodeFlowCallbackHandlerService { switchMap((response) => { if (response) { const authResult: AuthResult = { - ...response, + ...(response as any), state: callbackContext.state, session_state: callbackContext.sessionState, }; diff --git a/src/flows/callback-handling/history-jwt-keys-callback-handler.service.spec.ts b/src/flows/callback-handling/history-jwt-keys-callback-handler.service.spec.ts index b7385fa..1c94db8 100644 --- a/src/flows/callback-handling/history-jwt-keys-callback-handler.service.spec.ts +++ b/src/flows/callback-handling/history-jwt-keys-callback-handler.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@/testing'; -import { lastValueFrom, of, throwError } from 'rxjs'; +import { firstValueFrom, of, throwError } from 'rxjs'; import { vi } from 'vitest'; import { AuthStateService } from '../../auth-state/auth-state.service'; import { LoggerService } from '../../logging/logger.service'; @@ -83,14 +83,14 @@ describe('HistoryJwtKeysCallbackHandlerService', () => { vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( of({ keys: [] } as JwtKeys) ); - await lastValueFrom( + await firstValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, allConfigs ) ); - expect(storagePersistenceServiceSpy).toBeCalledWith([ + expect(storagePersistenceServiceSpy.mock.calls).toEqual([ ['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]], ['jwtKeys', { keys: [] }, allConfigs[0]], ]); @@ -121,14 +121,14 @@ describe('HistoryJwtKeysCallbackHandlerService', () => { of({ keys: [] } as JwtKeys) ); - await lastValueFrom( + await firstValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, allConfigs ) ); - expect(storagePersistenceServiceSpy).toBeCalledWith([ + expect(storagePersistenceServiceSpy.mock.calls).toEqual([ ['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]], ['jwtKeys', { keys: [] }, allConfigs[0]], ]); @@ -159,14 +159,14 @@ describe('HistoryJwtKeysCallbackHandlerService', () => { vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( of({ keys: [] } as JwtKeys) ); - await lastValueFrom( + await firstValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, allConfigs ) ); - expect(storagePersistenceServiceSpy).toBeCalledWith([ + expect(storagePersistenceServiceSpy.mock.calls).toEqual([ ['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]], ['reusable_refresh_token', 'dummy_refresh_token', allConfigs[0]], ['jwtKeys', { keys: [] }, allConfigs[0]], @@ -194,7 +194,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => { vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( of({ keys: [] } as JwtKeys) ); - await lastValueFrom( + await firstValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, @@ -223,7 +223,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => { vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( of({ keys: [{ kty: 'henlo' } as JwtKey] } as JwtKeys) ); - const result = await lastValueFrom( + const result = await firstValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, @@ -257,7 +257,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => { of({} as JwtKeys) ); try { - await lastValueFrom( + await firstValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, @@ -290,7 +290,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => { throwError(() => new Error('error')) ); try { - await lastValueFrom( + await firstValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, @@ -316,7 +316,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => { ]; try { - await lastValueFrom( + await firstValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, @@ -353,7 +353,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => { ); try { - await lastValueFrom( + await firstValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, @@ -394,7 +394,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => { ); try { - await lastValueFrom( + await firstValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, @@ -436,7 +436,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => { ); try { - const callbackContext: CallbackContext = await lastValueFrom( + const callbackContext: CallbackContext = await firstValueFrom( service.callbackHistoryAndResetJwtKeys( initialCallbackContext, allConfigs[0]!, @@ -444,7 +444,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => { ) ); expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(2); - expect(storagePersistenceServiceSpy).toBeCalledWith([ + expect(storagePersistenceServiceSpy.mock.calls).toEqual([ ['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]], ['jwtKeys', DUMMY_JWT_KEYS, allConfigs[0]], ]); @@ -479,7 +479,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => { ); try { - const callbackContext: CallbackContext = await lastValueFrom( + const callbackContext: CallbackContext = await firstValueFrom( service.callbackHistoryAndResetJwtKeys( initialCallbackContext, allConfigs[0]!, @@ -523,7 +523,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => { ); try { - const callbackContext: CallbackContext = await lastValueFrom( + const callbackContext: CallbackContext = await firstValueFrom( service.callbackHistoryAndResetJwtKeys( initialCallbackContext, allConfigs[0]!, @@ -560,7 +560,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => { ); try { - const callbackContext: CallbackContext = await lastValueFrom( + const callbackContext: CallbackContext = await firstValueFrom( service.callbackHistoryAndResetJwtKeys( initialCallbackContext, allConfigs[0]!, diff --git a/src/flows/callback-handling/implicit-flow-callback-handler.service.spec.ts b/src/flows/callback-handling/implicit-flow-callback-handler.service.spec.ts index d7f9d11..e784f80 100644 --- a/src/flows/callback-handling/implicit-flow-callback-handler.service.spec.ts +++ b/src/flows/callback-handling/implicit-flow-callback-handler.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@/testing'; -import { lastValueFrom } from 'rxjs'; +import { firstValueFrom } from 'rxjs'; import { vi } from 'vitest'; import { DOCUMENT } from '../../dom'; import { LoggerService } from '../../logging/logger.service'; @@ -58,7 +58,7 @@ describe('ImplicitFlowCallbackHandlerService', () => { }, ]; - await lastValueFrom( + await firstValueFrom( service.implicitFlowCallback(allConfigs[0]!, allConfigs, 'any-hash') ); expect(resetAuthorizationDataSpy).toHaveBeenCalled(); @@ -76,7 +76,7 @@ describe('ImplicitFlowCallbackHandlerService', () => { }, ]; - await lastValueFrom( + await firstValueFrom( service.implicitFlowCallback(allConfigs[0]!, allConfigs, 'any-hash') ); expect(resetAuthorizationDataSpy).not.toHaveBeenCalled(); @@ -102,7 +102,7 @@ describe('ImplicitFlowCallbackHandlerService', () => { }, ]; - const callbackContext = await lastValueFrom( + const callbackContext = await firstValueFrom( service.implicitFlowCallback(allConfigs[0]!, allConfigs, 'anyHash') ); expect(callbackContext).toEqual(expectedCallbackContext); @@ -128,7 +128,7 @@ describe('ImplicitFlowCallbackHandlerService', () => { }, ]; - const callbackContext = await lastValueFrom( + const callbackContext = await firstValueFrom( service.implicitFlowCallback(allConfigs[0]!, allConfigs) ); expect(callbackContext).toEqual(expectedCallbackContext); diff --git a/src/flows/callback-handling/refresh-session-callback-handler.service.spec.ts b/src/flows/callback-handling/refresh-session-callback-handler.service.spec.ts index 46188d0..6b56839 100644 --- a/src/flows/callback-handling/refresh-session-callback-handler.service.spec.ts +++ b/src/flows/callback-handling/refresh-session-callback-handler.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@/testing'; -import { lastValueFrom } from 'rxjs'; +import { firstValueFrom } from 'rxjs'; import { vi } from 'vitest'; import { AuthStateService } from '../../auth-state/auth-state.service'; import { LoggerService } from '../../logging/logger.service'; @@ -54,7 +54,7 @@ describe('RefreshSessionCallbackHandlerService', () => { existingIdToken: 'henlo-legger', } as CallbackContext; - const callbackContext = await lastValueFrom( + const callbackContext = await firstValueFrom( service.refreshSessionWithRefreshTokens({ configId: 'configId1' }) ); expect(callbackContext).toEqual(expectedCallbackContext); @@ -69,7 +69,7 @@ describe('RefreshSessionCallbackHandlerService', () => { vi.spyOn(authStateService, 'getIdToken').mockReturnValue('henlo-legger'); try { - await lastValueFrom( + await firstValueFrom( service.refreshSessionWithRefreshTokens({ configId: 'configId1' }) ); } catch (err: any) { diff --git a/src/flows/callback-handling/refresh-token-callback-handler.service.spec.ts b/src/flows/callback-handling/refresh-token-callback-handler.service.spec.ts index f01dcab..32c4cf4 100644 --- a/src/flows/callback-handling/refresh-token-callback-handler.service.spec.ts +++ b/src/flows/callback-handling/refresh-token-callback-handler.service.spec.ts @@ -1,6 +1,6 @@ import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { HttpErrorResponse, HttpHeaders } from '@ngify/http'; -import { lastValueFrom, of, throwError } from 'rxjs'; +import { firstValueFrom, of, throwError } from 'rxjs'; import { vi } from 'vitest'; import { DataService } from '../../api/data.service'; import { LoggerService } from '../../logging/logger.service'; @@ -46,7 +46,7 @@ describe('RefreshTokenCallbackHandlerService', () => { it('throws error if no tokenEndpoint is given', async () => { try { - await lastValueFrom( + await firstValueFrom( (service as any).refreshTokensRequestTokens({} as CallbackContext) ); } catch (err: unknown) { @@ -63,7 +63,7 @@ describe('RefreshTokenCallbackHandlerService', () => { () => ({ tokenEndpoint: 'tokenEndpoint' }) ); - await lastValueFrom( + await firstValueFrom( service.refreshTokensRequestTokens({} as CallbackContext, { configId: 'configId1', }) @@ -90,7 +90,7 @@ describe('RefreshTokenCallbackHandlerService', () => { () => ({ tokenEndpoint: 'tokenEndpoint' }) ); - await lastValueFrom( + await firstValueFrom( service.refreshTokensRequestTokens({} as CallbackContext, { configId: 'configId1', }) @@ -115,7 +115,7 @@ describe('RefreshTokenCallbackHandlerService', () => { ); try { - await lastValueFrom( + await firstValueFrom( service.refreshTokensRequestTokens({} as CallbackContext, config) ); } catch (err: any) { @@ -139,7 +139,7 @@ describe('RefreshTokenCallbackHandlerService', () => { ); try { - const res = await lastValueFrom( + const res = await firstValueFrom( service.refreshTokensRequestTokens({} as CallbackContext, config) ); expect(res).toBeTruthy(); @@ -165,7 +165,7 @@ describe('RefreshTokenCallbackHandlerService', () => { ); try { - const res = await lastValueFrom( + const res = await firstValueFrom( service.refreshTokensRequestTokens({} as CallbackContext, config) ); expect(res).toBeFalsy(); diff --git a/src/flows/callback-handling/state-validation-callback-handler.service.spec.ts b/src/flows/callback-handling/state-validation-callback-handler.service.spec.ts index 7bdaf34..4c544e7 100644 --- a/src/flows/callback-handling/state-validation-callback-handler.service.spec.ts +++ b/src/flows/callback-handling/state-validation-callback-handler.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@/testing'; -import { lastValueFrom, of } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { vi } from 'vitest'; import { AuthStateService } from '../../auth-state/auth-state.service'; import { DOCUMENT } from '../../dom'; @@ -66,7 +66,7 @@ describe('StateValidationCallbackHandlerService', () => { ); const allConfigs = [{ configId: 'configId1' }]; - const newCallbackContext = await lastValueFrom( + const newCallbackContext = await firstValueFrom( service.callbackStateValidation( {} as CallbackContext, allConfigs[0]!, @@ -95,7 +95,7 @@ describe('StateValidationCallbackHandlerService', () => { const allConfigs = [{ configId: 'configId1' }]; try { - await lastValueFrom( + await firstValueFrom( service.callbackStateValidation( {} as CallbackContext, allConfigs[0]!, @@ -132,7 +132,7 @@ describe('StateValidationCallbackHandlerService', () => { const allConfigs = [{ configId: 'configId1' }]; try { - await lastValueFrom( + await firstValueFrom( service.callbackStateValidation( { isRenewProcess: true } as CallbackContext, allConfigs[0]!, diff --git a/src/flows/callback-handling/user-callback-handler.service.spec.ts b/src/flows/callback-handling/user-callback-handler.service.spec.ts index 3531a7b..a6830d1 100644 --- a/src/flows/callback-handling/user-callback-handler.service.spec.ts +++ b/src/flows/callback-handling/user-callback-handler.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@/testing'; -import { lastValueFrom, of } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { vi } from 'vitest'; import { AuthStateService } from '../../auth-state/auth-state.service'; import { LoggerService } from '../../logging/logger.service'; @@ -70,7 +70,7 @@ describe('UserCallbackHandlerService', () => { const spy = vi.spyOn(flowsDataService, 'setSessionState'); - const resultCallbackContext = await lastValueFrom( + const resultCallbackContext = await firstValueFrom( service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) ); expect(spy).toHaveBeenCalledExactlyOnceWith('mystate', allConfigs[0]); @@ -103,7 +103,7 @@ describe('UserCallbackHandlerService', () => { ]; const spy = vi.spyOn(flowsDataService, 'setSessionState'); - const resultCallbackContext = await lastValueFrom( + const resultCallbackContext = await firstValueFrom( service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) ); expect(spy).not.toHaveBeenCalled(); @@ -136,7 +136,7 @@ describe('UserCallbackHandlerService', () => { ]; const spy = vi.spyOn(flowsDataService, 'setSessionState'); - const resultCallbackContext = await lastValueFrom( + const resultCallbackContext = await firstValueFrom( service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) ); expect(spy).not.toHaveBeenCalled(); @@ -165,7 +165,7 @@ describe('UserCallbackHandlerService', () => { const spy = vi.spyOn(flowsDataService, 'setSessionState'); - const resultCallbackContext = await lastValueFrom( + const resultCallbackContext = await firstValueFrom( service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) ); expect(spy).not.toHaveBeenCalled(); @@ -203,7 +203,7 @@ describe('UserCallbackHandlerService', () => { 'updateAndPublishAuthState' ); - const resultCallbackContext = await lastValueFrom( + const resultCallbackContext = await firstValueFrom( service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) ); expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({ @@ -244,7 +244,7 @@ describe('UserCallbackHandlerService', () => { .spyOn(userService, 'getAndPersistUserDataInStore') .mockReturnValue(of({ user: 'some_data' })); - const resultCallbackContext = await lastValueFrom( + const resultCallbackContext = await firstValueFrom( service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) ); expect(getAndPersistUserDataInStoreSpy).toHaveBeenCalledExactlyOnceWith( @@ -292,7 +292,7 @@ describe('UserCallbackHandlerService', () => { 'updateAndPublishAuthState' ); - const resultCallbackContext = await lastValueFrom( + const resultCallbackContext = await firstValueFrom( service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) ); expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({ @@ -335,7 +335,7 @@ describe('UserCallbackHandlerService', () => { ); const setSessionStateSpy = vi.spyOn(flowsDataService, 'setSessionState'); - const resultCallbackContext = await lastValueFrom( + const resultCallbackContext = await firstValueFrom( service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) ); expect(setSessionStateSpy).toHaveBeenCalledExactlyOnceWith( @@ -381,7 +381,7 @@ describe('UserCallbackHandlerService', () => { ); try { - await lastValueFrom( + await firstValueFrom( service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) ); } catch (err: any) { @@ -432,7 +432,7 @@ describe('UserCallbackHandlerService', () => { ); try { - await lastValueFrom( + await firstValueFrom( service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) ); } catch (err: any) { diff --git a/src/flows/flows-data.service.spec.ts b/src/flows/flows-data.service.spec.ts index fba5495..d2baba4 100644 --- a/src/flows/flows-data.service.spec.ts +++ b/src/flows/flows-data.service.spec.ts @@ -247,15 +247,15 @@ describe('Flows Data Service', () => { }); describe('isSilentRenewRunning', () => { - it('silent renew process timeout exceeded reset state object and returns false result', () => { + it('silent renew process timeout exceeded reset state object and returns false result', async () => { const config = { silentRenewTimeoutInSeconds: 10, configId: 'configId1', }; vi.useRealTimers(); - vi.useFakeTimers(); const baseTime = new Date(); + vi.useFakeTimers(); vi.setSystemTime(baseTime); @@ -271,7 +271,7 @@ describe('Flows Data Service', () => { ); const spyWrite = vi.spyOn(storagePersistenceService, 'write'); - vi.advanceTimersByTimeAsync( + await vi.advanceTimersByTimeAsync( (config.silentRenewTimeoutInSeconds + 1) * 1000 ); diff --git a/src/flows/flows.service.spec.ts b/src/flows/flows.service.spec.ts index a7b36c1..1119b20 100644 --- a/src/flows/flows.service.spec.ts +++ b/src/flows/flows.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@/testing'; -import { lastValueFrom, of } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { vi } from 'vitest'; import { mockProvider } from '../testing/mock'; import type { CallbackContext } from './callback-context'; @@ -87,7 +87,7 @@ describe('Flows Service', () => { }, ]; - const value = await lastValueFrom( + const value = await firstValueFrom( service.processCodeFlowCallback( 'some-url1234', allConfigs[0]!, @@ -129,7 +129,7 @@ describe('Flows Service', () => { }, ]; - const value = await lastValueFrom( + const value = await firstValueFrom( service.processSilentRenewCodeFlowCallback( {} as CallbackContext, allConfigs[0]!, @@ -167,7 +167,7 @@ describe('Flows Service', () => { }, ]; - const value = await lastValueFrom( + const value = await firstValueFrom( service.processImplicitFlowCallback( allConfigs[0]!, allConfigs, @@ -211,7 +211,7 @@ describe('Flows Service', () => { }, ]; - const value = await lastValueFrom( + const value = await firstValueFrom( service.processRefreshToken(allConfigs[0]!, allConfigs) ); expect(value).toEqual({} as CallbackContext); diff --git a/src/flows/signin-key-data.service.spec.ts b/src/flows/signin-key-data.service.spec.ts index 9ffdcbb..2b533db 100644 --- a/src/flows/signin-key-data.service.spec.ts +++ b/src/flows/signin-key-data.service.spec.ts @@ -1,6 +1,6 @@ import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { HttpResponse } from '@ngify/http'; -import { EmptyError, isObservable, lastValueFrom, of, throwError } from 'rxjs'; +import { EmptyError, firstValueFrom, isObservable, of, throwError } from 'rxjs'; import { vi } from 'vitest'; import { DataService } from '../api/data.service'; import { LoggerService } from '../logging/logger.service'; @@ -60,7 +60,7 @@ describe('Signin Key Data Service', () => { const result = service.getSigningKeys({ configId: 'configId1' }); try { - await lastValueFrom(result); + await firstValueFrom(result); } catch (err: any) { expect(err).toBeTruthy(); } @@ -75,7 +75,7 @@ describe('Signin Key Data Service', () => { const result = service.getSigningKeys({ configId: 'configId1' }); try { - await lastValueFrom(result); + await firstValueFrom(result); } catch (err: any) { expect(err).toBeTruthy(); } @@ -92,7 +92,7 @@ describe('Signin Key Data Service', () => { const result = service.getSigningKeys({ configId: 'configId1' }); try { - await lastValueFrom(result); + await firstValueFrom(result); } catch (err: any) { if (err instanceof EmptyError) { expect(spy).toHaveBeenCalledExactlyOnceWith('someUrl', { @@ -115,7 +115,7 @@ describe('Signin Key Data Service', () => { ) ); - const res = await lastValueFrom( + const res = await firstValueFrom( service.getSigningKeys({ configId: 'configId1' }) ); expect(res).toBeTruthy(); @@ -136,7 +136,7 @@ describe('Signin Key Data Service', () => { ) ); - const res = await lastValueFrom( + const res = await firstValueFrom( service.getSigningKeys({ configId: 'configId1' }) ); expect(res).toBeTruthy(); @@ -159,7 +159,7 @@ describe('Signin Key Data Service', () => { ); try { - await lastValueFrom(service.getSigningKeys({ configId: 'configId1' })); + await firstValueFrom(service.getSigningKeys({ configId: 'configId1' })); } catch (err: any) { expect(err).toBeTruthy(); } @@ -180,7 +180,7 @@ describe('Signin Key Data Service', () => { const logSpy = vi.spyOn(loggerService, 'logError'); try { - await lastValueFrom( + await firstValueFrom( (service as any).handleErrorGetSigningKeys( new HttpResponse({ status: 400, statusText: 'nono' }), { configId: 'configId1' } @@ -198,7 +198,7 @@ describe('Signin Key Data Service', () => { const logSpy = vi.spyOn(loggerService, 'logError'); try { - await lastValueFrom( + await firstValueFrom( (service as any).handleErrorGetSigningKeys('Just some Error', { configId: 'configId1', }) @@ -215,7 +215,7 @@ describe('Signin Key Data Service', () => { const logSpy = vi.spyOn(loggerService, 'logError'); try { - await lastValueFrom( + await firstValueFrom( (service as any).handleErrorGetSigningKeys( { message: 'Just some Error' }, { configId: 'configId1' } diff --git a/src/http/index.ts b/src/http/index.ts index a400ba2..8daced4 100644 --- a/src/http/index.ts +++ b/src/http/index.ts @@ -1,5 +1,6 @@ import type { HttpFeature, HttpInterceptor } from '@ngify/http'; import { InjectionToken } from 'injection-js'; +export { HttpParams, HttpParamsOptions } from './params'; export const HTTP_INTERCEPTORS = new InjectionToken( 'HTTP_INTERCEPTORS' diff --git a/src/http/params.ts b/src/http/params.ts new file mode 100644 index 0000000..4480b93 --- /dev/null +++ b/src/http/params.ts @@ -0,0 +1,355 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type { HttpParameterCodec } from '@ngify/http'; + +/** + * Provides encoding and decoding of URL parameter and query-string values. + * + * Serializes and parses URL parameter keys and values to encode and decode them. + * If you pass URL query parameters without encoding, + * the query parameters can be misinterpreted at the receiving end. + * + */ +export class HttpUrlEncodingCodec implements HttpParameterCodec { + /** + * Encodes a key name for a URL parameter or query-string. + * @param key The key name. + * @returns The encoded key name. + */ + encodeKey(key: string): string { + return standardEncoding(key); + } + + /** + * Encodes the value of a URL parameter or query-string. + * @param value The value. + * @returns The encoded value. + */ + encodeValue(value: string): string { + return standardEncoding(value); + } + + /** + * Decodes an encoded URL parameter or query-string key. + * @param key The encoded key name. + * @returns The decoded key name. + */ + decodeKey(key: string): string { + return decodeURIComponent(key); + } + + /** + * Decodes an encoded URL parameter or query-string value. + * @param value The encoded value. + * @returns The decoded value. + */ + decodeValue(value: string) { + return decodeURIComponent(value); + } +} + +/** + * Encode input string with standard encodeURIComponent and then un-encode specific characters. + */ +const STANDARD_ENCODING_REGEX = /%(\d[a-f0-9])/gi; +const STANDARD_ENCODING_REPLACEMENTS: { [x: string]: string } = { + '40': '@', + '3A': ':', + '24': '$', + '2C': ',', + '3B': ';', + '3D': '=', + '3F': '?', + '2F': '/', +}; + +function standardEncoding(v: string): string { + return encodeURIComponent(v).replace( + STANDARD_ENCODING_REGEX, + (s, t) => STANDARD_ENCODING_REPLACEMENTS[t] ?? s + ); +} + +function paramParser( + rawParams: string, + codec: HttpParameterCodec +): Map { + const map = new Map(); + if (rawParams.length > 0) { + // The `window.location.search` can be used while creating an instance of the `HttpParams` class + // (e.g. `new HttpParams({ fromString: window.location.search })`). The `window.location.search` + // may start with the `?` char, so we strip it if it's present. + const params: string[] = rawParams.replace(/^\?/, '').split('&'); + params.forEach((param: string) => { + const eqIdx = param.indexOf('='); + const [key, val]: string[] = + eqIdx === -1 + ? [codec.decodeKey(param), ''] + : [ + codec.decodeKey(param.slice(0, eqIdx)), + codec.decodeValue(param.slice(eqIdx + 1)), + ]; + const list = map.get(key) || []; + list.push(val); + map.set(key, list); + }); + } + return map; +} + +interface Update { + param: string; + value?: string | number | boolean; + op: 'a' | 'd' | 's'; +} + +/** + * Options used to construct an `HttpParams` instance. + * + */ +export interface HttpParamsOptions { + /** + * String representation of the HTTP parameters in URL-query-string format. + * Mutually exclusive with `fromObject`. + */ + fromString?: string; + + /** Object map of the HTTP parameters. Mutually exclusive with `fromString`. */ + fromObject?: { + [param: string]: + | string + | number + | boolean + | ReadonlyArray; + }; + + /** Encoding codec used to parse and serialize the parameters. */ + encoder?: HttpParameterCodec; +} + +/** + * + * @ngify/http has slighty different implementation than Angular's HttpParams. + * So this file to keep implement to angular + * An HTTP request/response body that represents serialized parameters, + * per the MIME type `application/x-www-form-urlencoded`. + * + * This class is immutable; all mutation operations return a new instance. + */ +export class HttpParams { + private map: Map | null; + private encoder: HttpParameterCodec; + private updates: Update[] | null = null; + private cloneFrom: HttpParams | null = null; + + constructor(options: HttpParamsOptions = {} as HttpParamsOptions) { + this.encoder = options.encoder || new HttpUrlEncodingCodec(); + if (options.fromString) { + if (options.fromObject) { + throw new Error('Cannot specify both fromString and fromObject.'); + } + this.map = paramParser(options.fromString, this.encoder); + } else if (options.fromObject) { + this.map = new Map(); + Object.keys(options.fromObject).forEach((key) => { + const value = (options.fromObject as any)[key]; + // convert the values to strings + const values = Array.isArray(value) + ? value.map((value) => `${value}`) + : [`${value}`]; + this.map!.set(key, values); + }); + } else { + this.map = null; + } + } + + /** + * Reports whether the body includes one or more values for a given parameter. + * @param param The parameter name. + * @returns True if the parameter has one or more values, + * false if it has no value or is not present. + */ + has(param: string): boolean { + this.init(); + return this.map!.has(param); + } + + /** + * Retrieves the first value for a parameter. + * @param param The parameter name. + * @returns The first value of the given parameter, + * or `null` if the parameter is not present. + */ + get(param: string): string | null { + this.init(); + const res = this.map!.get(param); + return res ? res[0] : null; + } + + /** + * Retrieves all values for a parameter. + * @param param The parameter name. + * @returns All values in a string array, + * or `null` if the parameter not present. + */ + getAll(param: string): string[] | null { + this.init(); + return this.map!.get(param) || null; + } + + /** + * Retrieves all the parameters for this body. + * @returns The parameter names in a string array. + */ + keys(): string[] { + this.init(); + return Array.from(this.map!.keys()); + } + + /** + * Appends a new value to existing values for a parameter. + * @param param The parameter name. + * @param value The new value to add. + * @return A new body with the appended value. + */ + append(param: string, value: string | number | boolean): HttpParams { + return this.clone({ param, value, op: 'a' }); + } + + /** + * Constructs a new body with appended values for the given parameter name. + * @param params parameters and values + * @return A new body with the new value. + */ + appendAll(params: { + [param: string]: + | string + | number + | boolean + | ReadonlyArray; + }): HttpParams { + const updates: Update[] = []; + Object.keys(params).forEach((param) => { + const value = params[param]; + if (Array.isArray(value)) { + value.forEach((_value) => { + updates.push({ param, value: _value, op: 'a' }); + }); + } else { + updates.push({ + param, + value: value as string | number | boolean, + op: 'a', + }); + } + }); + return this.clone(updates); + } + + /** + * Replaces the value for a parameter. + * @param param The parameter name. + * @param value The new value. + * @return A new body with the new value. + */ + set(param: string, value: string | number | boolean): HttpParams { + return this.clone({ param, value, op: 's' }); + } + + /** + * Removes a given value or all values from a parameter. + * @param param The parameter name. + * @param value The value to remove, if provided. + * @return A new body with the given value removed, or with all values + * removed if no value is specified. + */ + delete(param: string, value?: string | number | boolean): HttpParams { + return this.clone({ param, value, op: 'd' }); + } + + /** + * Serializes the body to an encoded string, where key-value pairs (separated by `=`) are + * separated by `&`s. + */ + toString(): string { + this.init(); + return ( + this.keys() + .map((key) => { + const eKey = this.encoder.encodeKey(key); + // `a: ['1']` produces `'a=1'` + // `b: []` produces `''` + // `c: ['1', '2']` produces `'c=1&c=2'` + return this.map!.get(key)! + .map((value) => `${eKey}=${this.encoder.encodeValue(value)}`) + .join('&'); + }) + // filter out empty values because `b: []` produces `''` + // which results in `a=1&&c=1&c=2` instead of `a=1&c=1&c=2` if we don't + .filter((param) => param !== '') + .join('&') + ); + } + + private clone(update: Update | Update[]): HttpParams { + const clone = new HttpParams({ + encoder: this.encoder, + } as HttpParamsOptions); + clone.cloneFrom = this.cloneFrom || this; + clone.updates = (this.updates || []).concat(update); + return clone; + } + + private init() { + if (this.map === null) { + this.map = new Map(); + } + if (this.cloneFrom !== null) { + this.cloneFrom.init(); + this.cloneFrom + .keys() + .forEach((key) => this.map!.set(key, this.cloneFrom!.map!.get(key)!)); + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: + this.updates!.forEach((update) => { + switch (update.op) { + case 'a': + case 's': { + const base = + (update.op === 'a' ? this.map!.get(update.param) : undefined) || + []; + base.push(`${update.value!}`); + this.map!.set(update.param, base); + break; + } + case 'd': { + if (update.value !== undefined) { + const base = this.map!.get(update.param) || []; + const idx = base.indexOf(`${update.value}`); + if (idx !== -1) { + base.splice(idx, 1); + } + if (base.length > 0) { + this.map!.set(update.param, base); + } else { + this.map!.delete(update.param); + } + } else { + this.map!.delete(update.param); + break; + } + break; + } + default: + } + }); + this.cloneFrom = this.updates = null; + } + } +} diff --git a/src/iframe/check-session.service.spec.ts b/src/iframe/check-session.service.spec.ts index 9ab3a8c..baa6cde 100644 --- a/src/iframe/check-session.service.spec.ts +++ b/src/iframe/check-session.service.spec.ts @@ -1,6 +1,6 @@ import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; -import { lastValueFrom, of } from 'rxjs'; -import { skip } from 'rxjs/operators'; +import { ReplaySubject, firstValueFrom, of } from 'rxjs'; +import { share, skip } from 'rxjs/operators'; import { vi } from 'vitest'; import { LoggerService } from '../logging/logger.service'; import { OidcSecurityService } from '../oidc.security.service'; @@ -333,7 +333,7 @@ describe('CheckSessionService', () => { }); describe('init', () => { - it('returns falsy observable when lastIframerefresh and iframeRefreshInterval are bigger than now', async () => { + it('angular oidc client', async () => { const serviceAsAny = checkSessionService as any; const dateNow = new Date(); const lastRefresh = dateNow.setMinutes(dateNow.getMinutes() + 30); @@ -341,7 +341,7 @@ describe('CheckSessionService', () => { serviceAsAny.lastIFrameRefresh = lastRefresh; serviceAsAny.iframeRefreshInterval = lastRefresh; - const result = await lastValueFrom(serviceAsAny.init()); + const result = await firstValueFrom(serviceAsAny.init()); expect(result).toBeUndefined(); }); }); @@ -366,18 +366,27 @@ describe('CheckSessionService', () => { describe('checkSessionChanged$', () => { it('emits when internal event is thrown', async () => { - const result = await lastValueFrom( - checkSessionService.checkSessionChanged$.pipe(skip(1)) + const test$ = checkSessionService.checkSessionChanged$.pipe( + skip(1), + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: true, + }) ); - expect(result).toBe(true); + test$.subscribe(); const serviceAsAny = checkSessionService as any; serviceAsAny.checkSessionChangedInternal$.next(true); + + const result = await firstValueFrom(test$); + expect(result).toBe(true); }); it('emits false initially', async () => { - const result = await lastValueFrom( + const result = await firstValueFrom( checkSessionService.checkSessionChanged$ ); expect(result).toBe(false); @@ -387,7 +396,7 @@ describe('CheckSessionService', () => { const expectedResultsInOrder = [false, true]; let counter = 0; - const result = await lastValueFrom( + const result = await firstValueFrom( checkSessionService.checkSessionChanged$ ); expect(result).toBe(expectedResultsInOrder[counter]); diff --git a/src/iframe/check-session.service.ts b/src/iframe/check-session.service.ts index 7afade4..02e5dd4 100644 --- a/src/iframe/check-session.service.ts +++ b/src/iframe/check-session.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NgZone, type OnDestroy, inject } from 'injection-js'; +import { Injectable, inject } from 'injection-js'; import { BehaviorSubject, Observable, of } from 'rxjs'; import { take } from 'rxjs/operators'; import type { OpenIdConfiguration } from '../config/openid-configuration'; @@ -14,7 +14,7 @@ const IFRAME_FOR_CHECK_SESSION_IDENTIFIER = 'myiFrameForCheckSession'; // http://openid.net/specs/openid-connect-session-1_0-ID4.html @Injectable() -export class CheckSessionService implements OnDestroy { +export class CheckSessionService { private readonly loggerService = inject(LoggerService); private readonly storagePersistenceService = inject( @@ -25,8 +25,6 @@ export class CheckSessionService implements OnDestroy { private readonly eventService = inject(PublicEventsService); - private readonly zone = inject(NgZone); - private readonly document = inject(DOCUMENT); private checkSessionReceived = false; @@ -54,7 +52,7 @@ export class CheckSessionService implements OnDestroy { return this.checkSessionChangedInternal$.asObservable(); } - ngOnDestroy(): void { + [Symbol.dispose]() { this.stop(); const windowAsDefaultView = this.document.defaultView; @@ -104,9 +102,9 @@ export class CheckSessionService implements OnDestroy { ); } - private init(configuration: OpenIdConfiguration): Observable { + private init(configuration: OpenIdConfiguration): Observable { if (this.lastIFrameRefresh + this.iframeRefreshInterval > Date.now()) { - return of(); + return of(undefined); } const authWellKnownEndPoints = this.storagePersistenceService.read( @@ -120,7 +118,7 @@ export class CheckSessionService implements OnDestroy { 'CheckSession - init check session: authWellKnownEndpoints is undefined. Returning.' ); - return of(); + return of(undefined); } const existingIframe = this.getOrCreateIframe(configuration); @@ -138,7 +136,7 @@ export class CheckSessionService implements OnDestroy { 'CheckSession - init check session: checkSessionIframe is not configured to run' ); - return of(); + return of(undefined); } if (contentWindow) { @@ -228,13 +226,11 @@ export class CheckSessionService implements OnDestroy { ); } - this.zone.runOutsideAngular(() => { - this.scheduledHeartBeatRunning = - this.document?.defaultView?.setTimeout( - () => this.zone.run(pollServerSessionRecur), - this.heartBeatInterval - ) ?? null; - }); + this.scheduledHeartBeatRunning = + this.document?.defaultView?.setTimeout( + pollServerSessionRecur, + this.heartBeatInterval + ) ?? null; }); }; diff --git a/src/iframe/refresh-session-iframe.service.spec.ts b/src/iframe/refresh-session-iframe.service.spec.ts index 9069bb8..1753550 100644 --- a/src/iframe/refresh-session-iframe.service.spec.ts +++ b/src/iframe/refresh-session-iframe.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@/testing'; -import { lastValueFrom, of } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { vi } from 'vitest'; import { LoggerService } from '../logging/logger.service'; import { mockProvider } from '../testing/mock'; @@ -41,26 +41,27 @@ describe('RefreshSessionIframeService ', () => { .mockReturnValue(of(null)); const allConfigs = [{ configId: 'configId1' }]; - await lastValueFrom(refreshSessionIframeService - .refreshSessionWithIframe(allConfigs[0]!, allConfigs)); -expect( - sendAuthorizeRequestUsingSilentRenewSpy - ).toHaveBeenCalledExactlyOnceWith( - 'a-url', - allConfigs[0]!, - allConfigs - ); + await firstValueFrom( + refreshSessionIframeService.refreshSessionWithIframe( + allConfigs[0]!, + allConfigs + ) + ); + expect( + sendAuthorizeRequestUsingSilentRenewSpy + ).toHaveBeenCalledExactlyOnceWith('a-url', allConfigs[0]!, allConfigs); }); }); describe('initSilentRenewRequest', () => { - it('dispatches customevent to window object', async () => { - const dispatchEventSpy = vi.spyOn(window, 'dispatchEvent'); - - await lastValueFrom( - (refreshSessionIframeService as any).initSilentRenewRequest() + it('dispatches customevent to window object', () => { + const dispatchEventSpy = vi.spyOn( + document.defaultView?.window!, + 'dispatchEvent' ); + (refreshSessionIframeService as any).initSilentRenewRequest(); + expect(dispatchEventSpy).toHaveBeenCalledExactlyOnceWith( new CustomEvent('oidc-silent-renew-init', { detail: expect.any(Number), diff --git a/src/iframe/refresh-session-iframe.service.ts b/src/iframe/refresh-session-iframe.service.ts index 345c61a..9f8fae0 100644 --- a/src/iframe/refresh-session-iframe.service.ts +++ b/src/iframe/refresh-session-iframe.service.ts @@ -1,6 +1,11 @@ -import { Injectable, RendererFactory2, inject } from 'injection-js'; -import { Observable } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; +import { Injectable, inject } from 'injection-js'; +import { + Observable, + ReplaySubject, + type Subscription, + fromEventPattern, +} from 'rxjs'; +import { filter, share, switchMap, takeUntil } from 'rxjs/operators'; import type { OpenIdConfiguration } from '../config/openid-configuration'; import { DOCUMENT } from '../dom'; import { LoggerService } from '../logging/logger.service'; @@ -9,11 +14,6 @@ import { SilentRenewService } from './silent-renew.service'; @Injectable() export class RefreshSessionIframeService { - private readonly renderer = inject(RendererFactory2).createRenderer( - null, - null - ); - private readonly loggerService = inject(LoggerService); private readonly urlService = inject(UrlService); @@ -22,6 +22,8 @@ export class RefreshSessionIframeService { private readonly document = inject(DOCUMENT); + private silentRenewEventHandlerSubscription?: Subscription; + refreshSessionWithIframe( config: OpenIdConfiguration, allConfigs: OpenIdConfiguration[], @@ -80,24 +82,53 @@ export class RefreshSessionIframeService { ): void { const instanceId = Math.random(); - const initDestroyHandler = this.renderer.listen( - 'window', - 'oidc-silent-renew-init', - (e: CustomEvent) => { - if (e.detail !== instanceId) { - initDestroyHandler(); - renewDestroyHandler(); - } - } - ); - const renewDestroyHandler = this.renderer.listen( - 'window', - 'oidc-silent-renew-message', - (e) => - this.silentRenewService.silentRenewEventHandler(e, config, allConfigs) + const oidcSilentRenewInit$ = fromEventPattern( + (handler) => + this.document.defaultView.window.addEventListener( + 'oidc-silent-renew-init', + handler + ), + (handler) => + this.document.defaultView.window.removeEventListener( + 'oidc-silent-renew-init', + handler + ) ); - this.document.defaultView?.dispatchEvent( + const oidcSilentRenewInitNotSelf$ = oidcSilentRenewInit$.pipe( + filter((e: CustomEvent) => e.detail !== instanceId) + ); + + if (this.silentRenewEventHandlerSubscription) { + this.silentRenewEventHandlerSubscription.unsubscribe(); + } + this.silentRenewEventHandlerSubscription = fromEventPattern( + (handler) => + this.document.defaultView.window.addEventListener( + 'oidc-silent-renew-message', + handler + ), + (handler) => + this.document.defaultView.window.removeEventListener( + 'oidc-silent-renew-message', + handler + ) + ) + .pipe( + takeUntil(oidcSilentRenewInitNotSelf$), + switchMap((e) => + this.silentRenewService.silentRenewEventHandler(e, config, allConfigs) + ), + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: true, + }) + ) + .subscribe(); + + this.document.defaultView?.window.dispatchEvent( new CustomEvent('oidc-silent-renew-init', { detail: instanceId, }) diff --git a/src/iframe/silent-renew.service.spec.ts b/src/iframe/silent-renew.service.spec.ts index 9244f92..eed9743 100644 --- a/src/iframe/silent-renew.service.spec.ts +++ b/src/iframe/silent-renew.service.spec.ts @@ -1,5 +1,12 @@ import { TestBed } from '@/testing'; -import { Observable, lastValueFrom, of, throwError } from 'rxjs'; +import { + Observable, + ReplaySubject, + firstValueFrom, + of, + share, + throwError, +} from 'rxjs'; import { vi } from 'vitest'; import { AuthStateService } from '../auth-state/auth-state.service'; import { ImplicitFlowCallbackService } from '../callback/implicit-flow-callback.service'; @@ -28,6 +35,7 @@ describe('SilentRenewService ', () => { let intervalService: IntervalService; beforeEach(() => { + vi.useFakeTimers(); TestBed.configureTestingModule({ providers: [ SilentRenewService, @@ -54,6 +62,11 @@ describe('SilentRenewService ', () => { intervalService = TestBed.inject(IntervalService); }); + // biome-ignore lint/correctness/noUndeclaredVariables: + afterEach(() => { + vi.useRealTimers(); + }); + it('should create', () => { expect(silentRenewService).toBeTruthy(); }); @@ -149,7 +162,7 @@ describe('SilentRenewService ', () => { const urlParts = 'code=some-code&state=some-state&session_state=some-session-state'; - await lastValueFrom( + await firstValueFrom( silentRenewService.codeFlowCallbackSilentRenewIframe( [url, urlParts], config, @@ -188,7 +201,7 @@ describe('SilentRenewService ', () => { const urlParts = 'error=some_error'; try { - await lastValueFrom( + await firstValueFrom( silentRenewService.codeFlowCallbackSilentRenewIframe( [url, urlParts], config, @@ -312,19 +325,31 @@ describe('SilentRenewService ', () => { const eventData = { detail: 'detail?detail2' } as CustomEvent; const allConfigs = [{ configId: 'configId1' }]; - const result = await lastValueFrom( - silentRenewService.refreshSessionWithIFrameCompleted$ + const test$ = silentRenewService.refreshSessionWithIFrameCompleted$.pipe( + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: true, + }) ); + + test$.subscribe(); + + await firstValueFrom( + silentRenewService.silentRenewEventHandler( + eventData, + allConfigs[0]!, + allConfigs + ) + ); + await vi.advanceTimersByTimeAsync(1000); + + const result = await firstValueFrom(test$); + expect(result).toEqual({ refreshToken: 'callbackContext', } as CallbackContext); - - silentRenewService.silentRenewEventHandler( - eventData, - allConfigs[0]!, - allConfigs - ); - await vi.advanceTimersByTimeAsync(1000); }); it('loggs and calls flowsDataService.resetSilentRenewRunning in case of an error', async () => { @@ -341,10 +366,12 @@ describe('SilentRenewService ', () => { const allConfigs = [{ configId: 'configId1' }]; const eventData = { detail: 'detail?detail2' } as CustomEvent; - silentRenewService.silentRenewEventHandler( - eventData, - allConfigs[0]!, - allConfigs + await firstValueFrom( + silentRenewService.silentRenewEventHandler( + eventData, + allConfigs[0]!, + allConfigs + ) ); await vi.advanceTimersByTimeAsync(1000); expect(resetSilentRenewRunningSpy).toHaveBeenCalledTimes(1); @@ -360,17 +387,28 @@ describe('SilentRenewService ', () => { const eventData = { detail: 'detail?detail2' } as CustomEvent; const allConfigs = [{ configId: 'configId1' }]; - const result = await lastValueFrom( - silentRenewService.refreshSessionWithIFrameCompleted$ + const test$ = silentRenewService.refreshSessionWithIFrameCompleted$.pipe( + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: true, + }) ); - expect(result).toBeNull(); - silentRenewService.silentRenewEventHandler( - eventData, - allConfigs[0]!, - allConfigs + test$.subscribe(); + + await firstValueFrom( + silentRenewService.silentRenewEventHandler( + eventData, + allConfigs[0]!, + allConfigs + ) ); await vi.advanceTimersByTimeAsync(1000); + + const result = await firstValueFrom(test$); + expect(result).toBeNull(); }); }); }); diff --git a/src/iframe/silent-renew.service.ts b/src/iframe/silent-renew.service.ts index 46942bc..2c2f264 100644 --- a/src/iframe/silent-renew.service.ts +++ b/src/iframe/silent-renew.service.ts @@ -1,7 +1,6 @@ -import { HttpParams } from '@ngify/http'; -import { Injectable, inject } from 'injection-js'; -import { type Observable, Subject, throwError } from 'rxjs'; -import { catchError } from 'rxjs/operators'; +import { Injectable, inject } from 'injection-js'; +import { type Observable, Subject, of, throwError } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; import { AuthStateService } from '../auth-state/auth-state.service'; import { ImplicitFlowCallbackService } from '../callback/implicit-flow-callback.service'; import { IntervalService } from '../callback/interval.service'; @@ -10,6 +9,7 @@ import type { CallbackContext } from '../flows/callback-context'; import { FlowsDataService } from '../flows/flows-data.service'; import { FlowsService } from '../flows/flows.service'; import { ResetAuthDataService } from '../flows/reset-auth-data.service'; +import { HttpParams } from '../http'; import { LoggerService } from '../logging/logger.service'; import { FlowHelper } from '../utils/flowHelper/flow-helper.service'; import { ValidationResult } from '../validation/validation-result'; @@ -70,8 +70,9 @@ export class SilentRenewService { config: OpenIdConfiguration, allConfigs: OpenIdConfiguration[] ): Observable { - // TODO: fix @ngify/http - const params = new HttpParams(urlParts[1] || undefined); + const params = new HttpParams({ + fromString: urlParts[1], + }); const errorParam = params.get('error'); @@ -120,10 +121,10 @@ export class SilentRenewService { e: CustomEvent, config: OpenIdConfiguration, allConfigs: OpenIdConfiguration[] - ): void { + ): Observable { this.loggerService.logDebug(config, 'silentRenewEventHandler'); if (!e.detail) { - return; + return of(undefined); } let callback$: Observable; @@ -146,17 +147,19 @@ export class SilentRenewService { ); } - callback$.subscribe({ - next: (callbackContext) => { + return callback$.pipe( + map((callbackContext) => { this.refreshSessionWithIFrameCompletedInternal$.next(callbackContext); this.flowsDataService.resetSilentRenewRunning(config); - }, - error: (err: unknown) => { + return undefined; + }), + catchError((err: unknown) => { this.loggerService.logError(config, `Error: ${err}`); this.refreshSessionWithIFrameCompletedInternal$.next(null); this.flowsDataService.resetSilentRenewRunning(config); - }, - }); + return of(undefined); + }) + ); } private getExistingIframe(): HTMLIFrameElement | null { diff --git a/src/index.ts b/src/index.ts index 8a94a73..c93397b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ // Public classes. -export { PassedInitialConfig } from './auth-config'; +export type { PassedInitialConfig } from './auth-config'; export * from './auth-options'; export * from './auth-state/auth-result'; export * from './auth-state/auth-state'; diff --git a/src/injection/index.ts b/src/injection/index.ts index 73ce5ec..cf84fe3 100644 --- a/src/injection/index.ts +++ b/src/injection/index.ts @@ -1,3 +1,3 @@ -export { Module } from './module'; +export type { Module } from './module'; export { APP_INITIALIZER } from './convention'; export { injectAbstractType } from './inject'; diff --git a/src/injection/module.ts b/src/injection/module.ts index b715030..525cdef 100644 --- a/src/injection/module.ts +++ b/src/injection/module.ts @@ -1,4 +1,3 @@ -import 'reflect-metadata'; import type { Injector } from 'injection-js'; export type Module = (parentInjector: Injector) => Injector; diff --git a/src/interceptor/auth.interceptor.spec.ts b/src/interceptor/auth.interceptor.spec.ts index 0c1ef04..7b51fb1 100644 --- a/src/interceptor/auth.interceptor.spec.ts +++ b/src/interceptor/auth.interceptor.spec.ts @@ -10,7 +10,7 @@ import { HttpTestingController, provideHttpClientTesting, } from '@ngify/http/testing'; -import { lastValueFrom } from 'rxjs'; +import { firstValueFrom } from 'rxjs'; import { vi } from 'vitest'; import { AuthStateService } from '../auth-state/auth-state.service'; import { ConfigurationService } from '../config/config.service'; @@ -106,7 +106,7 @@ describe('AuthHttpInterceptor', () => { true ); - const response = await lastValueFrom(httpClient.get(actionUrl)); + const response = await firstValueFrom(httpClient.get(actionUrl)); expect(response).toBeTruthy(); const httpRequest = httpTestingController.expectOne(actionUrl); @@ -132,7 +132,7 @@ describe('AuthHttpInterceptor', () => { true ); - const response = await lastValueFrom(httpClient.get(actionUrl)); + const response = await firstValueFrom(httpClient.get(actionUrl)); expect(response).toBeTruthy(); const httpRequest = httpTestingController.expectOne(actionUrl); @@ -160,7 +160,7 @@ describe('AuthHttpInterceptor', () => { 'thisIsAToken' ); - const response = await lastValueFrom(httpClient.get(actionUrl)); + const response = await firstValueFrom(httpClient.get(actionUrl)); expect(response).toBeTruthy(); const httpRequest = httpTestingController.expectOne(actionUrl); @@ -185,7 +185,7 @@ describe('AuthHttpInterceptor', () => { true ); - const response = await lastValueFrom(httpClient.get(actionUrl)); + const response = await firstValueFrom(httpClient.get(actionUrl)); expect(response).toBeTruthy(); const httpRequest = httpTestingController.expectOne(actionUrl); @@ -211,7 +211,7 @@ describe('AuthHttpInterceptor', () => { ); vi.spyOn(authStateService, 'getAccessToken').mockReturnValue(''); - const response = await lastValueFrom(httpClient.get(actionUrl)); + const response = await firstValueFrom(httpClient.get(actionUrl)); expect(response).toBeTruthy(); const httpRequest = httpTestingController.expectOne(actionUrl); @@ -229,7 +229,7 @@ describe('AuthHttpInterceptor', () => { false ); - const response = await lastValueFrom(httpClient.get(actionUrl)); + const response = await firstValueFrom(httpClient.get(actionUrl)); expect(response).toBeTruthy(); const httpRequest = httpTestingController.expectOne(actionUrl); @@ -260,7 +260,7 @@ describe('AuthHttpInterceptor', () => { matchingConfig: null, }); - const response = await lastValueFrom(httpClient.get(actionUrl)); + const response = await firstValueFrom(httpClient.get(actionUrl)); expect(response).toBeTruthy(); const httpRequest = httpTestingController.expectOne(actionUrl); @@ -286,10 +286,10 @@ describe('AuthHttpInterceptor', () => { true ); - let response = await lastValueFrom(httpClient.get(actionUrl)); + let response = await firstValueFrom(httpClient.get(actionUrl)); expect(response).toBeTruthy(); - response = await lastValueFrom(httpClient.get(actionUrl2)); + response = await firstValueFrom(httpClient.get(actionUrl2)); expect(response).toBeTruthy(); const httpRequest = httpTestingController.expectOne(actionUrl); diff --git a/src/login/login.service.spec.ts b/src/login/login.service.spec.ts index d8518db..83568e8 100644 --- a/src/login/login.service.spec.ts +++ b/src/login/login.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@/testing'; -import { lastValueFrom, of } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { vi } from 'vitest'; import { StoragePersistenceService } from '../storage/storage-persistence.service'; import { mockProvider } from '../testing/mock'; @@ -83,7 +83,7 @@ describe('LoginService', () => { ); }); - it("should throw error if configuration is null and doesn't call loginPar or loginStandard", () => { + it("should throw error if configuration is null and doesn't call loginPar or loginStandard", async () => { // arrange // biome-ignore lint/suspicious/noEvolvingTypes: const config = null; @@ -91,13 +91,16 @@ describe('LoginService', () => { const standardLoginSpy = vi.spyOn(standardLoginService, 'loginStandard'); const authOptions = { customParams: { custom: 'params' } }; - // act - const fn = (): void => service.login(config, authOptions); - - // assert - expect(fn).toThrow( - new Error('Please provide a configuration before setting up the module') - ); + try { + await firstValueFrom(service.login(config, authOptions)); + expect.fail('should be error'); + } catch (error: unknown) { + expect(error).toEqual( + new Error( + 'Please provide a configuration before setting up the module' + ) + ); + } expect(loginParSpy).not.toHaveBeenCalled(); expect(standardLoginSpy).not.toHaveBeenCalled(); }); @@ -115,7 +118,7 @@ describe('LoginService', () => { .mockReturnValue(of({} as LoginResponse)); // act - await lastValueFrom(service.loginWithPopUp(config, [config])); + await firstValueFrom(service.loginWithPopUp(config, [config])); expect(loginWithPopUpPar).toHaveBeenCalledTimes(1); expect(loginWithPopUpStandardSpy).not.toHaveBeenCalled(); }); @@ -131,7 +134,7 @@ describe('LoginService', () => { .mockReturnValue(of({} as LoginResponse)); // act - await lastValueFrom(service.loginWithPopUp(config, [config])); + await firstValueFrom(service.loginWithPopUp(config, [config])); expect(loginWithPopUpPar).not.toHaveBeenCalled(); expect(loginWithPopUpStandardSpy).toHaveBeenCalledTimes(1); }); @@ -150,7 +153,7 @@ describe('LoginService', () => { ); // act - await lastValueFrom( + await firstValueFrom( service.loginWithPopUp(config, [config], authOptions) ); expect(storagePersistenceServiceSpy).toHaveBeenCalledExactlyOnceWith( @@ -174,7 +177,7 @@ describe('LoginService', () => { vi.spyOn(popUpService, 'isCurrentlyInPopup').mockReturnValue(true); // act - const result = await lastValueFrom( + const result = await firstValueFrom( service.loginWithPopUp(config, [config], authOptions) ); expect(result).toEqual({ diff --git a/src/login/login.service.ts b/src/login/login.service.ts index 709145b..cd099c8 100644 --- a/src/login/login.service.ts +++ b/src/login/login.service.ts @@ -1,5 +1,5 @@ import { Injectable, inject } from 'injection-js'; -import { type Observable, of } from 'rxjs'; +import { type Observable, of, throwError } from 'rxjs'; import type { AuthOptions } from '../auth-options'; import type { OpenIdConfiguration } from '../config/openid-configuration'; import { StoragePersistenceService } from '../storage/storage-persistence.service'; @@ -27,10 +27,13 @@ export class LoginService { login( configuration: OpenIdConfiguration | null, authOptions?: AuthOptions - ): void { + ): Observable { if (!configuration) { - throw new Error( - 'Please provide a configuration before setting up the module' + return throwError( + () => + new Error( + 'Please provide a configuration before setting up the module' + ) ); } @@ -45,10 +48,9 @@ export class LoginService { } if (usePushedAuthorisationRequests) { - this.parLoginService.loginPar(configuration, authOptions); - } else { - this.standardLoginService.loginStandard(configuration, authOptions); + return this.parLoginService.loginPar(configuration, authOptions); } + return this.standardLoginService.loginStandard(configuration, authOptions); } loginWithPopUp( diff --git a/src/login/par/par-login.service.spec.ts b/src/login/par/par-login.service.spec.ts index 6ffd68a..a797972 100644 --- a/src/login/par/par-login.service.spec.ts +++ b/src/login/par/par-login.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed, spyOnProperty } from '@/testing'; -import { lastValueFrom, of } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { vi } from 'vitest'; import { CheckAuthService } from '../../auth-state/check-auth.service'; import { AuthWellKnownService } from '../../config/auth-well-known/auth-well-known.service'; @@ -65,7 +65,7 @@ describe('ParLoginService', () => { ).mockReturnValue(false); const loggerSpy = vi.spyOn(loggerService, 'logError'); - const result = await lastValueFrom(service.loginPar({})); + const result = await firstValueFrom(service.loginPar({})); expect(result).toBeUndefined(); expect(loggerSpy).toHaveBeenCalled(); @@ -86,7 +86,7 @@ describe('ParLoginService', () => { .spyOn(parService, 'postParRequest') .mockReturnValue(of({ requestUri: 'requestUri' } as ParResponse)); - const result = await lastValueFrom( + const result = await firstValueFrom( service.loginPar({ authWellknownEndpointUrl: 'authWellknownEndpoint', responseType: 'stubValue', @@ -116,7 +116,7 @@ describe('ParLoginService', () => { .spyOn(parService, 'postParRequest') .mockReturnValue(of({ requestUri: 'requestUri' } as ParResponse)); - const result = await lastValueFrom( + const result = await firstValueFrom( service.loginPar(config, { customParams: { some: 'thing' }, }) @@ -149,7 +149,7 @@ describe('ParLoginService', () => { vi.spyOn(urlService, 'getAuthorizeParUrl').mockReturnValue(''); const spy = vi.spyOn(loggerService, 'logError'); - const result = await lastValueFrom(service.loginPar(config)); + const result = await firstValueFrom(service.loginPar(config)); expect(result).toBeUndefined(); expect(spy).toHaveBeenCalledTimes(1); @@ -180,7 +180,7 @@ describe('ParLoginService', () => { ); const spy = vi.spyOn(redirectService, 'redirectTo'); - await lastValueFrom(service.loginPar(config, authOptions)); + await firstValueFrom(service.loginPar(config, authOptions)); expect(spy).toHaveBeenCalledExactlyOnceWith('some-par-url'); }); @@ -212,7 +212,7 @@ describe('ParLoginService', () => { spy(url); }; - service.loginPar(config, { urlHandler }); + await firstValueFrom(service.loginPar(config, { urlHandler })); expect(spy).toHaveBeenCalledExactlyOnceWith('some-par-url'); expect(redirectToSpy).not.toHaveBeenCalled(); @@ -230,7 +230,7 @@ describe('ParLoginService', () => { const allConfigs = [config]; try { - await lastValueFrom(service.loginWithPopUpPar(config, allConfigs)); + await firstValueFrom(service.loginWithPopUpPar(config, allConfigs)); } catch (err: any) { expect(loggerSpy).toHaveBeenCalled(); expect(err.message).toBe('Invalid response type!'); @@ -258,7 +258,7 @@ describe('ParLoginService', () => { .mockReturnValue(of({ requestUri: 'requestUri' } as ParResponse)); try { - await lastValueFrom(service.loginWithPopUpPar(config, allConfigs)); + await firstValueFrom(service.loginWithPopUpPar(config, allConfigs)); } catch (err: any) { expect(spy).toHaveBeenCalled(); expect(err.message).toBe( @@ -288,7 +288,7 @@ describe('ParLoginService', () => { .mockReturnValue(of({ requestUri: 'requestUri' } as ParResponse)); try { - await lastValueFrom( + await firstValueFrom( service.loginWithPopUpPar(config, allConfigs, { customParams: { some: 'thing' }, }) @@ -326,7 +326,7 @@ describe('ParLoginService', () => { const spy = vi.spyOn(loggerService, 'logError'); try { - await lastValueFrom( + await firstValueFrom( service.loginWithPopUpPar(config, allConfigs, { customParams: { some: 'thing' }, }) @@ -369,7 +369,7 @@ describe('ParLoginService', () => { ); const spy = vi.spyOn(popupService, 'openPopUp'); - await lastValueFrom(service.loginWithPopUpPar(config, allConfigs)); + await firstValueFrom(service.loginWithPopUpPar(config, allConfigs)); expect(spy).toHaveBeenCalledExactlyOnceWith( 'some-par-url', undefined, @@ -419,7 +419,7 @@ describe('ParLoginService', () => { spyOnProperty(popupService, 'result$').mockReturnValue(of(popupResult)); - const result = await lastValueFrom( + const result = await firstValueFrom( service.loginWithPopUpPar(config, allConfigs) ); expect(checkAuthSpy).toHaveBeenCalledExactlyOnceWith( @@ -465,7 +465,7 @@ describe('ParLoginService', () => { spyOnProperty(popupService, 'result$').mockReturnValue(of(popupResult)); - const result = await lastValueFrom( + const result = await firstValueFrom( service.loginWithPopUpPar(config, allConfigs) ); expect(checkAuthSpy).not.toHaveBeenCalled(); diff --git a/src/login/par/par-login.service.ts b/src/login/par/par-login.service.ts index 8bf81cf..28c31a8 100644 --- a/src/login/par/par-login.service.ts +++ b/src/login/par/par-login.service.ts @@ -1,6 +1,6 @@ import { Injectable, inject } from 'injection-js'; import { type Observable, of, throwError } from 'rxjs'; -import { map, shareReplay, switchMap, take } from 'rxjs/operators'; +import { map, switchMap, take } from 'rxjs/operators'; import type { AuthOptions } from '../../auth-options'; import { CheckAuthService } from '../../auth-state/check-auth.service'; import { AuthWellKnownService } from '../../config/auth-well-known/auth-well-known.service'; @@ -47,7 +47,7 @@ export class ParLoginService { ) { this.loggerService.logError(configuration, 'Invalid response type!'); - return; + return of(undefined); } this.loggerService.logDebug( @@ -55,53 +55,42 @@ export class ParLoginService { 'BEGIN Authorize OIDC Flow, no auth data' ); - const result$ = this.authWellKnownService + return this.authWellKnownService .queryAndStoreAuthWellKnownEndPoints(configuration) .pipe( switchMap(() => this.parService.postParRequest(configuration, authOptions) ), - map(() => { - (response) => { - this.loggerService.logDebug( + map((response) => { + this.loggerService.logDebug( + configuration, + 'par response: ', + response + ); + + const url = this.urlService.getAuthorizeParUrl( + response.requestUri, + configuration + ); + + this.loggerService.logDebug(configuration, 'par request url: ', url); + + if (!url) { + this.loggerService.logError( configuration, - 'par response: ', - response + `Could not create URL with param ${response.requestUri}: '${url}'` ); - const url = this.urlService.getAuthorizeParUrl( - response.requestUri, - configuration - ); + return; + } - this.loggerService.logDebug( - configuration, - 'par request url: ', - url - ); - - if (!url) { - this.loggerService.logError( - configuration, - `Could not create URL with param ${response.requestUri}: '${url}'` - ); - - return; - } - - if (authOptions?.urlHandler) { - authOptions.urlHandler(url); - } else { - this.redirectService.redirectTo(url); - } - }; - }), - shareReplay(1) + if (authOptions?.urlHandler) { + authOptions.urlHandler(url); + } else { + this.redirectService.redirectTo(url); + } + }) ); - - result$.subscribe(); - - return result$; } loginWithPopUpPar( diff --git a/src/login/par/par.service.spec.ts b/src/login/par/par.service.spec.ts index d9799dc..736b6b7 100644 --- a/src/login/par/par.service.spec.ts +++ b/src/login/par/par.service.spec.ts @@ -1,6 +1,6 @@ import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { HttpHeaders } from '@ngify/http'; -import { lastValueFrom, of, throwError } from 'rxjs'; +import { firstValueFrom, of, throwError } from 'rxjs'; import { vi } from 'vitest'; import { DataService } from '../../api/data.service'; import { LoggerService } from '../../logging/logger.service'; @@ -20,6 +20,7 @@ describe('ParService', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ + ParService, mockProvider(LoggerService), mockProvider(UrlService), mockProvider(DataService), @@ -48,7 +49,7 @@ describe('ParService', () => { () => null ); try { - await lastValueFrom(service.postParRequest({ configId: 'configId1' })); + await firstValueFrom(service.postParRequest({ configId: 'configId1' })); } catch (err: any) { expect(err.message).toBe( 'Could not read PAR endpoint because authWellKnownEndPoints are not given' @@ -66,7 +67,7 @@ describe('ParService', () => { () => ({ some: 'thing' }) ); try { - await lastValueFrom(service.postParRequest({ configId: 'configId1' })); + await firstValueFrom(service.postParRequest({ configId: 'configId1' })); } catch (err: any) { expect(err.message).toBe( 'Could not read PAR endpoint from authWellKnownEndpoints' @@ -88,7 +89,7 @@ describe('ParService', () => { .spyOn(dataService, 'post') .mockReturnValue(of({})); - await lastValueFrom(service.postParRequest({ configId: 'configId1' })); + await firstValueFrom(service.postParRequest({ configId: 'configId1' })); expect(dataServiceSpy).toHaveBeenCalledExactlyOnceWith( 'parEndpoint', 'some-url123', @@ -109,7 +110,7 @@ describe('ParService', () => { vi.spyOn(dataService, 'post').mockReturnValue( of({ expires_in: 123, request_uri: 'request_uri' }) ); - const result = await lastValueFrom( + const result = await firstValueFrom( service.postParRequest({ configId: 'configId1' }) ); expect(result).toEqual({ expiresIn: 123, requestUri: 'request_uri' }); @@ -130,7 +131,7 @@ describe('ParService', () => { const loggerSpy = vi.spyOn(loggerService, 'logError'); try { - await lastValueFrom(service.postParRequest({ configId: 'configId1' })); + await firstValueFrom(service.postParRequest({ configId: 'configId1' })); } catch (err: any) { expect(err.message).toBe( 'There was an error on ParService postParRequest' @@ -159,7 +160,7 @@ describe('ParService', () => { ) ); - const res = await lastValueFrom( + const res = await firstValueFrom( service.postParRequest({ configId: 'configId1' }) ); expect(res).toBeTruthy(); @@ -183,7 +184,7 @@ describe('ParService', () => { ) ); - const res = await lastValueFrom( + const res = await firstValueFrom( service.postParRequest({ configId: 'configId1' }) ); expect(res).toBeTruthy(); @@ -209,7 +210,7 @@ describe('ParService', () => { ); try { - await lastValueFrom(service.postParRequest({ configId: 'configId1' })); + await firstValueFrom(service.postParRequest({ configId: 'configId1' })); } catch (err: any) { expect(err).toBeTruthy(); } diff --git a/src/login/popup/popup-login.service.spec.ts b/src/login/popup/popup-login.service.spec.ts index dbfbaba..0f0fff4 100644 --- a/src/login/popup/popup-login.service.spec.ts +++ b/src/login/popup/popup-login.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed, spyOnProperty } from '@/testing'; -import { lastValueFrom, of } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { vi } from 'vitest'; import { CheckAuthService } from '../../auth-state/check-auth.service'; import { AuthWellKnownService } from '../../config/auth-well-known/auth-well-known.service'; @@ -60,7 +60,7 @@ describe('PopUpLoginService', () => { const loggerSpy = vi.spyOn(loggerService, 'logError'); try { - await lastValueFrom( + await firstValueFrom( popUpLoginService.loginWithPopUpStandard(config, [config]) ); } catch (err: any) { @@ -91,7 +91,7 @@ describe('PopUpLoginService', () => { of({} as LoginResponse) ); - await lastValueFrom( + await firstValueFrom( popUpLoginService.loginWithPopUpStandard(config, [config]) ); expect(urlService.getAuthorizeUrl).toHaveBeenCalled(); @@ -120,7 +120,7 @@ describe('PopUpLoginService', () => { ); const popupSpy = vi.spyOn(popupService, 'openPopUp'); - await lastValueFrom( + await firstValueFrom( popUpLoginService.loginWithPopUpStandard(config, [config]) ); expect(popupSpy).toHaveBeenCalled(); @@ -160,7 +160,7 @@ describe('PopUpLoginService', () => { spyOnProperty(popupService, 'result$').mockReturnValue(of(popupResult)); - const result = await lastValueFrom( + const result = await firstValueFrom( popUpLoginService.loginWithPopUpStandard(config, [config]) ); expect(checkAuthSpy).toHaveBeenCalledExactlyOnceWith( @@ -201,7 +201,7 @@ describe('PopUpLoginService', () => { spyOnProperty(popupService, 'result$').mockReturnValue(of(popupResult)); - const result = await lastValueFrom( + const result = await firstValueFrom( popUpLoginService.loginWithPopUpStandard(config, [config]) ); expect(checkAuthSpy).not.toHaveBeenCalled(); diff --git a/src/login/popup/popup.service.spec.ts b/src/login/popup/popup.service.spec.ts index 3008425..992fabc 100644 --- a/src/login/popup/popup.service.spec.ts +++ b/src/login/popup/popup.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed, spyOnProperty } from '@/testing'; -import { lastValueFrom } from 'rxjs'; +import { ReplaySubject, firstValueFrom, map, share } from 'rxjs'; import { type MockInstance, vi } from 'vitest'; import type { OpenIdConfiguration } from '../../config/openid-configuration'; import { LoggerService } from '../../logging/logger.service'; @@ -18,6 +18,7 @@ describe('PopUpService', () => { providers: [ mockProvider(StoragePersistenceService), mockProvider(LoggerService), + PopUpService, ], }); storagePersistenceService = TestBed.inject(StoragePersistenceService); @@ -53,7 +54,11 @@ describe('PopUpService', () => { vi.spyOn(popUpService as any, 'canAccessSessionStorage').mockReturnValue( false ); - spyOnProperty(popUpService as any, 'windowInternal').mockReturnValue({ + spyOnProperty( + popUpService as any, + 'windowInternal', + 'get' + ).mockReturnValue({ opener: {} as Window, }); vi.spyOn(storagePersistenceService, 'read').mockReturnValue({ @@ -113,10 +118,23 @@ describe('PopUpService', () => { receivedUrl: 'some-url1111', }; - const result = await lastValueFrom(popUpService.result$); - expect(result).toBe(popupResult); + const test$ = popUpService.result$.pipe( + map((result) => { + expect(result).toBe(popupResult); + }), + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: true, + }) + ); + + test$.subscribe(); (popUpService as any).resultInternal$.next(popupResult); + + await firstValueFrom(test$); }); }); @@ -183,7 +201,8 @@ describe('PopUpService', () => { let popupResult: PopupResult; let cleanUpSpy: MockInstance; - beforeEach(async () => { + beforeEach(() => { + vi.useFakeTimers(); popup = { closed: false, close: () => undefined, @@ -195,8 +214,14 @@ describe('PopUpService', () => { popupResult = {} as PopupResult; - const result = await lastValueFrom(popUpService.result$); - popupResult = result; + popUpService.result$.subscribe((result) => { + popupResult = result; + }); + }); + + // biome-ignore lint/correctness/noUndeclaredVariables: + afterEach(() => { + vi.useRealTimers(); }); it('message received with data', async () => { @@ -273,9 +298,18 @@ describe('PopUpService', () => { }); describe('sendMessageToMainWindow', () => { + beforeEach(() => { + vi.useFakeTimers({}); + }); + + // biome-ignore lint/correctness/noUndeclaredVariables: + afterEach(() => { + vi.useRealTimers(); + }); + it('does nothing if window.opener is null', async () => { // arrange - spyOnProperty(window, 'opener').mockReturnValue(null); + spyOnProperty(window, 'opener', 'get', () => null); const sendMessageSpy = vi.spyOn(popUpService as any, 'sendMessage'); diff --git a/src/login/standard/standard-login.service.spec.ts b/src/login/standard/standard-login.service.spec.ts index 69f1e5a..4759311 100644 --- a/src/login/standard/standard-login.service.spec.ts +++ b/src/login/standard/standard-login.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@/testing'; -import { lastValueFrom, of } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { vi } from 'vitest'; import { AuthWellKnownService } from '../../config/auth-well-known/auth-well-known.service'; import { FlowsDataService } from '../../flows/flows-data.service'; @@ -20,6 +20,7 @@ describe('StandardLoginService', () => { let flowsDataService: FlowsDataService; beforeEach(() => { + vi.useFakeTimers(); TestBed.configureTestingModule({ imports: [], providers: [ @@ -44,6 +45,11 @@ describe('StandardLoginService', () => { flowsDataService = TestBed.inject(FlowsDataService); }); + // biome-ignore lint/correctness/noUndeclaredVariables: + afterEach(() => { + vi.useRealTimers(); + }); + it('should create', () => { expect(standardLoginService).toBeTruthy(); }); @@ -56,7 +62,7 @@ describe('StandardLoginService', () => { ).mockReturnValue(false); const loggerSpy = vi.spyOn(loggerService, 'logError'); - const result = await lastValueFrom( + const result = await firstValueFrom( standardLoginService.loginStandard({ configId: 'configId1', }) @@ -83,7 +89,7 @@ describe('StandardLoginService', () => { vi.spyOn(urlService, 'getAuthorizeUrl').mockReturnValue(of('someUrl')); const flowsDataSpy = vi.spyOn(flowsDataService, 'setCodeFlowInProgress'); - const result = await lastValueFrom( + const result = await firstValueFrom( standardLoginService.loginStandard(config) ); @@ -107,7 +113,7 @@ describe('StandardLoginService', () => { ).mockReturnValue(of({})); vi.spyOn(urlService, 'getAuthorizeUrl').mockReturnValue(of('someUrl')); - const result = await lastValueFrom( + const result = await firstValueFrom( standardLoginService.loginStandard(config) ); @@ -131,7 +137,7 @@ describe('StandardLoginService', () => { vi.spyOn(urlService, 'getAuthorizeUrl').mockReturnValue(of('someUrl')); const redirectSpy = vi.spyOn(redirectService, 'redirectTo'); - standardLoginService.loginStandard(config); + await firstValueFrom(standardLoginService.loginStandard(config)); await vi.advanceTimersByTimeAsync(0); expect(redirectSpy).toHaveBeenCalledExactlyOnceWith('someUrl'); }); @@ -159,7 +165,9 @@ describe('StandardLoginService', () => { spy(url); }; - standardLoginService.loginStandard(config, { urlHandler }); + await firstValueFrom( + standardLoginService.loginStandard(config, { urlHandler }) + ); await vi.advanceTimersByTimeAsync(0); expect(spy).toHaveBeenCalledExactlyOnceWith('someUrl'); expect(redirectSpy).not.toHaveBeenCalled(); @@ -185,7 +193,7 @@ describe('StandardLoginService', () => { 'resetSilentRenewRunning' ); - standardLoginService.loginStandard(config, {}); + await firstValueFrom(standardLoginService.loginStandard(config, {})); await vi.advanceTimersByTimeAsync(0); expect(flowsDataSpy).toHaveBeenCalled(); @@ -212,9 +220,11 @@ describe('StandardLoginService', () => { .spyOn(redirectService, 'redirectTo') .mockImplementation(() => undefined); - standardLoginService.loginStandard(config, { - customParams: { to: 'add', as: 'well' }, - }); + await firstValueFrom( + standardLoginService.loginStandard(config, { + customParams: { to: 'add', as: 'well' }, + }) + ); await vi.advanceTimersByTimeAsync(0); expect(redirectSpy).toHaveBeenCalledExactlyOnceWith('someUrl'); expect(getAuthorizeUrlSpy).toHaveBeenCalledExactlyOnceWith(config, { @@ -243,7 +253,7 @@ describe('StandardLoginService', () => { .spyOn(redirectService, 'redirectTo') .mockImplementation(() => undefined); - standardLoginService.loginStandard(config); + await firstValueFrom(standardLoginService.loginStandard(config)); await vi.advanceTimersByTimeAsync(0); expect(loggerSpy).toHaveBeenCalledExactlyOnceWith( config, diff --git a/src/login/standard/standard-login.service.ts b/src/login/standard/standard-login.service.ts index 6c32268..53eb1d5 100644 --- a/src/login/standard/standard-login.service.ts +++ b/src/login/standard/standard-login.service.ts @@ -1,5 +1,5 @@ import { Injectable, inject } from 'injection-js'; -import { type Observable, map, shareReplay, switchMap } from 'rxjs'; +import { type Observable, map, of, switchMap } from 'rxjs'; import type { AuthOptions } from '../../auth-options'; import { AuthWellKnownService } from '../../config/auth-well-known/auth-well-known.service'; import type { OpenIdConfiguration } from '../../config/openid-configuration'; @@ -28,7 +28,7 @@ export class StandardLoginService { loginStandard( configuration: OpenIdConfiguration, authOptions?: AuthOptions - ): Observable { + ): Observable { if ( !this.responseTypeValidationService.hasConfigValidResponseType( configuration @@ -36,7 +36,7 @@ export class StandardLoginService { ) { this.loggerService.logError(configuration, 'Invalid response type!'); - return; + return of(undefined); } this.loggerService.logDebug( @@ -45,7 +45,7 @@ export class StandardLoginService { ); this.flowsDataService.setCodeFlowInProgress(configuration); - const result$ = this.authWellKnownService + return this.authWellKnownService .queryAndStoreAuthWellKnownEndPoints(configuration) .pipe( switchMap(() => { @@ -70,12 +70,8 @@ export class StandardLoginService { } else { this.redirectService.redirectTo(url); } - }), - shareReplay(1) + return undefined; + }) ); - - result$.subscribe(); - - return result$; } } diff --git a/src/logoff-revoke/logoff-revocation.service.spec.ts b/src/logoff-revoke/logoff-revocation.service.spec.ts index 7854d86..326c440 100644 --- a/src/logoff-revoke/logoff-revocation.service.spec.ts +++ b/src/logoff-revoke/logoff-revocation.service.spec.ts @@ -1,6 +1,6 @@ import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import type { HttpHeaders } from '@ngify/http'; -import { Observable, lastValueFrom, of, throwError } from 'rxjs'; +import { Observable, firstValueFrom, of, throwError } from 'rxjs'; import { vi } from 'vitest'; import { DataService } from '../api/data.service'; import { ResetAuthDataService } from '../flows/reset-auth-data.service'; @@ -26,6 +26,7 @@ describe('Logout and Revoke Service', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ + LogoffRevocationService, mockProvider(DataService), mockProvider(LoggerService), mockProvider(StoragePersistenceService), @@ -120,7 +121,7 @@ describe('Logout and Revoke Service', () => { const config = { configId: 'configId1' }; // Act - const result = await lastValueFrom(service.revokeAccessToken(config)); + const result = await firstValueFrom(service.revokeAccessToken(config)); expect(result).toEqual({ data: 'anything' }); expect(loggerSpy).toHaveBeenCalled(); }); @@ -142,7 +143,7 @@ describe('Logout and Revoke Service', () => { // Act try { - await lastValueFrom(service.revokeAccessToken(config)); + await firstValueFrom(service.revokeAccessToken(config)); } catch (err: any) { expect(loggerSpy).toHaveBeenCalled(); expect(err).toBeTruthy(); @@ -167,7 +168,7 @@ describe('Logout and Revoke Service', () => { ) ); - const res = await lastValueFrom(service.revokeAccessToken(config)); + const res = await firstValueFrom(service.revokeAccessToken(config)); expect(res).toBeTruthy(); expect(res).toEqual({ data: 'anything' }); expect(loggerSpy).toHaveBeenCalled(); @@ -192,7 +193,7 @@ describe('Logout and Revoke Service', () => { ) ); - const res = await lastValueFrom(service.revokeAccessToken(config)); + const res = await firstValueFrom(service.revokeAccessToken(config)); expect(res).toBeTruthy(); expect(res).toEqual({ data: 'anything' }); expect(loggerSpy).toHaveBeenCalled(); @@ -219,7 +220,7 @@ describe('Logout and Revoke Service', () => { ); try { - await lastValueFrom(service.revokeAccessToken(config)); + await firstValueFrom(service.revokeAccessToken(config)); } catch (err: any) { expect(err).toBeTruthy(); expect(loggerSpy).toHaveBeenCalled(); @@ -297,7 +298,7 @@ describe('Logout and Revoke Service', () => { const config = { configId: 'configId1' }; // Act - const result = await lastValueFrom(service.revokeRefreshToken(config)); + const result = await firstValueFrom(service.revokeRefreshToken(config)); expect(result).toEqual({ data: 'anything' }); expect(loggerSpy).toHaveBeenCalled(); }); @@ -319,7 +320,7 @@ describe('Logout and Revoke Service', () => { // Act try { - await lastValueFrom(service.revokeRefreshToken(config)); + await firstValueFrom(service.revokeRefreshToken(config)); } catch (err: any) { expect(loggerSpy).toHaveBeenCalled(); expect(err).toBeTruthy(); @@ -344,7 +345,7 @@ describe('Logout and Revoke Service', () => { ) ); - const res = await lastValueFrom(service.revokeRefreshToken(config)); + const res = await firstValueFrom(service.revokeRefreshToken(config)); expect(res).toBeTruthy(); expect(res).toEqual({ data: 'anything' }); expect(loggerSpy).toHaveBeenCalled(); @@ -369,7 +370,7 @@ describe('Logout and Revoke Service', () => { ) ); - const res = await lastValueFrom(service.revokeRefreshToken(config)); + const res = await firstValueFrom(service.revokeRefreshToken(config)); expect(res).toBeTruthy(); expect(res).toEqual({ data: 'anything' }); expect(loggerSpy).toHaveBeenCalled(); @@ -396,7 +397,7 @@ describe('Logout and Revoke Service', () => { ); try { - await lastValueFrom(service.revokeRefreshToken(config)); + await firstValueFrom(service.revokeRefreshToken(config)); } catch (err: any) { expect(err).toBeTruthy(); expect(loggerSpy).toHaveBeenCalled(); @@ -419,7 +420,7 @@ describe('Logout and Revoke Service', () => { const result$ = service.logoff(config, [config]); // Assert - await lastValueFrom(result$); + await firstValueFrom(result$); expect(serverStateChangedSpy).not.toHaveBeenCalled(); }); @@ -435,7 +436,7 @@ describe('Logout and Revoke Service', () => { const result$ = service.logoff(config, [config]); // Assert - await lastValueFrom(result$); + await firstValueFrom(result$); expect(redirectSpy).not.toHaveBeenCalled(); }); @@ -461,7 +462,7 @@ describe('Logout and Revoke Service', () => { const result$ = service.logoff(config, [config], { urlHandler }); // Assert - await lastValueFrom(result$); + await firstValueFrom(result$); expect(redirectSpy).not.toHaveBeenCalled(); expect(spy).toHaveBeenCalledExactlyOnceWith('someValue'); expect(resetAuthorizationDataSpy).toHaveBeenCalled(); @@ -482,7 +483,7 @@ describe('Logout and Revoke Service', () => { const result$ = service.logoff(config, [config]); // Assert - await lastValueFrom(result$); + await firstValueFrom(result$); expect(redirectSpy).toHaveBeenCalledExactlyOnceWith('someValue'); }); @@ -501,7 +502,7 @@ describe('Logout and Revoke Service', () => { const result$ = service.logoff(config, [config], { logoffMethod: 'GET' }); // Assert - await lastValueFrom(result$); + await firstValueFrom(result$); expect(redirectSpy).toHaveBeenCalledExactlyOnceWith('someValue'); }); @@ -533,7 +534,7 @@ describe('Logout and Revoke Service', () => { }); // Assert - await lastValueFrom(result$); + await firstValueFrom(result$); expect(redirectSpy).not.toHaveBeenCalled(); expect(postSpy).toHaveBeenCalledExactlyOnceWith( 'some-url', @@ -585,7 +586,7 @@ describe('Logout and Revoke Service', () => { }); // Assert - await lastValueFrom(result$); + await firstValueFrom(result$); expect(redirectSpy).not.toHaveBeenCalled(); expect(postSpy).toHaveBeenCalledExactlyOnceWith( 'some-url', @@ -647,7 +648,7 @@ describe('Logout and Revoke Service', () => { .mockReturnValue(of({ any: 'thing' })); // Act - await lastValueFrom(service.logoffAndRevokeTokens(config, [config])); + await firstValueFrom(service.logoffAndRevokeTokens(config, [config])); expect(revokeRefreshTokenSpy).toHaveBeenCalled(); expect(revokeAccessTokenSpy).toHaveBeenCalled(); }); @@ -676,7 +677,7 @@ describe('Logout and Revoke Service', () => { // Act try { - await lastValueFrom(service.logoffAndRevokeTokens(config, [config])); + await firstValueFrom(service.logoffAndRevokeTokens(config, [config])); } catch (err: any) { expect(loggerSpy).toHaveBeenCalled(); expect(err).toBeTruthy(); @@ -700,7 +701,7 @@ describe('Logout and Revoke Service', () => { const config = { configId: 'configId1' }; // Act - await lastValueFrom(service.logoffAndRevokeTokens(config, [config])); + await firstValueFrom(service.logoffAndRevokeTokens(config, [config])); expect(logoffSpy).toHaveBeenCalled(); }); @@ -722,7 +723,7 @@ describe('Logout and Revoke Service', () => { const config = { configId: 'configId1' }; // Act - await lastValueFrom( + await firstValueFrom( service.logoffAndRevokeTokens(config, [config], { urlHandler }) ); expect(logoffSpy).toHaveBeenCalledExactlyOnceWith(config, [config], { @@ -749,7 +750,7 @@ describe('Logout and Revoke Service', () => { .mockReturnValue(of({ any: 'thing' })); // Act - await lastValueFrom(service.logoffAndRevokeTokens(config, [config])); + await firstValueFrom(service.logoffAndRevokeTokens(config, [config])); expect(revokeRefreshTokenSpy).not.toHaveBeenCalled(); expect(revokeAccessTokenSpy).toHaveBeenCalled(); }); @@ -774,7 +775,7 @@ describe('Logout and Revoke Service', () => { // Act try { - await lastValueFrom(service.logoffAndRevokeTokens(config, [config])); + await firstValueFrom(service.logoffAndRevokeTokens(config, [config])); } catch (err: any) { expect(loggerSpy).toHaveBeenCalled(); expect(err).toBeTruthy(); @@ -798,7 +799,7 @@ describe('Logout and Revoke Service', () => { // Assert expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(2); expect(checkSessionServiceSpy).toHaveBeenCalledTimes(2); - expect(resetAuthorizationDataSpy).toBeCalledWith([ + expect(resetAuthorizationDataSpy.mock.calls).toEqual([ [allConfigs[0]!, allConfigs], [allConfigs[1], allConfigs], ]); diff --git a/src/oidc.security.service.spec.ts b/src/oidc.security.service.spec.ts index d7f3890..60ea71e 100644 --- a/src/oidc.security.service.spec.ts +++ b/src/oidc.security.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed, spyOnProperty } from '@/testing'; -import { Observable, lastValueFrom, of } from 'rxjs'; +import { Observable, firstValueFrom, of } from 'rxjs'; import { type MockInstance, vi } from 'vitest'; import { AuthStateService } from './auth-state/auth-state.service'; import { CheckAuthService } from './auth-state/check-auth.service'; @@ -89,36 +89,38 @@ describe('OidcSecurityService', () => { expect(oidcSecurityService).toBeTruthy(); }); - describe('userData$', () => { - it('calls userService.userData$', async () => { - await lastValueFrom(oidcSecurityService.userData$); - // 1x from this subscribe - // 1x by the signal property - expect(userDataSpy).toHaveBeenCalledTimes(2); - }); - }); + // without signal + // describe('userData$', () => { + // it('calls userService.userData$', async () => { + // await firstValueFrom(oidcSecurityService.userData()); + // // 1x from this subscribe + // // 1x by the signal property + // expect(userDataSpy).toHaveBeenCalledTimes(2); + // }); + // }); describe('userData', () => { it('calls userService.userData$', async () => { - const _userdata = await lastValueFrom(oidcSecurityService.userData()); + const _userdata = await firstValueFrom(oidcSecurityService.userData$); expect(userDataSpy).toHaveBeenCalledTimes(1); }); }); - describe('isAuthenticated$', () => { - it('calls authStateService.isAuthenticated$', async () => { - await lastValueFrom(oidcSecurityService.isAuthenticated$); - // 1x from this subscribe - // 1x by the signal property - expect(authenticatedSpy).toHaveBeenCalledTimes(2); - }); - }); + // describe('isAuthenticated$', () => { + // it('calls authStateService.isAuthenticated$', async () => { + // await firstValueFrom(oidcSecurityService.isAuthenticated()); + // // 1x from this subscribe + // // 1x by the signal property + // expect(authenticatedSpy).toHaveBeenCalledTimes(2); + // }); + // }); + // without signal describe('authenticated', () => { it('calls authStateService.isAuthenticated$', async () => { - const _authenticated = await lastValueFrom( - oidcSecurityService.authenticated() + const _authenticated = await firstValueFrom( + oidcSecurityService.isAuthenticated$ ); expect(authenticatedSpy).toHaveBeenCalledTimes(1); @@ -131,19 +133,20 @@ describe('OidcSecurityService', () => { checkSessionService, 'checkSessionChanged$' ).mockReturnValue(of(true)); - await lastValueFrom(oidcSecurityService.checkSessionChanged$); + await firstValueFrom(oidcSecurityService.checkSessionChanged$); expect(spy).toHaveBeenCalledTimes(1); }); }); describe('stsCallback$', () => { - it('calls callbackService.stsCallback$', async () => { + it('calls callbackService.stsCallback$', () => { const spy = spyOnProperty( callbackService, 'stsCallback$' ).mockReturnValue(of()); - await lastValueFrom(oidcSecurityService.stsCallback$); + oidcSecurityService.stsCallback$.subscribe(); + expect(spy).toHaveBeenCalledTimes(1); }); }); @@ -159,7 +162,7 @@ describe('OidcSecurityService', () => { .spyOn(authWellKnownService, 'queryAndStoreAuthWellKnownEndPoints') .mockReturnValue(of({})); - await lastValueFrom(oidcSecurityService.preloadAuthWellKnownDocument()); + await firstValueFrom(oidcSecurityService.preloadAuthWellKnownDocument()); expect(spy).toHaveBeenCalledExactlyOnceWith(config); }); }); @@ -210,7 +213,7 @@ describe('OidcSecurityService', () => { some: 'thing', }); - await lastValueFrom(oidcSecurityService.getUserData('configId')); + await firstValueFrom(oidcSecurityService.getUserData('configId')); expect(spy).toHaveBeenCalledExactlyOnceWith(config); }); @@ -225,8 +228,10 @@ describe('OidcSecurityService', () => { some: 'thing', }); - const result = await lastValueFrom(oidcSecurityService.getUserData('configId')); -expect(result).toEqual({ some: 'thing' }); + const result = await firstValueFrom( + oidcSecurityService.getUserData('configId') + ); + expect(result).toEqual({ some: 'thing' }); }); }); @@ -242,7 +247,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(checkAuthService, 'checkAuth') .mockReturnValue(of({} as LoginResponse)); - await lastValueFrom(oidcSecurityService.checkAuth()); + await firstValueFrom(oidcSecurityService.checkAuth()); expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config], undefined); }); @@ -257,7 +262,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(checkAuthService, 'checkAuth') .mockReturnValue(of({} as LoginResponse)); - await lastValueFrom(oidcSecurityService.checkAuth('some-url')); + await firstValueFrom(oidcSecurityService.checkAuth('some-url')); expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config], 'some-url'); }); }); @@ -274,7 +279,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(checkAuthService, 'checkAuthMultiple') .mockReturnValue(of([{}] as LoginResponse[])); - await lastValueFrom(oidcSecurityService.checkAuthMultiple()); + await firstValueFrom(oidcSecurityService.checkAuthMultiple()); expect(spy).toHaveBeenCalledExactlyOnceWith([config], undefined); }); @@ -289,8 +294,8 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(checkAuthService, 'checkAuthMultiple') .mockReturnValue(of([{}] as LoginResponse[])); - await lastValueFrom(oidcSecurityService.checkAuthMultiple('some-url')); - expect(spy).toHaveBeenCalledExactlyOnceWith([config], 'some-u-+rl'); + await firstValueFrom(oidcSecurityService.checkAuthMultiple('some-url')); + expect(spy).toHaveBeenCalledExactlyOnceWith([config], 'some-url'); }); }); @@ -306,7 +311,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(authStateService, 'isAuthenticated') .mockReturnValue(true); - await lastValueFrom(oidcSecurityService.isAuthenticated()); + await firstValueFrom(oidcSecurityService.isAuthenticated()); expect(spy).toHaveBeenCalledExactlyOnceWith(config); }); }); @@ -323,7 +328,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(checkAuthService, 'checkAuthIncludingServer') .mockReturnValue(of({} as LoginResponse)); - await lastValueFrom(oidcSecurityService.checkAuthIncludingServer()); + await firstValueFrom(oidcSecurityService.checkAuthIncludingServer()); expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config]); }); }); @@ -340,7 +345,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(authStateService, 'getAccessToken') .mockReturnValue(''); - await lastValueFrom(oidcSecurityService.getAccessToken()); + await firstValueFrom(oidcSecurityService.getAccessToken()); expect(spy).toHaveBeenCalledExactlyOnceWith(config); }); }); @@ -355,7 +360,7 @@ expect(result).toEqual({ some: 'thing' }); const spy = vi.spyOn(authStateService, 'getIdToken').mockReturnValue(''); - await lastValueFrom(oidcSecurityService.getIdToken()); + await firstValueFrom(oidcSecurityService.getIdToken()); expect(spy).toHaveBeenCalledExactlyOnceWith(config); }); }); @@ -371,7 +376,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(authStateService, 'getRefreshToken') .mockReturnValue(''); - await lastValueFrom(oidcSecurityService.getRefreshToken()); + await firstValueFrom(oidcSecurityService.getRefreshToken()); expect(spy).toHaveBeenCalledExactlyOnceWith(config); }); }); @@ -388,7 +393,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(authStateService, 'getAuthenticationResult') .mockReturnValue(null); - await lastValueFrom(oidcSecurityService.getAuthenticationResult()); + await firstValueFrom(oidcSecurityService.getAuthenticationResult()); expect(spy).toHaveBeenCalledExactlyOnceWith(config); }); }); @@ -405,7 +410,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(tokenHelperService, 'getPayloadFromToken') .mockReturnValue(null); - await lastValueFrom(oidcSecurityService.getPayloadFromIdToken()); + await firstValueFrom(oidcSecurityService.getPayloadFromIdToken()); expect(spy).toHaveBeenCalledExactlyOnceWith('some-token', false, config); }); @@ -420,7 +425,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(tokenHelperService, 'getPayloadFromToken') .mockReturnValue(null); - await lastValueFrom(oidcSecurityService.getPayloadFromIdToken(true)); + await firstValueFrom(oidcSecurityService.getPayloadFromIdToken(true)); expect(spy).toHaveBeenCalledExactlyOnceWith('some-token', true, config); }); }); @@ -439,7 +444,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(tokenHelperService, 'getPayloadFromToken') .mockReturnValue(null); - await lastValueFrom(oidcSecurityService.getPayloadFromAccessToken()); + await firstValueFrom(oidcSecurityService.getPayloadFromAccessToken()); expect(spy).toHaveBeenCalledExactlyOnceWith( 'some-access-token', false, @@ -460,7 +465,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(tokenHelperService, 'getPayloadFromToken') .mockReturnValue(null); - await lastValueFrom(oidcSecurityService.getPayloadFromAccessToken(true)); + await firstValueFrom(oidcSecurityService.getPayloadFromAccessToken(true)); expect(spy).toHaveBeenCalledExactlyOnceWith( 'some-access-token', true, @@ -478,7 +483,7 @@ expect(result).toEqual({ some: 'thing' }); ); const spy = vi.spyOn(flowsDataService, 'setAuthStateControl'); - await lastValueFrom(oidcSecurityService.setState('anyString')); + await firstValueFrom(oidcSecurityService.setState('anyString')); expect(spy).toHaveBeenCalledExactlyOnceWith('anyString', config); }); }); @@ -492,7 +497,7 @@ expect(result).toEqual({ some: 'thing' }); ); const spy = vi.spyOn(flowsDataService, 'getAuthStateControl'); - await lastValueFrom(oidcSecurityService.getState()); + await firstValueFrom(oidcSecurityService.getState()); expect(spy).toHaveBeenCalledExactlyOnceWith(config); }); }); @@ -504,9 +509,11 @@ expect(result).toEqual({ some: 'thing' }); vi.spyOn(configurationService, 'getOpenIDConfiguration').mockReturnValue( of(config) ); - const spy = vi.spyOn(loginService, 'login'); + const spy = vi + .spyOn(loginService, 'login') + .mockReturnValue(of(undefined)); - await lastValueFrom(oidcSecurityService.authorize()); + await firstValueFrom(oidcSecurityService.authorize()); expect(spy).toHaveBeenCalledExactlyOnceWith(config, undefined); }); @@ -517,9 +524,11 @@ expect(result).toEqual({ some: 'thing' }); vi.spyOn(configurationService, 'getOpenIDConfiguration').mockReturnValue( of(config) ); - const spy = vi.spyOn(loginService, 'login'); + const spy = vi + .spyOn(loginService, 'login') + .mockReturnValue(of(undefined)); - await lastValueFrom( + await firstValueFrom( oidcSecurityService.authorize('configId', { customParams: { some: 'param' }, }) @@ -542,7 +551,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(loginService, 'loginWithPopUp') .mockImplementation(() => of({} as LoginResponse)); - await lastValueFrom(oidcSecurityService.authorizeWithPopUp()); + await firstValueFrom(oidcSecurityService.authorizeWithPopUp()); expect(spy).toHaveBeenCalledExactlyOnceWith( config, [config], @@ -564,7 +573,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(refreshSessionService, 'userForceRefreshSession') .mockReturnValue(of({} as LoginResponse)); - await lastValueFrom(oidcSecurityService.forceRefreshSession()); + await firstValueFrom(oidcSecurityService.forceRefreshSession()); expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config], undefined); }); }); @@ -580,7 +589,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(logoffRevocationService, 'logoffAndRevokeTokens') .mockReturnValue(of(null)); - await lastValueFrom(oidcSecurityService.logoffAndRevokeTokens()); + await firstValueFrom(oidcSecurityService.logoffAndRevokeTokens()); expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config], undefined); }); }); @@ -596,7 +605,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(logoffRevocationService, 'logoff') .mockReturnValue(of(null)); - await lastValueFrom(oidcSecurityService.logoff()); + await firstValueFrom(oidcSecurityService.logoff()); expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config], undefined); }); }); @@ -609,7 +618,7 @@ expect(result).toEqual({ some: 'thing' }); of({ allConfigs: [config], currentConfig: config }) ); const spy = vi.spyOn(logoffRevocationService, 'logoffLocal'); - await lastValueFrom(oidcSecurityService.logoffLocal()); + await firstValueFrom(oidcSecurityService.logoffLocal()); expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config]); }); }); @@ -623,7 +632,7 @@ expect(result).toEqual({ some: 'thing' }); ); const spy = vi.spyOn(logoffRevocationService, 'logoffLocalMultiple'); - await lastValueFrom(oidcSecurityService.logoffLocalMultiple()); + await firstValueFrom(oidcSecurityService.logoffLocalMultiple()); expect(spy).toHaveBeenCalledExactlyOnceWith([config]); }); }); @@ -639,7 +648,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(logoffRevocationService, 'revokeAccessToken') .mockReturnValue(of(null)); - await lastValueFrom(oidcSecurityService.revokeAccessToken()); + await firstValueFrom(oidcSecurityService.revokeAccessToken()); expect(spy).toHaveBeenCalledExactlyOnceWith(config, undefined); }); @@ -653,7 +662,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(logoffRevocationService, 'revokeAccessToken') .mockReturnValue(of(null)); - await lastValueFrom( + await firstValueFrom( oidcSecurityService.revokeAccessToken('access_token') ); expect(spy).toHaveBeenCalledExactlyOnceWith(config, 'access_token'); @@ -671,7 +680,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(logoffRevocationService, 'revokeRefreshToken') .mockReturnValue(of(null)); - await lastValueFrom(oidcSecurityService.revokeRefreshToken()); + await firstValueFrom(oidcSecurityService.revokeRefreshToken()); expect(spy).toHaveBeenCalledExactlyOnceWith(config, undefined); }); @@ -685,7 +694,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(logoffRevocationService, 'revokeRefreshToken') .mockReturnValue(of(null)); - await lastValueFrom( + await firstValueFrom( oidcSecurityService.revokeRefreshToken('refresh_token') ); expect(spy).toHaveBeenCalledExactlyOnceWith(config, 'refresh_token'); @@ -704,7 +713,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(urlService, 'getEndSessionUrl') .mockReturnValue(null); - await lastValueFrom(oidcSecurityService.getEndSessionUrl()); + await firstValueFrom(oidcSecurityService.getEndSessionUrl()); expect(spy).toHaveBeenCalledExactlyOnceWith(config, undefined); }); @@ -719,7 +728,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(urlService, 'getEndSessionUrl') .mockReturnValue(null); - await lastValueFrom( + await firstValueFrom( oidcSecurityService.getEndSessionUrl({ custom: 'params' }) ); expect(spy).toHaveBeenCalledExactlyOnceWith(config, { @@ -740,7 +749,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(urlService, 'getAuthorizeUrl') .mockReturnValue(of(null)); - await lastValueFrom(oidcSecurityService.getAuthorizeUrl()); + await firstValueFrom(oidcSecurityService.getAuthorizeUrl()); expect(spy).toHaveBeenCalledExactlyOnceWith(config, undefined); }); @@ -755,7 +764,7 @@ expect(result).toEqual({ some: 'thing' }); .spyOn(urlService, 'getAuthorizeUrl') .mockReturnValue(of(null)); - await lastValueFrom( + await firstValueFrom( oidcSecurityService.getAuthorizeUrl({ custom: 'params' }) ); expect(spy).toHaveBeenCalledExactlyOnceWith(config, { diff --git a/src/oidc.security.service.ts b/src/oidc.security.service.ts index fa6030a..802b463 100644 --- a/src/oidc.security.service.ts +++ b/src/oidc.security.service.ts @@ -1,6 +1,6 @@ import { Injectable, inject } from 'injection-js'; -import type { Observable } from 'rxjs'; -import { concatMap, map, shareReplay } from 'rxjs/operators'; +import { BehaviorSubject, type Observable } from 'rxjs'; +import { concatMap, map, switchMap } from 'rxjs/operators'; import type { AuthOptions, LogoutAuthOptions } from './auth-options'; import type { AuthenticatedResult } from './auth-state/auth-result'; import { AuthStateService } from './auth-state/auth-state.service'; @@ -20,6 +20,7 @@ import type { PopupOptions } from './login/popup/popup-options'; import { LogoffRevocationService } from './logoff-revoke/logoff-revocation.service'; import { UserService } from './user-data/user.service'; import type { UserDataResult } from './user-data/userdata-result'; +import { MockUtil } from './utils/reflect/index'; import { TokenHelperService } from './utils/tokenHelper/token-helper.service'; import { UrlService } from './utils/url/url.service'; @@ -160,6 +161,8 @@ export class OidcSecurityService { * * @returns An array of `LoginResponse` objects containing all information about the logins */ + + @MockUtil({ implementation: () => new BehaviorSubject(undefined) }) checkAuthMultiple(url?: string): Observable { return this.configurationService .getOpenIDConfigurations() @@ -336,16 +339,11 @@ export class OidcSecurityService { * @param authOptions The custom options for the the authentication request. */ authorize(configId?: string, authOptions?: AuthOptions): Observable { - const result$ = this.configurationService + return this.configurationService .getOpenIDConfiguration(configId) .pipe( - map((config) => this.loginService.login(config, authOptions)), - shareReplay(1) + switchMap((config) => this.loginService.login(config, authOptions)) ); - - result$.subscribe(); - - return result$; } /** @@ -458,17 +456,13 @@ export class OidcSecurityService { * * @param configId The configId to perform the action in behalf of. If not passed, the first configs will be taken */ - logoffLocal(configId?: string): Observable { - const result$ = this.configurationService - .getOpenIDConfigurations(configId) - .pipe( - map(({ allConfigs, currentConfig }) => - this.logoffRevocationService.logoffLocal(currentConfig, allConfigs) - ), - shareReplay(1) - ); - result$.subscribe(); - return result$; + logoffLocal(configId?: string): Observable { + return this.configurationService.getOpenIDConfigurations(configId).pipe( + map(({ allConfigs, currentConfig }) => { + this.logoffRevocationService.logoffLocal(currentConfig, allConfigs); + return undefined; + }) + ); } /** @@ -476,16 +470,13 @@ export class OidcSecurityService { * Use this method if you have _multiple_ configs enabled. */ logoffLocalMultiple(): Observable { - const result$ = this.configurationService.getOpenIDConfigurations().pipe( - map(({ allConfigs }) => - this.logoffRevocationService.logoffLocalMultiple(allConfigs) - ), - shareReplay(1) - ); - - result$.subscribe(); - - return result$; + return this.configurationService + .getOpenIDConfigurations() + .pipe( + map(({ allConfigs }) => + this.logoffRevocationService.logoffLocalMultiple(allConfigs) + ) + ); } /** diff --git a/src/provide-auth.spec.ts b/src/provide-auth.spec.ts index ef27217..fd63ce3 100644 --- a/src/provide-auth.spec.ts +++ b/src/provide-auth.spec.ts @@ -1,7 +1,8 @@ -import { TestBed, createSpyObj } from '@/testing'; -import { mockProvider } from '@/testing/mock'; +import { TestBed } from '@/testing'; +import { mockClass, mockProvider } from '@/testing/mock'; import { APP_INITIALIZER } from 'oidc-client-rx'; import { of } from 'rxjs'; +import { vi } from 'vitest'; import { PASSED_CONFIG } from './auth-config'; import { ConfigurationService } from './config/config.service'; import { @@ -60,12 +61,13 @@ describe('provideAuth', () => { describe('features', () => { let oidcSecurityServiceMock: OidcSecurityService; + let spy: any; beforeEach(async () => { - oidcSecurityServiceMock = createSpyObj( - 'OidcSecurityService', - ['checkAuthMultiple'] - ); + //@ts-ignore + + oidcSecurityServiceMock = new (mockClass(OidcSecurityService))(); + spy = vi.spyOn(oidcSecurityServiceMock, 'checkAuthMultiple'); await TestBed.configureTestingModule({ providers: [ provideAuth( @@ -83,14 +85,11 @@ describe('provideAuth', () => { it('should provide APP_INITIALIZER config', () => { const config = TestBed.inject(APP_INITIALIZER); - expect( config.length, 'Expected an APP_INITIALIZER to be registered' ).toBe(1); - expect(oidcSecurityServiceMock.checkAuthMultiple).toHaveBeenCalledTimes( - 1 - ); + expect(spy).toHaveBeenCalledTimes(1); }); }); }); diff --git a/src/provide-auth.ts b/src/provide-auth.ts index 3f12f7f..82bfcb3 100644 --- a/src/provide-auth.ts +++ b/src/provide-auth.ts @@ -1,4 +1,5 @@ import type { Provider } from 'injection-js'; +import { firstValueFrom } from 'rxjs'; import { PASSED_CONFIG, type PassedInitialConfig, @@ -65,7 +66,7 @@ export function withAppInitializerAuthCheck(): AuthFeature { ɵproviders: [ { provide: APP_INITIALIZER, - useFactory: (oidcSecurityService: OidcSecurityService) => () => + useFactory: (oidcSecurityService: OidcSecurityService) => oidcSecurityService.checkAuthMultiple(), multi: true, deps: [OidcSecurityService], diff --git a/src/public-events/public-events.service.spec.ts b/src/public-events/public-events.service.spec.ts index eedbe0f..7249706 100644 --- a/src/public-events/public-events.service.spec.ts +++ b/src/public-events/public-events.service.spec.ts @@ -1,6 +1,6 @@ import { TestBed } from '@/testing'; -import { lastValueFrom } from 'rxjs'; -import { filter } from 'rxjs/operators'; +import { ReplaySubject, firstValueFrom, timer } from 'rxjs'; +import { filter, share } from 'rxjs/operators'; import { vi } from 'vitest'; import { EventTypes } from './event-types'; import { PublicEventsService } from './public-events.service'; @@ -20,47 +20,63 @@ describe('Events Service', () => { }); it('registering to single event with one event emit works', async () => { - const firedEvent = await lastValueFrom(eventsService.registerForEvents()); + eventsService.fireEvent(EventTypes.ConfigLoaded, { myKey: 'myValue' }); + + const firedEvent = await firstValueFrom(eventsService.registerForEvents()); expect(firedEvent).toBeTruthy(); expect(firedEvent).toEqual({ type: EventTypes.ConfigLoaded, value: { myKey: 'myValue' }, }); - eventsService.fireEvent(EventTypes.ConfigLoaded, { myKey: 'myValue' }); }); it('registering to single event with multiple same event emit works', async () => { - const spy = vi.fn()('spy'); + const spy = vi.fn(); + + eventsService.registerForEvents().subscribe((firedEvent) => { + spy(firedEvent); + expect(firedEvent).toBeTruthy(); + }); - const firedEvent = await lastValueFrom(eventsService.registerForEvents()); - spy(firedEvent); - expect(firedEvent).toBeTruthy(); eventsService.fireEvent(EventTypes.ConfigLoaded, { myKey: 'myValue' }); eventsService.fireEvent(EventTypes.ConfigLoaded, { myKey: 'myValue2' }); - expect(spy.calls.count()).toBe(2); - expect(spy.calls.first().args[0]).toEqual({ + expect(spy.mock.calls.length).toBe(2); + expect(spy.mock.calls[0]?.[0]).toEqual({ type: EventTypes.ConfigLoaded, value: { myKey: 'myValue' }, }); - expect(spy.postSpy.mock.calls.at(-1)?.[0]).toEqual({ + expect(spy.mock.calls.at(-1)?.[0]).toEqual({ type: EventTypes.ConfigLoaded, value: { myKey: 'myValue2' }, }); + + await firstValueFrom(timer(0)); }); it('registering to single event with multiple emit works', async () => { - const firedEvent = await lastValueFrom( - eventsService - .registerForEvents() - .pipe(filter((x) => x.type === EventTypes.ConfigLoaded)) + const o$ = eventsService.registerForEvents().pipe( + filter((x) => x.type === EventTypes.ConfigLoaded), + share({ + connector: () => new ReplaySubject(1), + resetOnError: false, + resetOnComplete: false, + resetOnRefCountZero: true, + }) ); - expect(firedEvent).toBeTruthy(); - expect(firedEvent).toEqual({ - type: EventTypes.ConfigLoaded, - value: { myKey: 'myValue' }, + + o$.subscribe((firedEvent) => { + expect(firedEvent).toBeTruthy(); + expect(firedEvent).toEqual({ + type: EventTypes.ConfigLoaded, + value: { myKey: 'myValue' }, + }); + return firedEvent; }); + eventsService.fireEvent(EventTypes.ConfigLoaded, { myKey: 'myValue' }); eventsService.fireEvent(EventTypes.NewAuthenticationResult, true); + + await firstValueFrom(o$); }); }); diff --git a/src/storage/browser-storage.service.spec.ts b/src/storage/browser-storage.service.spec.ts index 9df218a..d88418f 100644 --- a/src/storage/browser-storage.service.spec.ts +++ b/src/storage/browser-storage.service.spec.ts @@ -13,6 +13,7 @@ describe('BrowserStorageService', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ + BrowserStorageService, mockProvider(LoggerService), { provide: AbstractSecurityStorage, diff --git a/src/storage/browser-storage.service.ts b/src/storage/browser-storage.service.ts index 3eac0e3..00ba029 100644 --- a/src/storage/browser-storage.service.ts +++ b/src/storage/browser-storage.service.ts @@ -1,5 +1,6 @@ -import { inject, Injectable } from 'injection-js'; +import { Injectable, inject } from 'injection-js'; import type { OpenIdConfiguration } from '../config/openid-configuration'; +import { injectAbstractType } from '../injection'; import { LoggerService } from '../logging/logger.service'; import { AbstractSecurityStorage } from './abstract-security-storage'; @@ -7,7 +8,9 @@ import { AbstractSecurityStorage } from './abstract-security-storage'; export class BrowserStorageService { private readonly loggerService = inject(LoggerService); - private readonly abstractSecurityStorage = inject(AbstractSecurityStorage); + private readonly abstractSecurityStorage = injectAbstractType( + AbstractSecurityStorage + ); read(key: string, configuration: OpenIdConfiguration): any { const { configId } = configuration; diff --git a/src/storage/default-localstorage.service.spec.ts b/src/storage/default-localstorage.service.spec.ts index 37f9766..082c1c2 100644 --- a/src/storage/default-localstorage.service.spec.ts +++ b/src/storage/default-localstorage.service.spec.ts @@ -18,7 +18,8 @@ describe('DefaultLocalStorageService', () => { describe('read', () => { it('should call localstorage.getItem', () => { - const spy = vi.spyOn(localStorage, 'getItem'); + // https://github.com/jsdom/jsdom/issues/2318 + const spy = vi.spyOn(Storage.prototype, 'getItem'); service.read('henlo'); @@ -28,7 +29,8 @@ describe('DefaultLocalStorageService', () => { describe('write', () => { it('should call localstorage.setItem', () => { - const spy = vi.spyOn(localStorage, 'setItem'); + // https://github.com/jsdom/jsdom/issues/2318 + const spy = vi.spyOn(Storage.prototype, 'setItem'); service.write('henlo', 'furiend'); @@ -38,7 +40,8 @@ describe('DefaultLocalStorageService', () => { describe('remove', () => { it('should call localstorage.removeItem', () => { - const spy = vi.spyOn(localStorage, 'removeItem'); + // https://github.com/jsdom/jsdom/issues/2318 + const spy = vi.spyOn(Storage.prototype, 'removeItem'); service.remove('henlo'); @@ -48,7 +51,8 @@ describe('DefaultLocalStorageService', () => { describe('clear', () => { it('should call localstorage.clear', () => { - const spy = vi.spyOn(localStorage, 'clear'); + // https://github.com/jsdom/jsdom/issues/2318 + const spy = vi.spyOn(Storage.prototype, 'clear'); service.clear(); diff --git a/src/storage/default-sessionstorage.service.spec.ts b/src/storage/default-sessionstorage.service.spec.ts index f2eabfd..5fb0c24 100644 --- a/src/storage/default-sessionstorage.service.spec.ts +++ b/src/storage/default-sessionstorage.service.spec.ts @@ -18,7 +18,8 @@ describe('DefaultSessionStorageService', () => { describe('read', () => { it('should call sessionstorage.getItem', () => { - const spy = vi.spyOn(sessionStorage, 'getItem'); + // https://github.com/jsdom/jsdom/issues/2318 + const spy = vi.spyOn(Storage.prototype, 'getItem'); service.read('henlo'); @@ -28,7 +29,8 @@ describe('DefaultSessionStorageService', () => { describe('write', () => { it('should call sessionstorage.setItem', () => { - const spy = vi.spyOn(sessionStorage, 'setItem'); + // https://github.com/jsdom/jsdom/issues/2318 + const spy = vi.spyOn(Storage.prototype, 'setItem'); service.write('henlo', 'furiend'); @@ -38,7 +40,8 @@ describe('DefaultSessionStorageService', () => { describe('remove', () => { it('should call sessionstorage.removeItem', () => { - const spy = vi.spyOn(sessionStorage, 'removeItem'); + // https://github.com/jsdom/jsdom/issues/2318 + const spy = vi.spyOn(Storage.prototype, 'removeItem'); service.remove('henlo'); @@ -48,7 +51,8 @@ describe('DefaultSessionStorageService', () => { describe('clear', () => { it('should call sessionstorage.clear', () => { - const spy = vi.spyOn(sessionStorage, 'clear'); + // https://github.com/jsdom/jsdom/issues/2318 + const spy = vi.spyOn(Storage.prototype, 'clear'); service.clear(); diff --git a/src/storage/storage-persistence.service.spec.ts b/src/storage/storage-persistence.service.spec.ts index 18ba47a..935f788 100644 --- a/src/storage/storage-persistence.service.spec.ts +++ b/src/storage/storage-persistence.service.spec.ts @@ -1,4 +1,4 @@ -import { TestBed } from '@/testing'; +import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { vi } from 'vitest'; import { mockProvider } from '../testing/mock'; import { BrowserStorageService } from './browser-storage.service'; @@ -10,7 +10,10 @@ describe('Storage Persistence Service', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [mockProvider(BrowserStorageService)], + providers: [ + StoragePersistenceService, + mockProvider(BrowserStorageService), + ], }); service = TestBed.inject(StoragePersistenceService); securityStorage = TestBed.inject(BrowserStorageService); @@ -239,10 +242,16 @@ describe('Storage Persistence Service', () => { }; const spy = vi.spyOn(securityStorage, 'read'); - spy - .withArgs('reusable_refresh_token', config) - .mockReturnValue(returnValue); - spy.withArgs('authnResult', config).mockReturnValue(undefined); + mockImplementationWhenArgsEqual( + spy, + ['reusable_refresh_token', config], + () => returnValue + ); + mockImplementationWhenArgsEqual( + spy, + ['authnResult', config], + () => undefined + ); const result = service.getRefreshToken(config); expect(result).toBe(returnValue.reusable_refresh_token); diff --git a/src/testing/create-retriable-stream.helper.ts b/src/testing/create-retriable-stream.helper.ts index ce0c86f..d7d761a 100644 --- a/src/testing/create-retriable-stream.helper.ts +++ b/src/testing/create-retriable-stream.helper.ts @@ -4,10 +4,12 @@ import { vi } from 'vitest'; // Create retriable observable stream to test retry / retryWhen. Credits to: // https://stackoverflow.com/questions/51399819/how-to-create-a-mock-observable-to-test-http-rxjs-retry-retrywhen-in-angular -export const createRetriableStream = (...resp$: any): Observable => { - const fetchData = vi.fn()('fetchData'); +export const createRetriableStream = (...resp$: any[]): Observable => { + const fetchData = vi.fn(); - fetchData.mockReturnValues(...resp$); + for (const r of resp$) { + fetchData.mockReturnValueOnce(r); + } return of(null).pipe(switchMap((_) => fetchData())); }; diff --git a/src/testing/init-test.ts b/src/testing/init-test.ts index 8de1e00..771d3f6 100644 --- a/src/testing/init-test.ts +++ b/src/testing/init-test.ts @@ -1,14 +1,11 @@ -import { getTestBed } from '@/testing/testbed'; -import { - BrowserDynamicTestingModule, - platformBrowserDynamicTesting, -} from '@angular/platform-browser-dynamic/testing'; +import { TestBed } from '@/testing/testbed'; +import { DOCUMENT } from 'oidc-client-rx/dom'; +import 'reflect-metadata'; // First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - BrowserDynamicTestingModule, - platformBrowserDynamicTesting(), +TestBed.initTestEnvironment([ { - teardown: { destroyAfterEach: false }, - } -); + provide: DOCUMENT, + useValue: document, + }, +]); diff --git a/src/testing/mock.ts b/src/testing/mock.ts index cc9f692..05f50d2 100644 --- a/src/testing/mock.ts +++ b/src/testing/mock.ts @@ -1,6 +1,10 @@ import type { Provider } from 'injection-js'; -export function mockClass(obj: new (...args: any[]) => T): any { +export function mockClass( + obj: new (...args: any[]) => T +): new ( + ...args: any[] +) => T { const keys = Object.getOwnPropertyNames(obj.prototype); const allMethods = keys.filter((key) => { try { @@ -14,9 +18,16 @@ export function mockClass(obj: new (...args: any[]) => T): any { const mockedClass = class T {}; for (const method of allMethods) { - (mockedClass.prototype as any)[method] = (): void => { - return; - }; + const mockImplementation = Reflect.getMetadata( + 'mock:implementation', + obj.prototype, + method + ); + (mockedClass.prototype as any)[method] = + mockImplementation ?? + ((): any => { + return; + }); } for (const method of allProperties) { @@ -28,12 +39,15 @@ export function mockClass(obj: new (...args: any[]) => T): any { }); } - return mockedClass; + return mockedClass as any; } -export function mockProvider(obj: new (...args: any[]) => T): Provider { +export function mockProvider( + obj: new (...args: any[]) => T, + token?: any +): Provider { return { - provide: obj, + provide: token ?? obj, useClass: mockClass(obj), }; } diff --git a/src/testing/router.ts b/src/testing/router.ts index e0eeddb..006526a 100644 --- a/src/testing/router.ts +++ b/src/testing/router.ts @@ -1,23 +1,31 @@ import type { Provider } from 'injection-js'; +import { JSDOM } from 'jsdom'; import { AbstractRouter, type Navigation, type UrlTree } from 'oidc-client-rx'; export class MockRouter extends AbstractRouter { + dom = new JSDOM('', { + url: 'http://localhost', + }); + navigation: Navigation = { id: 1, extras: {}, - initialUrl: new URL('https://localhost/'), - extractedUrl: new URL('https://localhost/'), + initialUrl: this.parseUrl(this.dom.window.location.href), + extractedUrl: this.parseUrl(this.dom.window.location.href), trigger: 'imperative', previousNavigation: null, }; navigateByUrl(url: string): void { const prevNavigation = this.navigation; + this.dom.reconfigure({ + url: new URL(url, this.dom.window.location.href).href, + }); this.navigation = { id: prevNavigation.id + 1, extras: {}, initialUrl: prevNavigation.initialUrl, - extractedUrl: new URL(url), + extractedUrl: this.parseUrl(this.dom.window.location.href), trigger: prevNavigation.trigger, previousNavigation: prevNavigation, }; @@ -26,7 +34,8 @@ export class MockRouter extends AbstractRouter { return this.navigation; } parseUrl(url: string): UrlTree { - return new URL(url); + const u = new URL(url, this.dom.window.location.href); + return `${u.pathname}${u.search}${u.hash}`; } } diff --git a/src/testing/spy.ts b/src/testing/spy.ts index afa3813..cb5ead8 100644 --- a/src/testing/spy.ts +++ b/src/testing/spy.ts @@ -35,20 +35,80 @@ export function mockImplementationWhenArgsEqual>( }); } -export function mockImplementationWhenArgs>( - mockInstance: M, - whenArgs: ( - ...args: Parameters ? T : never> - ) => boolean, - implementation: Exclude, undefined> -): M { - const spyImpl = mockInstance.getMockImplementation()!; +type Procedure = (...args: any[]) => any; +type Methods = keyof { + [K in keyof T as T[K] extends Procedure ? K : never]: T[K]; +}; +type Classes = { + [K in keyof T]: T[K] extends new (...args: any[]) => any ? K : never; +}[keyof T] & + (string | symbol); + +export type MockInstanceWithOrigin = MockInstance & { + getOriginImplementation?: () => any; +}; + +export function spyOnWithOrigin< + T, + M extends Classes> | Methods>, +>( + obj: T, + methodName: M +): Required[M] extends { + new (...args: infer A): infer R; +} + ? MockInstanceWithOrigin<(this: R, ...args: A) => R> + : T[M] extends Procedure + ? MockInstanceWithOrigin + : never { + let currentObj = obj; + let origin: + | (Required[M] extends { + new (...args: infer A): infer R; + } + ? (this: R, ...args: A) => R + : T[M] extends Procedure + ? T[M] + : never) + | undefined; + while (currentObj) { + origin = currentObj[methodName] as any; + if (origin) { + break; + } + currentObj = Object.getPrototypeOf(currentObj); + } + + const spy = vi.spyOn(obj, methodName as any) as Required[M] extends { + new (...args: infer A): infer R; + } + ? MockInstanceWithOrigin<(this: R, ...args: A) => R> + : T[M] extends Procedure + ? MockInstanceWithOrigin + : never; + + spy.getOriginImplementation = () => origin; + + return spy; +} + +export function mockImplementationWhenArgs( + mockInstance: MockInstance & { getOriginImplementation?: () => T }, + whenArgs: (...args: Parameters) => boolean, + implementation: T +): MockInstance { + const spyImpl = + mockInstance.getMockImplementation() ?? + mockInstance.getOriginImplementation?.(); return mockInstance.mockImplementation((...args) => { - if (isEqual(args, whenArgs)) { + if (whenArgs(...args)) { return implementation(...args); } - return spyImpl?.(...args); + if (spyImpl) { + return spyImpl(...args); + } + throw new Error('Mock implementation not defined for these arguments.'); }); } @@ -58,46 +118,37 @@ export function mockImplementationWhenArgs>( export function spyOnProperty( obj: T, propertyKey: K, - accessType: 'get' | 'set' = 'get', - mockImplementation?: any + accessType: 'get' | 'set' = 'get' ) { - const originalDescriptor = Object.getOwnPropertyDescriptor(obj, propertyKey); - - if (!originalDescriptor) { - throw new Error( - `Property ${String(propertyKey)} does not exist on the object.` - ); + const ownDescriptor = Object.getOwnPropertyDescriptor(obj, propertyKey); + let finalDescriptor: PropertyDescriptor | undefined; + let currentObj = obj; + while (currentObj) { + finalDescriptor = Object.getOwnPropertyDescriptor(currentObj, propertyKey); + if (finalDescriptor) { + break; + } + currentObj = Object.getPrototypeOf(currentObj); } const spy = vi.fn(); - let value: T[K] | undefined; - if (accessType === 'get') { Object.defineProperty(obj, propertyKey, { - get: mockImplementation - ? () => { - value = mockImplementation(); - return value; - } - : spy, + get: spy, configurable: true, }); } else if (accessType === 'set') { Object.defineProperty(obj, propertyKey, { - set: mockImplementation - ? (next) => { - value = next; - } - : spy, + set: spy, configurable: true, }); } // 恢复原始属性 spy.mockRestore = () => { - if (originalDescriptor) { - Object.defineProperty(obj, propertyKey, originalDescriptor); + if (ownDescriptor) { + Object.defineProperty(obj, propertyKey, ownDescriptor); } else { delete obj[propertyKey]; } diff --git a/src/testing/testbed.ts b/src/testing/testbed.ts index 1339522..e8cbd42 100644 --- a/src/testing/testbed.ts +++ b/src/testing/testbed.ts @@ -13,21 +13,33 @@ export interface TestModuleMetadata { } export class TestBed { + static environmentInjector?: Injector; private injector: ReflectiveInjector; private providers: Provider[] = []; private imports: Injector[] = []; - constructor(metadata: TestModuleMetadata = {}) { + constructor( + metadata: TestModuleMetadata = {}, + environmentInjector?: Injector + ) { const providers = metadata.providers ?? []; const imports = metadata.imports ?? []; - this.injector = ReflectiveInjector.resolveAndCreate(providers); + this.injector = ReflectiveInjector.resolveAndCreate( + providers, + environmentInjector + ); this.imports = imports.map((importFn) => importFn(this.injector)); } static #instance?: TestBed; + static initTestEnvironment(providers: Provider[] = []) { + TestBed.environmentInjector = + ReflectiveInjector.resolveAndCreate(providers); + } + static configureTestingModule(metadata: TestModuleMetadata = {}) { - const newTestBed = new TestBed(metadata); + const newTestBed = new TestBed(metadata, TestBed.environmentInjector); TestBed.#instance = newTestBed; return newTestBed; diff --git a/src/user-data/user-service.spec.ts b/src/user-data/user-service.spec.ts index 22860e7..bda1fd5 100644 --- a/src/user-data/user-service.spec.ts +++ b/src/user-data/user-service.spec.ts @@ -1,5 +1,5 @@ import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; -import { Observable, lastValueFrom, of, throwError } from 'rxjs'; +import { Observable, firstValueFrom, of, throwError } from 'rxjs'; import { vi } from 'vitest'; import { DataService } from '../api/data.service'; import type { OpenIdConfiguration } from '../config/openid-configuration'; @@ -30,6 +30,7 @@ describe('User Service', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ + UserService, mockProvider(StoragePersistenceService), mockProvider(LoggerService), mockProvider(DataService), @@ -70,7 +71,7 @@ describe('User Service', () => { userDataInstore ); - const token = await lastValueFrom( + const token = await firstValueFrom( userService.getAndPersistUserDataInStore( config, [config], @@ -99,7 +100,7 @@ describe('User Service', () => { ); vi.spyOn(userService, 'setUserDataToStore'); - const token = await lastValueFrom( + const token = await firstValueFrom( userService.getAndPersistUserDataInStore( config, [config], @@ -129,7 +130,7 @@ describe('User Service', () => { userDataInstore ); - const token = await lastValueFrom( + const token = await firstValueFrom( userService.getAndPersistUserDataInStore( config, [config], @@ -161,7 +162,7 @@ describe('User Service', () => { .spyOn(userService as any, 'getIdentityUserData') .mockReturnValue(of(userDataFromSts)); - const token = await lastValueFrom( + const token = await firstValueFrom( userService.getAndPersistUserDataInStore( config, [config], @@ -202,7 +203,7 @@ describe('User Service', () => { 'accessToken' ); - const token = await lastValueFrom( + const token = await firstValueFrom( userService.getAndPersistUserDataInStore( config, [config], @@ -246,7 +247,7 @@ describe('User Service', () => { ); try { - await lastValueFrom( + await firstValueFrom( userService.getAndPersistUserDataInStore( config, [config], @@ -283,7 +284,7 @@ describe('User Service', () => { .spyOn(userService as any, 'getIdentityUserData') .mockReturnValue(of(userDataFromSts)); - const token = await lastValueFrom( + const token = await firstValueFrom( userService.getAndPersistUserDataInStore( config, [config], @@ -539,7 +540,7 @@ describe('User Service', () => { () => null ); try { - await lastValueFrom(serviceAsAny.getIdentityUserData(config)); + await firstValueFrom(serviceAsAny.getIdentityUserData(config)); } catch (err: any) { expect(err).toBeTruthy(); } @@ -560,7 +561,7 @@ describe('User Service', () => { ); try { - await lastValueFrom(serviceAsAny.getIdentityUserData(config)); + await firstValueFrom(serviceAsAny.getIdentityUserData(config)); } catch (err: any) { expect(err).toBeTruthy(); } @@ -580,7 +581,7 @@ describe('User Service', () => { () => ({ userInfoEndpoint: 'userInfoEndpoint' }) ); - await lastValueFrom(serviceAsAny.getIdentityUserData(config)); + await firstValueFrom(serviceAsAny.getIdentityUserData(config)); expect(spy).toHaveBeenCalledExactlyOnceWith( 'userInfoEndpoint', config, @@ -607,7 +608,7 @@ describe('User Service', () => { ) ); - const res = await lastValueFrom( + const res = await firstValueFrom( (userService as any).getIdentityUserData(config) ); @@ -634,7 +635,7 @@ describe('User Service', () => { ) ); - const res = await lastValueFrom( + const res = await firstValueFrom( (userService as any).getIdentityUserData(config) ); expect(res).toBeTruthy(); @@ -662,7 +663,7 @@ describe('User Service', () => { ); try { - await lastValueFrom((userService as any).getIdentityUserData(config)); + await firstValueFrom((userService as any).getIdentityUserData(config)); } catch (err: any) { expect(err).toBeTruthy(); } diff --git a/src/utils/reflect/index.ts b/src/utils/reflect/index.ts new file mode 100644 index 0000000..cd35cda --- /dev/null +++ b/src/utils/reflect/index.ts @@ -0,0 +1,17 @@ +/// + +// biome-ignore lint/complexity/noBannedTypes: +export function MockUtil(options: { implementation: F }) { + return ( + targetClass: any, + propertyKey: string, + _descriptor?: TypedPropertyDescriptor<(...args: any[]) => any> + ): void => { + Reflect?.defineMetadata?.( + 'mock:implementation', + options.implementation, + targetClass, + propertyKey + ); + }; +} diff --git a/src/utils/tokenHelper/token-helper.service.spec.ts b/src/utils/tokenHelper/token-helper.service.spec.ts index b27ba45..c89a492 100644 --- a/src/utils/tokenHelper/token-helper.service.spec.ts +++ b/src/utils/tokenHelper/token-helper.service.spec.ts @@ -8,7 +8,7 @@ describe('Token Helper Service', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [mockProvider(LoggerService)], + providers: [TokenHelperService, mockProvider(LoggerService)], }); tokenHelperService = TestBed.inject(TokenHelperService); }); diff --git a/src/utils/url/current-url.service.spec.ts b/src/utils/url/current-url.service.spec.ts index 3a9d21b..54198b2 100644 --- a/src/utils/url/current-url.service.spec.ts +++ b/src/utils/url/current-url.service.spec.ts @@ -13,6 +13,7 @@ describe('CurrentUrlService with existing Url', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ + CurrentUrlService, { provide: DOCUMENT, useValue: documentValue, diff --git a/src/utils/url/url.service.spec.ts b/src/utils/url/url.service.spec.ts index 6d3a3af..1e1a5de 100644 --- a/src/utils/url/url.service.spec.ts +++ b/src/utils/url/url.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; -import { lastValueFrom, of } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { vi } from 'vitest'; import type { OpenIdConfiguration } from '../../config/openid-configuration'; import { FlowsDataService } from '../../flows/flows-data.service'; @@ -1041,14 +1041,14 @@ describe('UrlService Tests', () => { describe('getAuthorizeUrl', () => { it('returns null if no config is given', async () => { - const url = await lastValueFrom(service.getAuthorizeUrl(null)); + const url = await firstValueFrom(service.getAuthorizeUrl(null)); expect(url).toBeNull(); }); it('returns null if current flow is code flow and no redirect url is defined', async () => { vi.spyOn(flowHelper, 'isCurrentFlowCodeFlow').mockReturnValue(true); - const result = await lastValueFrom( + const result = await firstValueFrom( service.getAuthorizeUrl({ configId: 'configId1' }) ); expect(result).toBeNull(); @@ -1062,7 +1062,7 @@ describe('UrlService Tests', () => { redirectUrl: 'some-redirectUrl', } as OpenIdConfiguration; - const result = await lastValueFrom(service.getAuthorizeUrl(config)); + const result = await firstValueFrom(service.getAuthorizeUrl(config)); expect(result).toBe(''); }); @@ -1090,7 +1090,7 @@ describe('UrlService Tests', () => { () => ({ authorizationEndpoint }) ); - const result = await lastValueFrom(service.getAuthorizeUrl(config)); + const result = await firstValueFrom(service.getAuthorizeUrl(config)); expect(result).toBe( 'authorizationEndpoint?client_id=some-clientId&redirect_uri=some-redirectUrl&response_type=testResponseType&scope=testScope&nonce=undefined&state=undefined&code_challenge=some-code-challenge&code_challenge_method=S256' ); @@ -1107,7 +1107,7 @@ describe('UrlService Tests', () => { 'createUrlImplicitFlowAuthorize' ); - await lastValueFrom(service.getAuthorizeUrl({ configId: 'configId1' })); + await firstValueFrom(service.getAuthorizeUrl({ configId: 'configId1' })); expect(spyCreateUrlCodeFlowAuthorize).not.toHaveBeenCalled(); expect(spyCreateUrlImplicitFlowAuthorize).toHaveBeenCalled(); }); @@ -1119,18 +1119,20 @@ describe('UrlService Tests', () => { .mockReturnValue(''); const resultObs$ = service.getAuthorizeUrl({ configId: 'configId1' }); - const result = await lastValueFrom(resultObs$); + const result = await firstValueFrom(resultObs$); expect(spy).toHaveBeenCalled(); expect(result).toBe(''); }); }); describe('getRefreshSessionSilentRenewUrl', () => { - it('calls createUrlCodeFlowWithSilentRenew if current flow is code flow', () => { + it('calls createUrlCodeFlowWithSilentRenew if current flow is code flow', async () => { vi.spyOn(flowHelper, 'isCurrentFlowCodeFlow').mockReturnValue(true); const spy = vi.spyOn(service as any, 'createUrlCodeFlowWithSilentRenew'); - service.getRefreshSessionSilentRenewUrl({ configId: 'configId1' }); + await firstValueFrom( + service.getRefreshSessionSilentRenewUrl({ configId: 'configId1' }) + ); expect(spy).toHaveBeenCalled(); }); @@ -1159,7 +1161,7 @@ describe('UrlService Tests', () => { configId: 'configId1', }); - const result = await lastValueFrom(resultObs$); + const result = await firstValueFrom(resultObs$); expect(spy).toHaveBeenCalled(); expect(result).toBe(''); }); @@ -1344,7 +1346,7 @@ describe('UrlService Tests', () => { redirectUrl: '', }); - const result = await lastValueFrom(resultObs$); + const result = await firstValueFrom(resultObs$); expect(result).toBe(null); }); @@ -1372,7 +1374,7 @@ describe('UrlService Tests', () => { const resultObs$ = service.createBodyForParCodeFlowRequest(config); - const result = await lastValueFrom(resultObs$); + const result = await firstValueFrom(resultObs$); expect(result).toBe( 'client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256' ); @@ -1402,7 +1404,7 @@ describe('UrlService Tests', () => { const resultObs$ = service.createBodyForParCodeFlowRequest(config); - const result = await lastValueFrom(resultObs$); + const result = await firstValueFrom(resultObs$); expect(result).toBe( 'client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256&hd=testHdParam' ); @@ -1432,7 +1434,7 @@ describe('UrlService Tests', () => { const resultObs$ = service.createBodyForParCodeFlowRequest(config); - const result = await lastValueFrom(resultObs$); + const result = await firstValueFrom(resultObs$); expect(result).toBe( 'client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256&hd=testHdParam&any=thing' ); @@ -1466,7 +1468,7 @@ describe('UrlService Tests', () => { }, }); - const result = await lastValueFrom(resultObs$); + const result = await firstValueFrom(resultObs$); expect(result).toBe( 'client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256&hd=testHdParam&any=thing&any=otherThing' ); @@ -1596,7 +1598,7 @@ describe('UrlService Tests', () => { const resultObs$ = serviceAsAny.createUrlCodeFlowWithSilentRenew(config); - const result = await lastValueFrom(resultObs$); + const result = await firstValueFrom(resultObs$); expect(result).toBe(''); }); @@ -1639,7 +1641,7 @@ describe('UrlService Tests', () => { const resultObs$ = serviceAsAny.createUrlCodeFlowWithSilentRenew(config); - const result = await lastValueFrom(resultObs$); + const result = await firstValueFrom(resultObs$); expect(result).toBe( `authorizationEndpoint?client_id=${clientId}&redirect_uri=http%3A%2F%2Fany-url.com&response_type=${responseType}&scope=${scope}&nonce=${nonce}&state=${state}&prompt=none` ); @@ -1680,7 +1682,7 @@ describe('UrlService Tests', () => { const resultObs$ = serviceAsAny.createUrlCodeFlowWithSilentRenew(config); - const result = await lastValueFrom(resultObs$); + const result = await firstValueFrom(resultObs$); expect(result).toBe(''); }); }); @@ -1796,7 +1798,7 @@ describe('UrlService Tests', () => { const resultObs$ = serviceAsAny.createUrlCodeFlowAuthorize(config); - const result = await lastValueFrom(resultObs$); + const result = await firstValueFrom(resultObs$); expect(result).toBeNull(); }); @@ -1838,7 +1840,7 @@ describe('UrlService Tests', () => { const resultObs$ = serviceAsAny.createUrlCodeFlowAuthorize(config); - const result = await lastValueFrom(resultObs$); + const result = await firstValueFrom(resultObs$); expect(result).toBe( `authorizationEndpoint?client_id=clientId&redirect_uri=http%3A%2F%2Fany-url.com&response_type=${responseType}&scope=${scope}&nonce=${nonce}&state=${state}` ); @@ -1887,7 +1889,7 @@ describe('UrlService Tests', () => { customParams: { to: 'add', as: 'well' }, }); - const result = await lastValueFrom(resultObs$); + const result = await firstValueFrom(resultObs$); expect(result).toBe( `authorizationEndpoint?client_id=clientId&redirect_uri=http%3A%2F%2Fany-url.com&response_type=${responseType}&scope=${scope}&nonce=${nonce}&state=${state}&to=add&as=well` ); @@ -1924,7 +1926,7 @@ describe('UrlService Tests', () => { const resultObs$ = serviceAsAny.createUrlCodeFlowAuthorize(config); - const result = await lastValueFrom(resultObs$); + const result = await firstValueFrom(resultObs$); expect(result).toBe(''); }); }); diff --git a/src/utils/url/url.service.ts b/src/utils/url/url.service.ts index 9c43bc5..0272679 100644 --- a/src/utils/url/url.service.ts +++ b/src/utils/url/url.service.ts @@ -1,10 +1,10 @@ -import { HttpParams } from '@ngify/http'; import { Injectable, inject } from 'injection-js'; import { type Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; import type { AuthOptions } from '../../auth-options'; import type { OpenIdConfiguration } from '../../config/openid-configuration'; import { FlowsDataService } from '../../flows/flows-data.service'; +import { HttpParams } from '../../http'; import { LoggerService } from '../../logging/logger.service'; import { StoragePersistenceService } from '../../storage/storage-persistence.service'; import { JwtWindowCryptoService } from '../../validation/jwt-window-crypto.service'; @@ -873,8 +873,8 @@ export class UrlService { private createHttpParams(existingParams?: string): HttpParams { existingParams = existingParams ?? ''; - // @TODO @ngify/http - return new HttpParams(existingParams || undefined, { + return new HttpParams({ + fromString: existingParams, encoder: new UriEncoder(), }); } diff --git a/src/validation/jwt-window-crypto.service.spec.ts b/src/validation/jwt-window-crypto.service.spec.ts index 1654867..128d0e3 100644 --- a/src/validation/jwt-window-crypto.service.spec.ts +++ b/src/validation/jwt-window-crypto.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@/testing'; -import { lastValueFrom } from 'rxjs'; +import { firstValueFrom } from 'rxjs'; import { CryptoService } from '../utils/crypto/crypto.service'; import { JwtWindowCryptoService } from './jwt-window-crypto.service'; @@ -25,7 +25,7 @@ describe('JwtWindowCryptoService', () => { '44445543344242132145455aaabbdc3b4' ); - const value = await lastValueFrom(observable); + const value = await firstValueFrom(observable); expect(value).toBe(outcome); }); }); diff --git a/src/validation/jwt-window-crypto.service.ts b/src/validation/jwt-window-crypto.service.ts index 4373d6f..5f97f19 100644 --- a/src/validation/jwt-window-crypto.service.ts +++ b/src/validation/jwt-window-crypto.service.ts @@ -1,12 +1,14 @@ -import { inject, Injectable } from 'injection-js'; -import { from, type Observable } from 'rxjs'; +import { Injectable, inject } from 'injection-js'; +import { BehaviorSubject, type Observable, from } from 'rxjs'; import { map } from 'rxjs/operators'; import { CryptoService } from '../utils/crypto/crypto.service'; +import { MockUtil } from '../utils/reflect'; @Injectable() export class JwtWindowCryptoService { private readonly cryptoService = inject(CryptoService); + @MockUtil({ implementation: () => new BehaviorSubject(undefined) }) generateCodeChallenge(codeVerifier: string): Observable { return this.calcHash(codeVerifier).pipe( map((challengeRaw: string) => this.base64UrlEncode(challengeRaw)) diff --git a/src/validation/state-validation-result.ts b/src/validation/state-validation-result.ts index 6146fee..224cb39 100644 --- a/src/validation/state-validation-result.ts +++ b/src/validation/state-validation-result.ts @@ -2,22 +2,12 @@ import { ValidationResult } from './validation-result'; export class StateValidationResult { constructor( - // biome-ignore lint/style/noParameterProperties: - // biome-ignore lint/nursery/useConsistentMemberAccessibility: public accessToken = '', - // biome-ignore lint/style/noParameterProperties: - // biome-ignore lint/nursery/useConsistentMemberAccessibility: public idToken = '', - // biome-ignore lint/style/noParameterProperties: - // biome-ignore lint/nursery/useConsistentMemberAccessibility: public authResponseIsValid = false, - // biome-ignore lint/style/noParameterProperties: - // biome-ignore lint/nursery/useConsistentMemberAccessibility: public decodedIdToken: any = { at_hash: '', }, - // biome-ignore lint/style/noParameterProperties: - // biome-ignore lint/nursery/useConsistentMemberAccessibility: public state: ValidationResult = ValidationResult.NotSet ) {} } diff --git a/src/validation/state-validation.service.spec.ts b/src/validation/state-validation.service.spec.ts index 34f6faf..46cba8f 100644 --- a/src/validation/state-validation.service.spec.ts +++ b/src/validation/state-validation.service.spec.ts @@ -1,5 +1,9 @@ import { TestBed } from '@/testing'; -import { lastValueFrom, of } from 'rxjs'; +import { + mockImplementationWhenArgs, + mockImplementationWhenArgsEqual, +} from '@/testing/spy'; +import { firstValueFrom, of } from 'rxjs'; import { vi } from 'vitest'; import type { AuthWellKnownEndpoints } from '../config/auth-well-known/auth-well-known-endpoints'; import type { OpenIdConfiguration } from '../config/openid-configuration'; @@ -28,6 +32,7 @@ describe('State Validation Service', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ + StateValidationService, mockProvider(StoragePersistenceService), mockProvider(TokenValidationService), mockProvider(LoggerService), @@ -687,8 +692,8 @@ describe('State Validation Service', () => { config ); - const isValid = await lastValueFrom(isValidObs$); - expect(isValid.authResponseIsValid).toBe(false); + const isValid = await firstValueFrom(isValidObs$); + expect(isValid.authResponseIsValid).toBeFalsy(); }); it('should return invalid context error', async () => { @@ -723,7 +728,7 @@ describe('State Validation Service', () => { config ); - const isValid = await lastValueFrom(isValidObs$); + const isValid = await firstValueFrom(isValidObs$); expect(isValid.authResponseIsValid).toBe(false); }); @@ -787,13 +792,23 @@ describe('State Validation Service', () => { ).mockReturnValue(false); const readSpy = vi.spyOn(storagePersistenceService, 'read'); - readSpy - .withArgs('authWellKnownEndPoints', config) - .mockReturnValue(authWellKnownEndpoints); - readSpy - .withArgs('authStateControl', config) - .mockReturnValue('authStateControl'); - readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); + mockImplementationWhenArgsEqual( + readSpy, + ['authWellKnownEndPoints', config], + () => authWellKnownEndpoints + ); + + mockImplementationWhenArgsEqual( + readSpy, + ['authStateControl', config], + () => 'authStateControl' + ); + + mockImplementationWhenArgsEqual( + readSpy, + ['authNonce', config], + () => 'authNonce' + ); const logWarningSpy = vi .spyOn(loggerService, 'logWarning') @@ -818,7 +833,7 @@ describe('State Validation Service', () => { config ); - const state = await lastValueFrom(stateObs$); + const state = await firstValueFrom(stateObs$); expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( config, 'authCallback id token expired' @@ -832,12 +847,18 @@ describe('State Validation Service', () => { it('should return invalid result if validateStateFromHashCallback is false', async () => { const readSpy = vi.spyOn(storagePersistenceService, 'read'); - readSpy - .withArgs('authWellKnownEndPoints', config) - .mockReturnValue(authWellKnownEndpoints); - readSpy - .withArgs('authStateControl', config) - .mockReturnValue('authStateControl'); + mockImplementationWhenArgsEqual( + readSpy, + ['authWellKnownEndPoints', config], + () => authWellKnownEndpoints + ); + + mockImplementationWhenArgsEqual( + readSpy, + ['authStateControl', config], + () => 'authStateControl' + ); + vi.spyOn( tokenValidationService, 'validateStateFromHashCallback' @@ -870,7 +891,7 @@ describe('State Validation Service', () => { tokenValidationService.validateStateFromHashCallback ).toHaveBeenCalled(); - const state = await lastValueFrom(stateObs$); + const state = await firstValueFrom(stateObs$); expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( config, 'authCallback incorrect state' @@ -940,13 +961,23 @@ describe('State Validation Service', () => { const readSpy = vi.spyOn(storagePersistenceService, 'read'); - readSpy - .withArgs('authWellKnownEndPoints', config) - .mockReturnValue(authWellKnownEndpoints); - readSpy - .withArgs('authStateControl', config) - .mockReturnValue('authStateControl'); - readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); + mockImplementationWhenArgsEqual( + readSpy, + ['authWellKnownEndPoints', config], + () => authWellKnownEndpoints + ); + + mockImplementationWhenArgsEqual( + readSpy, + ['authStateControl', config], + () => 'authStateControl' + ); + + mockImplementationWhenArgsEqual( + readSpy, + ['authNonce', config], + () => 'authNonce' + ); const callbackContext = { code: 'fdffsdfsdf', @@ -967,7 +998,7 @@ describe('State Validation Service', () => { config ); - const state = await lastValueFrom(stateObs$); + const state = await firstValueFrom(stateObs$); expect(state.accessToken).toBe('access_tokenTEST'); expect(state.idToken).toBe('id_tokenTEST'); expect(state.decodedIdToken).toBe('decoded_id_token'); @@ -990,12 +1021,16 @@ describe('State Validation Service', () => { const readSpy = vi.spyOn(storagePersistenceService, 'read'); - readSpy - .withArgs('authWellKnownEndPoints', config) - .mockReturnValue(authWellKnownEndpoints); - readSpy - .withArgs('authStateControl', config) - .mockReturnValue('authStateControl'); + mockImplementationWhenArgsEqual( + readSpy, + ['authWellKnownEndPoints', config], + () => authWellKnownEndpoints + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authStateControl', config], + () => 'authStateControl' + ); const logDebugSpy = vi .spyOn(loggerService, 'logDebug') .mockImplementation(() => undefined); @@ -1020,8 +1055,8 @@ describe('State Validation Service', () => { config ); - const state = await lastValueFrom(stateObs$); - expect(logDebugSpy).toBeCalledWith([ + const state = await firstValueFrom(stateObs$); + expect(logDebugSpy.mock.calls).toEqual([ [config, 'authCallback Signature validation failed id_token'], [config, 'authCallback token(s) invalid'], ]); @@ -1049,13 +1084,21 @@ describe('State Validation Service', () => { ); const readSpy = vi.spyOn(storagePersistenceService, 'read'); - readSpy - .withArgs('authWellKnownEndPoints', config) - .mockReturnValue(authWellKnownEndpoints); - readSpy - .withArgs('authStateControl', config) - .mockReturnValue('authStateControl'); - readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); + mockImplementationWhenArgsEqual( + readSpy, + ['authWellKnownEndPoints', config], + () => authWellKnownEndpoints + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authStateControl', config], + () => 'authStateControl' + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authNonce', config], + () => 'authNonce' + ); const logWarningSpy = vi .spyOn(loggerService, 'logWarning') @@ -1080,7 +1123,7 @@ describe('State Validation Service', () => { config ); - const state = await lastValueFrom(stateObs$); + const state = await firstValueFrom(stateObs$); expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( config, 'authCallback incorrect nonce, did you call the checkAuth() method multiple times?' @@ -1118,13 +1161,21 @@ describe('State Validation Service', () => { ).mockReturnValue(false); const readSpy = vi.spyOn(storagePersistenceService, 'read'); - readSpy - .withArgs('authWellKnownEndPoints', config) - .mockReturnValue(authWellKnownEndpoints); - readSpy - .withArgs('authStateControl', config) - .mockReturnValue('authStateControl'); - readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); + mockImplementationWhenArgsEqual( + readSpy, + ['authWellKnownEndPoints', config], + () => authWellKnownEndpoints + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authStateControl', config], + () => 'authStateControl' + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authNonce', config], + () => 'authNonce' + ); const logDebugSpy = vi .spyOn(loggerService, 'logDebug') .mockImplementation(() => undefined); @@ -1148,7 +1199,7 @@ describe('State Validation Service', () => { config ); - const state = await lastValueFrom(stateObs$); + const state = await firstValueFrom(stateObs$); expect(logDebugSpy).toHaveBeenCalledWith( config, 'authCallback Validation, one of the REQUIRED properties missing from id_token' @@ -1193,13 +1244,21 @@ describe('State Validation Service', () => { config.maxIdTokenIatOffsetAllowedInSeconds = 0; const readSpy = vi.spyOn(storagePersistenceService, 'read'); - readSpy - .withArgs('authWellKnownEndPoints', config) - .mockReturnValue(authWellKnownEndpoints); - readSpy - .withArgs('authStateControl', config) - .mockReturnValue('authStateControl'); - readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); + mockImplementationWhenArgsEqual( + readSpy, + ['authWellKnownEndPoints', config], + () => authWellKnownEndpoints + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authStateControl', config], + () => 'authStateControl' + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authNonce', config], + () => 'authNonce' + ); const logWarningSpy = vi .spyOn(loggerService, 'logWarning') .mockImplementation(() => undefined); @@ -1223,7 +1282,7 @@ describe('State Validation Service', () => { config ); - const state = await lastValueFrom(stateObs$); + const state = await firstValueFrom(stateObs$); expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( config, 'authCallback Validation, iat rejected id_token was issued too far away from the current time' @@ -1271,13 +1330,21 @@ describe('State Validation Service', () => { ); const readSpy = vi.spyOn(storagePersistenceService, 'read'); - readSpy - .withArgs('authWellKnownEndPoints', config) - .mockReturnValue(authWellKnownEndpoints); - readSpy - .withArgs('authStateControl', config) - .mockReturnValue('authStateControl'); - readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); + mockImplementationWhenArgsEqual( + readSpy, + ['authWellKnownEndPoints', config], + () => authWellKnownEndpoints + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authStateControl', config], + () => 'authStateControl' + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authNonce', config], + () => 'authNonce' + ); const logWarningSpy = vi .spyOn(loggerService, 'logWarning') .mockImplementation(() => undefined); @@ -1301,7 +1368,7 @@ describe('State Validation Service', () => { config ); - const state = await lastValueFrom(stateObs$); + const state = await firstValueFrom(stateObs$); expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( config, 'authCallback incorrect iss does not match authWellKnownEndpoints issuer' @@ -1339,11 +1406,21 @@ describe('State Validation Service', () => { config.maxIdTokenIatOffsetAllowedInSeconds = 0; const readSpy = vi.spyOn(storagePersistenceService, 'read'); - readSpy.withArgs('authWellKnownEndPoints', config).mockReturnValue(null); - readSpy - .withArgs('authStateControl', config) - .mockReturnValue('authStateControl'); - readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); + mockImplementationWhenArgsEqual( + readSpy, + ['authWellKnownEndPoints', config], + () => null + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authStateControl', config], + () => 'authStateControl' + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authNonce', config], + () => 'authNonce' + ); const logWarningSpy = vi .spyOn(loggerService, 'logWarning') .mockImplementation(() => undefined); @@ -1367,7 +1444,7 @@ describe('State Validation Service', () => { config ); - const state = await lastValueFrom(stateObs$); + const state = await firstValueFrom(stateObs$); expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( config, 'authWellKnownEndpoints is undefined' @@ -1414,13 +1491,21 @@ describe('State Validation Service', () => { config.clientId = ''; const readSpy = vi.spyOn(storagePersistenceService, 'read'); - readSpy - .withArgs('authWellKnownEndPoints', config) - .mockReturnValue(authWellKnownEndpoints); - readSpy - .withArgs('authStateControl', config) - .mockReturnValue('authStateControl'); - readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); + mockImplementationWhenArgsEqual( + readSpy, + ['authWellKnownEndPoints', config], + () => authWellKnownEndpoints + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authStateControl', config], + () => 'authStateControl' + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authNonce', config], + () => 'authNonce' + ); const logWarningSpy = vi .spyOn(loggerService, 'logWarning') .mockImplementation(() => undefined); @@ -1444,7 +1529,7 @@ describe('State Validation Service', () => { config ); - const state = await lastValueFrom(stateObs$); + const state = await firstValueFrom(stateObs$); expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( config, 'authCallback incorrect aud' @@ -1494,13 +1579,21 @@ describe('State Validation Service', () => { config.clientId = ''; const readSpy = vi.spyOn(storagePersistenceService, 'read'); - readSpy - .withArgs('authWellKnownEndPoints', config) - .mockReturnValue(authWellKnownEndpoints); - readSpy - .withArgs('authStateControl', config) - .mockReturnValue('authStateControl'); - readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); + mockImplementationWhenArgsEqual( + readSpy, + ['authWellKnownEndPoints', config], + () => authWellKnownEndpoints + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authStateControl', config], + () => 'authStateControl' + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authNonce', config], + () => 'authNonce' + ); const logWarningSpy = vi .spyOn(loggerService, 'logWarning') .mockImplementation(() => undefined); @@ -1524,7 +1617,7 @@ describe('State Validation Service', () => { config ); - const state = await lastValueFrom(stateObs$); + const state = await firstValueFrom(stateObs$); expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( config, 'authCallback missing azp' @@ -1579,13 +1672,21 @@ describe('State Validation Service', () => { config.clientId = ''; const readSpy = vi.spyOn(storagePersistenceService, 'read'); - readSpy - .withArgs('authWellKnownEndPoints', config) - .mockReturnValue(authWellKnownEndpoints); - readSpy - .withArgs('authStateControl', config) - .mockReturnValue('authStateControl'); - readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); + mockImplementationWhenArgsEqual( + readSpy, + ['authWellKnownEndPoints', config], + () => authWellKnownEndpoints + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authStateControl', config], + () => 'authStateControl' + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authNonce', config], + () => 'authNonce' + ); const logWarningSpy = vi .spyOn(loggerService, 'logWarning') .mockImplementation(() => undefined); @@ -1609,7 +1710,7 @@ describe('State Validation Service', () => { config ); - const state = await lastValueFrom(stateObs$); + const state = await firstValueFrom(stateObs$); expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( config, 'authCallback incorrect azp' @@ -1668,13 +1769,21 @@ describe('State Validation Service', () => { config.clientId = ''; const readSpy = vi.spyOn(storagePersistenceService, 'read'); - readSpy - .withArgs('authWellKnownEndPoints', config) - .mockReturnValue(authWellKnownEndpoints); - readSpy - .withArgs('authStateControl', config) - .mockReturnValue('authStateControl'); - readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); + mockImplementationWhenArgsEqual( + readSpy, + ['authWellKnownEndPoints', config], + () => authWellKnownEndpoints + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authStateControl', config], + () => 'authStateControl' + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authNonce', config], + () => 'authNonce' + ); const logWarningSpy = vi .spyOn(loggerService, 'logWarning') .mockImplementation(() => undefined); @@ -1698,7 +1807,7 @@ describe('State Validation Service', () => { config ); - const state = await lastValueFrom(stateObs$); + const state = await firstValueFrom(stateObs$); expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( config, 'authCallback pre, post id_token claims do not match in refresh' @@ -1769,13 +1878,21 @@ describe('State Validation Service', () => { config.autoCleanStateAfterAuthentication = false; const readSpy = vi.spyOn(storagePersistenceService, 'read'); - readSpy - .withArgs('authWellKnownEndPoints', config) - .mockReturnValue(authWellKnownEndpoints); - readSpy - .withArgs('authStateControl', config) - .mockReturnValue('authStateControl'); - readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); + mockImplementationWhenArgsEqual( + readSpy, + ['authWellKnownEndPoints', config], + () => authWellKnownEndpoints + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authStateControl', config], + () => 'authStateControl' + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authNonce', config], + () => 'authNonce' + ); const logDebugSpy = vi .spyOn(loggerService, 'logDebug') @@ -1801,7 +1918,7 @@ describe('State Validation Service', () => { config ); - const state = await lastValueFrom(stateObs$); + const state = await firstValueFrom(stateObs$); expect(logDebugSpy).toHaveBeenCalledWith( config, 'authCallback token(s) validated, continue' @@ -1875,13 +1992,21 @@ describe('State Validation Service', () => { const readSpy = vi.spyOn(storagePersistenceService, 'read'); - readSpy - .withArgs('authWellKnownEndPoints', config) - .mockReturnValue(authWellKnownEndpoints); - readSpy - .withArgs('authStateControl', config) - .mockReturnValue('authStateControl'); - readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); + mockImplementationWhenArgsEqual( + readSpy, + ['authWellKnownEndPoints', config], + () => authWellKnownEndpoints + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authStateControl', config], + () => 'authStateControl' + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authNonce', config], + () => 'authNonce' + ); const logWarningSpy = vi .spyOn(loggerService, 'logWarning') @@ -1906,7 +2031,7 @@ describe('State Validation Service', () => { config ); - const state = await lastValueFrom(stateObs$); + const state = await firstValueFrom(stateObs$); expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( config, 'authCallback incorrect at_hash' @@ -1974,13 +2099,21 @@ describe('State Validation Service', () => { config.responseType = 'id_token token'; const readSpy = vi.spyOn(storagePersistenceService, 'read'); - readSpy - .withArgs('authWellKnownEndPoints', config) - .mockReturnValue(authWellKnownEndpoints); - readSpy - .withArgs('authStateControl', config) - .mockReturnValue('authStateControl'); - readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); + mockImplementationWhenArgsEqual( + readSpy, + ['authWellKnownEndPoints', config], + () => authWellKnownEndpoints + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authStateControl', config], + () => 'authStateControl' + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authNonce', config], + () => 'authNonce' + ); const logDebugSpy = vi.spyOn(loggerService, 'logDebug'); // .mockImplementation(() => undefined); @@ -2003,8 +2136,9 @@ describe('State Validation Service', () => { config ); - const state = await lastValueFrom(stateObs$); - expect(logDebugSpy).toBeCalledWith([ + const state = await firstValueFrom(stateObs$); + + expect(logDebugSpy.mock.calls).toEqual([ [config, 'iss validation is turned off, this is not recommended!'], [config, 'authCallback token(s) validated, continue'], ]); @@ -2060,13 +2194,21 @@ describe('State Validation Service', () => { const readSpy = vi.spyOn(storagePersistenceService, 'read'); - readSpy - .withArgs('authWellKnownEndPoints', config) - .mockReturnValue(authWellKnownEndpoints); - readSpy - .withArgs('authStateControl', config) - .mockReturnValue('authStateControl'); - readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); + mockImplementationWhenArgsEqual( + readSpy, + ['authWellKnownEndPoints', config], + () => authWellKnownEndpoints + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authStateControl', config], + () => 'authStateControl' + ); + mockImplementationWhenArgsEqual( + readSpy, + ['authNonce', config], + () => 'authNonce' + ); const callbackContext = { code: 'fdffsdfsdf', @@ -2088,7 +2230,7 @@ describe('State Validation Service', () => { config ); - const state = await lastValueFrom(stateObs$); + const state = await firstValueFrom(stateObs$); expect(state.accessToken).toBe('access_tokenTEST'); expect(state.idToken).toBe(''); expect(state.decodedIdToken).toBeDefined(); @@ -2127,7 +2269,7 @@ describe('State Validation Service', () => { config ); - const isValid = await lastValueFrom(isValidObs$); + const isValid = await firstValueFrom(isValidObs$); expect(isValid.state).toBe(ValidationResult.Ok); expect(isValid.authResponseIsValid).toBe(true); }); @@ -2164,7 +2306,7 @@ describe('State Validation Service', () => { config ); - const isValid = await lastValueFrom(isValidObs$); + const isValid = await firstValueFrom(isValidObs$); expect(isValid.state).toBe(ValidationResult.Ok); expect(isValid.authResponseIsValid).toBe(true); }); @@ -2201,7 +2343,7 @@ describe('State Validation Service', () => { config ); - const isValid = await lastValueFrom(isValidObs$); + const isValid = await firstValueFrom(isValidObs$); expect(isValid.state).toBe(ValidationResult.Ok); expect(isValid.authResponseIsValid).toBe(true); }); diff --git a/src/validation/token-validation.service.spec.ts b/src/validation/token-validation.service.spec.ts index 8b13049..a8fe434 100644 --- a/src/validation/token-validation.service.spec.ts +++ b/src/validation/token-validation.service.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@/testing'; -import { lastValueFrom, of } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { vi } from 'vitest'; import { JwkExtractor } from '../extractors/jwk.extractor'; import { LoggerService } from '../logging/logger.service'; @@ -503,7 +503,7 @@ describe('TokenValidationService', () => { { configId: 'configId1' } ); - const valueFalse = await lastValueFrom(valueFalse$); + const valueFalse = await firstValueFrom(valueFalse$); expect(valueFalse).toEqual(false); }); @@ -514,7 +514,7 @@ describe('TokenValidationService', () => { { configId: 'configId1' } ); - const valueFalse = await lastValueFrom(valueFalse$); + const valueFalse = await firstValueFrom(valueFalse$); expect(valueFalse).toEqual(true); }); @@ -525,7 +525,7 @@ describe('TokenValidationService', () => { { configId: 'configId1' } ); - const valueFalse = await lastValueFrom(valueFalse$); + const valueFalse = await firstValueFrom(valueFalse$); expect(valueFalse).toEqual(false); }); @@ -542,7 +542,7 @@ describe('TokenValidationService', () => { { configId: 'configId1' } ); - const valueFalse = await lastValueFrom(valueFalse$); + const valueFalse = await firstValueFrom(valueFalse$); expect(valueFalse).toEqual(false); }); @@ -561,7 +561,7 @@ describe('TokenValidationService', () => { { configId: 'configId1' } ); - const valueFalse = await lastValueFrom(valueFalse$); + const valueFalse = await firstValueFrom(valueFalse$); expect(valueFalse).toEqual(false); }); @@ -597,7 +597,7 @@ describe('TokenValidationService', () => { { configId: 'configId1' } ); - const valueFalse = await lastValueFrom(valueFalse$); + const valueFalse = await firstValueFrom(valueFalse$); expect(valueFalse).toEqual(false); }); @@ -634,7 +634,7 @@ describe('TokenValidationService', () => { { configId: 'configId1' } ); - const valueTrue = await lastValueFrom(valueTrue$); + const valueTrue = await firstValueFrom(valueTrue$); expect(valueTrue).toEqual(true); }); }); @@ -651,7 +651,7 @@ describe('TokenValidationService', () => { { configId: 'configId1' } ); - const result = await lastValueFrom(result$); + const result = await firstValueFrom(result$); expect(result).toEqual(true); }); @@ -660,7 +660,7 @@ describe('TokenValidationService', () => { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1jNTdkTzZRR1RWQndhTmsifQ.eyJleHAiOjE1ODkyMTAwODYsIm5iZiI6MTU4OTIwNjQ4NiwidmVyIjoiMS4wIiwiaXNzIjoiaHR0cHM6Ly9kYW1pZW5ib2QuYjJjbG9naW4uY29tL2EwOTU4ZjQ1LTE5NWItNDAzNi05MjU5LWRlMmY3ZTU5NGRiNi92Mi4wLyIsInN1YiI6ImY4MzZmMzgwLTNjNjQtNDgwMi04ZGJjLTAxMTk4MWMwNjhmNSIsImF1ZCI6ImYxOTM0YTZlLTk1OGQtNDE5OC05ZjM2LTYxMjdjZmM0Y2RiMyIsIm5vbmNlIjoiMDA3YzQxNTNiNmEwNTE3YzBlNDk3NDc2ZmIyNDk5NDhlYzVjbE92UVEiLCJpYXQiOjE1ODkyMDY0ODYsImF1dGhfdGltZSI6MTU4OTIwNjQ4NiwibmFtZSI6ImRhbWllbmJvZCIsImVtYWlscyI6WyJkYW1pZW5AZGFtaWVuYm9kLm9ubWljcm9zb2Z0LmNvbSJdLCJ0ZnAiOiJCMkNfMV9iMmNwb2xpY3lkYW1pZW4iLCJhdF9oYXNoIjoiWmswZktKU19wWWhPcE04SUJhMTJmdyJ9.E5Z-0kOzNU7LBkeVHHMyNoER8TUapGzUUfXmW6gVu4v6QMM5fQ4sJ7KC8PHh8lBFYiCnaDiTtpn3QytUwjXEFnLDAX5qcZT1aPoEgL_OmZMC-8y-4GyHp35l7VFD4iNYM9fJmLE8SYHTVl7eWPlXSyz37Ip0ciiV0Fd6eoksD_aVc-hkIqngDfE4fR8ZKfv4yLTNN_SfknFfuJbZ56yN-zIBL4GkuHsbQCBYpjtWQ62v98p1jO7NhHKV5JP2ec_Ge6oYc_bKTrE6OIX38RJ2rIm7zU16mtdjnl_350Nw3ytHcTPnA1VpP_VLElCfe83jr5aDHc_UQRYaAcWlOgvmVg'; const atHash = 'bad'; - const result = await lastValueFrom( + const result = await firstValueFrom( tokenValidationService.validateIdTokenAtHash( accessToken, atHash, @@ -688,7 +688,7 @@ describe('TokenValidationService', () => { { configId: 'configId1' } ); - const result = await lastValueFrom(result$); + const result = await firstValueFrom(result$); expect(result).toEqual(true); }); @@ -704,7 +704,7 @@ describe('TokenValidationService', () => { { configId: 'configId1' } ); - const result = await lastValueFrom(result$); + const result = await firstValueFrom(result$); expect(result).toEqual(false); }); @@ -720,7 +720,7 @@ describe('TokenValidationService', () => { { configId: 'configId1' } ); - const result = await lastValueFrom(result$); + const result = await firstValueFrom(result$); expect(result).toEqual(false); }); }); diff --git a/src/validation/token-validation.service.ts b/src/validation/token-validation.service.ts index eebb6d5..573941f 100644 --- a/src/validation/token-validation.service.ts +++ b/src/validation/token-validation.service.ts @@ -390,7 +390,8 @@ export class TokenValidationService { localState: any, configuration: OpenIdConfiguration ): boolean { - if ((state as string) !== (localState as string)) { + console.error(state, localState, `${state}`, `${localState}`); + if (`${state}` !== `${localState}`) { this.loggerService.logDebug( configuration, `ValidateStateFromHashCallback failed, state: ${state} local_state:${localState}` diff --git a/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts deleted file mode 100644 index 8641cb5..0000000 --- a/tests-examples/demo-todo-app.spec.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { test, expect, type Page } from '@playwright/test'; - -test.beforeEach(async ({ page }) => { - await page.goto('https://demo.playwright.dev/todomvc'); -}); - -const TODO_ITEMS = [ - 'buy some cheese', - 'feed the cat', - 'book a doctors appointment' -] as const; - -test.describe('New Todo', () => { - test('should allow me to add todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create 1st todo. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Make sure the list only has one todo item. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0] - ]); - - // Create 2nd todo. - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - - // Make sure the list now has two todo items. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[1] - ]); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); - - test('should clear text input field when an item is added', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create one todo item. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Check that input is empty. - await expect(newTodo).toBeEmpty(); - await checkNumberOfTodosInLocalStorage(page, 1); - }); - - test('should append new items to the bottom of the list', async ({ page }) => { - // Create 3 items. - await createDefaultTodos(page); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - // Check test using different methods. - await expect(page.getByText('3 items left')).toBeVisible(); - await expect(todoCount).toHaveText('3 items left'); - await expect(todoCount).toContainText('3'); - await expect(todoCount).toHaveText(/3/); - - // Check all items in one call. - await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); - await checkNumberOfTodosInLocalStorage(page, 3); - }); -}); - -test.describe('Mark all as completed', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test.afterEach(async ({ page }) => { - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should allow me to mark all items as completed', async ({ page }) => { - // Complete all todos. - await page.getByLabel('Mark all as complete').check(); - - // Ensure all todos have 'completed' class. - await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - }); - - test('should allow me to clear the complete state of all items', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - // Check and then immediately uncheck. - await toggleAll.check(); - await toggleAll.uncheck(); - - // Should be no completed classes. - await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); - }); - - test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - await toggleAll.check(); - await expect(toggleAll).toBeChecked(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Uncheck first todo. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').uncheck(); - - // Reuse toggleAll locator and make sure its not checked. - await expect(toggleAll).not.toBeChecked(); - - await firstTodo.getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Assert the toggle all is checked again. - await expect(toggleAll).toBeChecked(); - }); -}); - -test.describe('Item', () => { - - test('should allow me to mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - // Check first item. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').check(); - await expect(firstTodo).toHaveClass('completed'); - - // Check second item. - const secondTodo = page.getByTestId('todo-item').nth(1); - await expect(secondTodo).not.toHaveClass('completed'); - await secondTodo.getByRole('checkbox').check(); - - // Assert completed class. - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).toHaveClass('completed'); - }); - - test('should allow me to un-mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const firstTodo = page.getByTestId('todo-item').nth(0); - const secondTodo = page.getByTestId('todo-item').nth(1); - const firstTodoCheckbox = firstTodo.getByRole('checkbox'); - - await firstTodoCheckbox.check(); - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await firstTodoCheckbox.uncheck(); - await expect(firstTodo).not.toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 0); - }); - - test('should allow me to edit an item', async ({ page }) => { - await createDefaultTodos(page); - - const todoItems = page.getByTestId('todo-item'); - const secondTodo = todoItems.nth(1); - await secondTodo.dblclick(); - await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); - await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); - - // Explicitly assert the new text value. - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2] - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); -}); - -test.describe('Editing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should hide other controls when editing', async ({ page }) => { - const todoItem = page.getByTestId('todo-item').nth(1); - await todoItem.dblclick(); - await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); - await expect(todoItem.locator('label', { - hasText: TODO_ITEMS[1], - })).not.toBeVisible(); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should save edits on blur', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should trim entered text', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should remove the item if an empty text string was entered', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[2], - ]); - }); - - test('should cancel edits on escape', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); - await expect(todoItems).toHaveText(TODO_ITEMS); - }); -}); - -test.describe('Counter', () => { - test('should display the current number of todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - await expect(todoCount).toContainText('1'); - - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - await expect(todoCount).toContainText('2'); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); -}); - -test.describe('Clear completed button', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - }); - - test('should display the correct text', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); - }); - - test('should remove completed items when clicked', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).getByRole('checkbox').check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(todoItems).toHaveCount(2); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should be hidden when there are no items that are completed', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); - }); -}); - -test.describe('Persistence', () => { - test('should persist its data', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const todoItems = page.getByTestId('todo-item'); - const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); - await firstTodoCheck.check(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - - // Ensure there is 1 completed item. - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - // Now reload. - await page.reload(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - }); -}); - -test.describe('Routing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - // make sure the app had a chance to save updated todos in storage - // before navigating to a new view, otherwise the items can get lost :( - // in some frameworks like Durandal - await checkTodosInLocalStorage(page, TODO_ITEMS[0]); - }); - - test('should allow me to display active items', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await expect(todoItem).toHaveCount(2); - await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should respect the back button', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await test.step('Showing all items', async () => { - await page.getByRole('link', { name: 'All' }).click(); - await expect(todoItem).toHaveCount(3); - }); - - await test.step('Showing active items', async () => { - await page.getByRole('link', { name: 'Active' }).click(); - }); - - await test.step('Showing completed items', async () => { - await page.getByRole('link', { name: 'Completed' }).click(); - }); - - await expect(todoItem).toHaveCount(1); - await page.goBack(); - await expect(todoItem).toHaveCount(2); - await page.goBack(); - await expect(todoItem).toHaveCount(3); - }); - - test('should allow me to display completed items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Completed' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(1); - }); - - test('should allow me to display all items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await page.getByRole('link', { name: 'Completed' }).click(); - await page.getByRole('link', { name: 'All' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(3); - }); - - test('should highlight the currently applied filter', async ({ page }) => { - await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); - - //create locators for active and completed links - const activeLink = page.getByRole('link', { name: 'Active' }); - const completedLink = page.getByRole('link', { name: 'Completed' }); - await activeLink.click(); - - // Page change - active items. - await expect(activeLink).toHaveClass('selected'); - await completedLink.click(); - - // Page change - completed items. - await expect(completedLink).toHaveClass('selected'); - }); -}); - -async function createDefaultTodos(page: Page) { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } -} - -async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).length === e; - }, expected); -} - -async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; - }, expected); -} - -async function checkTodosInLocalStorage(page: Page, title: string) { - return await page.waitForFunction(t => { - return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); - }, title); -} diff --git a/tsconfig.json b/tsconfig.json index 191f686..89328c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ }, "files": [], "include": [], + "exclude": ["node_modules"], "references": [ { "path": "./tsconfig.lib.json" diff --git a/tsconfig.lib.json b/tsconfig.lib.json index 8d6cadb..5f7fcc8 100644 --- a/tsconfig.lib.json +++ b/tsconfig.lib.json @@ -4,6 +4,8 @@ "rootDir": ".", "outDir": "./dist/tsc-lib", "lib": ["dom", "es2018"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, "paths": { "injection-js": ["./node_modules/injection-js/lib/index.ts"] } diff --git a/tsconfig.spec.json b/tsconfig.spec.json index aa0cca3..330d985 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -6,6 +6,8 @@ "noUncheckedIndexedAccess": true, "outDir": "./dist/tsc-test", "types": ["vitest/globals", "node"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, "paths": { "@/testing": ["./src/testing"], "@/testing/*": ["./src/testing/*"], diff --git a/vitest.config.ts b/vitest.config.ts index b8b8063..3a2a115 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,17 +1,29 @@ +import swc from 'unplugin-swc'; import tsconfigPaths from 'vite-tsconfig-paths'; import { defineConfig } from 'vitest/config'; export default defineConfig({ cacheDir: '.vitest', test: { - include: ['src/**/*.spec.ts', 'tests-examples'], + setupFiles: ['src/testing/init-test.ts'], + environment: 'jsdom', + include: ['src/**/*.spec.ts'], globals: true, - browser: { - provider: 'playwright', // or 'webdriverio' - enabled: true, - // at least one instance is required - instances: [{ browser: 'chromium' }], - }, + restoreMocks: true, + // browser: { + // provider: 'playwright', // or 'webdriverio' + // enabled: true, + // instances: [{ browser: 'chromium' }], + // }, }, - plugins: [tsconfigPaths({})], + plugins: [ + tsconfigPaths(), + swc.vite({ + include: /\.[mc]?[jt]sx?$/, + exclude: [ + /node_modules\/(?!injection-js|\.pnpm)/, + /node_modules\/\.pnpm\/(?!injection-js)/, + ] as any, + }), + ], });