Compare commits

...

26 Commits

Author SHA1 Message Date
e662d7d123 fix: fix solid-js adapter and exclude spec from build
Some checks failed
Build, Lint & Test Lib / Build, Lint and Test Library (push) Has been cancelled
2025-03-04 23:23:36 +08:00
dff1e1f9a6 feat: add solid-js support 2025-03-04 22:02:42 +08:00
3aabcd6442 fix: fix router navigateUrl
Some checks failed
Build, Lint & Test Lib / Build, Lint and Test Library (push) Has been cancelled
2025-02-21 05:30:31 +08:00
0d957dfb1c feat: support client_secret 2025-02-21 03:53:46 +08:00
144e4c2f97 fix: fix deps 2025-02-21 01:03:03 +08:00
de07175ff4 fix: fix building 2025-02-21 00:52:47 +08:00
41f2b04c45 refactor: switch from jsdom to happy-dom 2025-02-18 16:00:16 +08:00
ba13828093 feat: add more auth features and remove auth module 2025-02-07 16:59:58 +08:00
fe10ed2850 feat: add more auth features and remove auth module
Some checks failed
Build, Lint & Test Lib / Build, Lint and Test Library (push) Has been cancelled
2025-02-07 16:48:38 +08:00
c8c4fc847d doc: fix docs 2025-02-06 04:40:35 +08:00
25a27e1998 ci: fix workflows 2025-02-06 04:34:59 +08:00
57ae2191ae ci: fix workflows 2025-02-06 04:32:45 +08:00
9eb1239842 fix: fix package.json 2025-02-06 04:29:34 +08:00
13886502b6 feat: add basic example 2025-02-06 04:26:07 +08:00
58d7b3c89e build: fix build and add examples 2025-02-05 22:49:38 +08:00
f00c1d1aef feat: fix api spec errors 2025-02-04 01:13:23 +08:00
26a06fdbf0 fix: fix interceptors 2025-02-03 23:53:49 +08:00
eacbbb2815 docs: first add docs and license 2025-02-02 02:23:41 +08:00
6a03a2bd62 fix: fix some tests 2025-02-02 00:45:46 +08:00
28da493462 fix: add more testsx 2025-01-31 08:01:26 +08:00
c9d0066d64 fix: fix all biome 2025-01-31 05:57:51 +08:00
316361bd3c fix: fix observable 2025-01-31 03:23:45 +08:00
733b697ee2 fix: add cli 2025-01-31 02:02:07 +08:00
ca5f4984a4 refactor: rewrite observable subscribe 2025-01-30 21:38:04 +08:00
7ff7e891fc feat: prevent workflow 2025-01-30 20:03:09 +08:00
1785df25e2 feat: init 2025-01-30 20:02:28 +08:00
228 changed files with 16866 additions and 9159 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

View File

@@ -8,258 +8,51 @@ on:
types: [opened, synchronize, reopened, closed] types: [opened, synchronize, reopened, closed]
branches: branches:
- main - main
workflow_dispatch:
jobs: jobs:
build_job: build_job:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Built, Lint and Test Library name: Build, Lint and Test Library
steps: steps:
- uses: actions/checkout@v2 - name: Checkout
with: uses: actions/checkout@v4
submodules: true
- name: Setup Node.js - name: Setup Node and Install Dependencies
uses: actions/setup-node@v2 uses: pnpm/action-setup@v4
with: with:
node-version: 20 version: 10
run_install: false
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Installing Dependencies
run: npm ci
- name: Linting Library - name: Linting Library
run: npm run lint-lib run: npm run lint
- name: Testing Frontend - name: Testing Frontend
run: npm run test-lib-ci run: npm run test-ci
- name: Coveralls - name: 'Report Coverage'
uses: coverallsapp/github-action@master if: (github.event_name == 'pull_request' && github.event.action != 'closed')
with: uses: davelosert/vitest-coverage-report-action@v2
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: './coverage/oidc-client-rx/lcov.info'
- name: Coveralls Finished
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.github_token }}
parallel-finished: true
- name: Building Frontend - name: Building Frontend
run: npm run build-lib-prod run: npm run build
- name: Copying essential additional files
run: npm run copy-files
- name: Show files - name: Show files
run: ls run: ls
- name: Upload Artefact - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: angular_auth_oidc_client_artefact name: oidc_client_rx_artifact
path: dist/oidc-client-rx path: dist
AngularLatestVersion:
needs: build_job
runs-on: ubuntu-latest
name: Angular latest
steps:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 20
- name: Download Artefact
uses: actions/download-artifact@v3
with:
name: angular_auth_oidc_client_artefact
path: oidc-client-rx-artefact
- name: Install AngularCLI globally
run: sudo npm install -g @angular/cli
- name: Show ng Version
run: ng version
- name: Create Angular Project
run: sudo ng new oidc-client-rx-test --skip-git
- name: Npm Install & Install Library from local artefact
run: |
sudo cp -R oidc-client-rx-artefact oidc-client-rx-test/
cd oidc-client-rx-test
sudo npm install --unsafe-perm=true
sudo ng add ./oidc-client-rx-artefact --authority-url-or-tenant-id "my-authority-url" --flow-type "OIDC Code Flow PKCE using refresh tokens" --use-local-package=true --skip-confirmation
- name: Test Angular Application
working-directory: ./oidc-client-rx-test
run: npm test -- --watch=false --browsers=ChromeHeadless
- name: Build Angular Application
working-directory: ./oidc-client-rx-test
run: sudo npm run build
AngularLatestVersionWithSchematics:
needs: build_job
runs-on: ubuntu-latest
name: Angular latest & Schematics Job
steps:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 20
- name: Download Artefact
uses: actions/download-artifact@v3
with:
name: angular_auth_oidc_client_artefact
path: oidc-client-rx-artefact
- name: Install AngularCLI globally
run: sudo npm install -g @angular/cli
- name: Show ng Version
run: ng version
- name: Create Angular Project
run: sudo ng new oidc-client-rx-test --skip-git
- name: Npm Install & Install Library from local artefact
run: |
sudo cp -R oidc-client-rx-artefact oidc-client-rx-test/
cd oidc-client-rx-test
sudo npm install --unsafe-perm=true
sudo ng add ./oidc-client-rx-artefact --authority-url-or-tenant-id "my-authority-url" --flow-type "Default config" --use-local-package=true --skip-confirmation
- name: Test Angular Application
working-directory: ./oidc-client-rx-test
run: npm test -- --watch=false --browsers=ChromeHeadless
- name: Build Angular Application
working-directory: ./oidc-client-rx-test
run: sudo npm run build
AngularLatestVersionWithNgModuleSchematics:
needs: build_job
runs-on: ubuntu-latest
name: Angular latest Standalone & Schematics Job
steps:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 20
- name: Download Artefact
uses: actions/download-artifact@v3
with:
name: angular_auth_oidc_client_artefact
path: oidc-client-rx-artefact
- name: Install AngularCLI globally
run: sudo npm install -g @angular/cli
- name: Show ng Version
run: ng version
- name: Create Angular Project
run: sudo ng new oidc-client-rx-test --skip-git --standalone=false
- name: Npm Install & Install Library from local artefact
run: |
sudo cp -R oidc-client-rx-artefact oidc-client-rx-test/
cd oidc-client-rx-test
sudo npm install --unsafe-perm=true
sudo ng add ./oidc-client-rx-artefact --authority-url-or-tenant-id "my-authority-url" --flow-type "OIDC Code Flow PKCE using refresh tokens" --use-local-package=true --skip-confirmation
- name: Test Angular Application
working-directory: ./oidc-client-rx-test
run: npm test -- --watch=false --browsers=ChromeHeadless
- name: Build Angular Application
working-directory: ./oidc-client-rx-test
run: sudo npm run build
Angular16VersionWithRxJs6:
needs: build_job
runs-on: ubuntu-latest
name: Angular 16 & RxJs 6
steps:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 20
- name: Download Artefact
uses: actions/download-artifact@v3
with:
name: angular_auth_oidc_client_artefact
path: oidc-client-rx-artefact
- name: Install AngularCLI globally
run: sudo npm install -g @angular/cli@16
- name: Show ng Version
run: ng version
- name: Create Angular Project
run: sudo ng new oidc-client-rx-test --skip-git
- name: npm install RxJs 6
working-directory: ./oidc-client-rx-test
run: sudo npm install rxjs@6.5.3
- name: Npm Install & Install Library from local artefact
run: |
sudo cp -R oidc-client-rx-artefact oidc-client-rx-test/
cd oidc-client-rx-test
sudo npm install --unsafe-perm=true
sudo ng add ./oidc-client-rx-artefact --authority-url-or-tenant-id "my-authority-url" --flow-type "OIDC Code Flow PKCE using refresh tokens" --use-local-package=true --skip-confirmation
- name: Test Angular Application
working-directory: ./oidc-client-rx-test
run: npm test -- --watch=false --browsers=ChromeHeadless
- name: Build Angular Application
working-directory: ./oidc-client-rx-test
run: sudo npm run build
LibWithAngularV16:
needs: build_job
runs-on: ubuntu-latest
name: Angular V16
steps:
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18
- name: Download Artefact
uses: actions/download-artifact@v3
with:
name: angular_auth_oidc_client_artefact
path: oidc-client-rx-artefact
- name: Install AngularCLI globally
run: sudo npm install -g @angular/cli@16
- name: Show ng Version
run: ng version
- name: Create Angular Project
run: sudo ng new oidc-client-rx-test --skip-git
- name: Npm Install & Install Library from local artefact
run: |
sudo cp -R oidc-client-rx-artefact oidc-client-rx-test/
cd oidc-client-rx-test
sudo npm install --unsafe-perm=true
sudo ng add ./oidc-client-rx-artefact --authority-url-or-tenant-id "my-authority-url" --flow-type "OIDC Code Flow PKCE using refresh tokens" --use-local-package=true --skip-confirmation
- name: Test Angular Application
working-directory: ./oidc-client-rx-test
run: npm test -- --watch=false --browsers=ChromeHeadless
- name: Build Angular Application
working-directory: ./oidc-client-rx-test
run: sudo npm run build

View File

@@ -1,61 +0,0 @@
name: Docs
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- main
jobs:
build_and_deploy_job:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest
name: Build and Deploy Docs Job
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 18
- name: Installing Dependencies
run: sudo npm install
- name: Installing Dependencies for docs - in docs folder
run: sudo npm install
working-directory: docs/site/oidc-client-rx
- name: Building Documentation
run: sudo npm run build
working-directory: docs/site/oidc-client-rx
- name: Build And Deploy
if: ${{ github.actor == 'damienbod' || github.actor == 'FabianGosebrink' }}
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
action: 'upload'
###### Repository/Build Configurations - These values can be configured to match you app requirements. ######
app_location: '/docs/site/oidc-client-rx' # App source code path
app_artifact_location: 'build' # Built app content directory - optional
###### End of Repository/Build Configurations ######
close_pull_request_job:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
name: Close Pull Request Job
steps:
- name: Close Pull Request
id: closepullrequest
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
action: 'close'

View File

@@ -1,27 +0,0 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm install -g pnpm && pnpm install
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
- name: Run Playwright tests
run: pnpm exec playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30

6
.gitignore vendored
View File

@@ -2,6 +2,7 @@
# compiled output # compiled output
/dist /dist
**/dist
/tmp /tmp
/out-tsc /out-tsc
# Only exists if Bazel was run # Only exists if Bazel was run
@@ -32,7 +33,6 @@ speed-measure-plugin*.json
.history/* .history/*
# misc # misc
/.angular/cache
/.sass-cache /.sass-cache
/connect.lock /connect.lock
/coverage /coverage
@@ -46,7 +46,6 @@ testem.log
.DS_Store .DS_Store
Thumbs.db Thumbs.db
/.angulardoc.json
debug.log debug.log
/.husky /.husky
@@ -55,3 +54,6 @@ debug.log
/playwright-report/ /playwright-report/
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
/.vitest
/.rslib
**/*.tsbuildinfo

219
README.md
View File

@@ -1,207 +1,66 @@
# Angular Lib for OpenID Connect & OAuth2 <h1 align="center">
<img src="./assets/logo-512.png" height="150" alt="OUTPOSTS">
<div style="color: #232848; font-weight: 700;">OIDC-CLIENT-RX</div>
<div align="center">
<img src="https://img.shields.io/github/actions/workflow/status/lonelyhentxi/oidc-client-rx/build.yml?branch=main" alt="build-status" />
<img src="https://img.shields.io/badge/status-work--in--progress-blue" alt="status-badge" />
</div>
</h1>
![Build Status](https://github.com/damienbod/oidc-client-rx/actions/workflows/build.yml/badge.svg?branch=main) [![npm](https://img.shields.io/npm/v/oidc-client-rx.svg)](https://www.npmjs.com/package/oidc-client-rx) [![npm](https://img.shields.io/npm/dm/oidc-client-rx.svg)](https://www.npmjs.com/package/oidc-client-rx) [![npm](https://img.shields.io/npm/l/oidc-client-rx.svg)](https://www.npmjs.com/package/oidc-client-rx) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) [![Coverage Status](https://coveralls.io/repos/github/damienbod/oidc-client-rx/badge.svg?branch=main)](https://coveralls.io/github/damienbod/oidc-client-rx?branch=main) <p align="center">ReactiveX enhanced OIDC and OAuth2 protocol support for browser-based JavaScript applications.</p>
<p align="center"> ## Quick Start
<a href="https://oidc-client-rx.com/"><img src="https://raw.githubusercontent.com/damienbod/oidc-client-rx/main/.github/angular-auth-logo.png" alt="" width="350" /></a>
</p>
Secure your Angular app using the latest standards for OpenID Connect & OAuth2. Provides support for token refresh, all modern OIDC Identity Providers and more. @TODO Add More Details
## Acknowledgements ### Install
This library is <a href="http://openid.net/certification/#RPs">certified</a> by OpenID Foundation. (RP Implicit and Config RP) ```sh
pnpm add oidc-client-rx @outposts/injection-js @abraham/reflection
<p align="center"> # npm install oidc-client-rx @outposts/injection-js @abraham/reflection
<a href="http://openid.net/certification/#RPs"><img src="https://damienbod.files.wordpress.com/2017/06/oid-l-certification-mark-l-rgb-150dpi-90mm.png" alt="" width="400" /></a> # yarn add oidc-client-rx @outposts/injection-js @abraham/reflection
</p>
## Features
- [Code samples](https://oidc-client-rx.com/docs/samples/) for most of the common use cases
- Supports schematics via `ng add` support
- Supports all modern OIDC identity providers
- Supports OpenID Connect Code Flow with PKCE
- Supports Code Flow PKCE with Refresh tokens
- [Supports OpenID Connect Implicit Flow](http://openid.net/specs/openid-connect-implicit-1_0.html)
- [Supports OpenID Connect Session Management 1.0](http://openid.net/specs/openid-connect-session-1_0.html)
- [Supports RFC7009 - OAuth 2.0 Token Revocation](https://tools.ietf.org/html/rfc7009)
- [Supports RFC7636 - Proof Key for Code Exchange (PKCE)](https://tools.ietf.org/html/rfc7636)
- [Supports OAuth 2.0 Pushed authorisation requests (PAR) draft](https://tools.ietf.org/html/draft-ietf-oauth-par-06)
- Semantic releases
- Github actions
- Modern coding guidelines with prettier, husky
- Up to date documentation
- Implements OIDC validation as specified, complete client side validation for REQUIRED features
- Supports authentication using redirect or popup
## Installation
### Ng Add
You can use the schematics and `ng add` the library.
```shell
ng add oidc-client-rx
``` ```
And answer the questions. A module will be created which encapsulates your configuration. ### Basic Usage
![oidc-client-rx schematics](https://raw.githubusercontent.com/damienbod/oidc-client-rx/main/.github/oidc-client-rx-schematics-720.gif) ```typescript
import '@abraham/reflection'; // or 'reflect-metadata' | 'core-js/es7/reflect'
import { type Injector, ReflectiveInjector } from '@outposts/injection-js';
import { LogLevel, OidcSecurityService, provideAuth, withDefaultFeatures } from 'oidc-client-rx';
### Npm / Yarn const injector = ReflectiveInjector.resolveAndCreate(
provideAuth(
Navigate to the level of your `package.json` and type {
```shell
npm install oidc-client-rx
```
or with yarn
```shell
yarn add oidc-client-rx
```
## Documentation
[Read the docs here](https://oidc-client-rx.com/)
## Samples
[Explore the Samples here](https://oidc-client-rx.com/docs/samples/)
## Quickstart
For the example of the Code Flow. For further examples please check the [Samples](https://oidc-client-rx.com/docs/samples/) Section.
> If you have done the installation with the schematics, these modules and files should be available already!
### Configuration
Import the `AuthModule` in your module.
```ts
import { NgModule } from '@angular/core';
import { AuthModule, LogLevel } from 'oidc-client-rx';
// ...
@NgModule({
// ...
imports: [
// ...
AuthModule.forRoot({
config: { config: {
authority: '<your authority address here>', authority: '<your-authority>',
redirectUrl: window.location.origin, redirectUrl: `${window.location.origin}/auth/callback`,
postLogoutRedirectUri: window.location.origin, postLogoutRedirectUri: window.location.origin,
clientId: '<your clientId>', clientId: '<your-client-id>',
scope: 'openid profile email offline_access', scope: 'openid profile email offline_access',
responseType: 'code', responseType: 'code',
silentRenew: true, silentRenew: true,
useRefreshToken: true, useRefreshToken: true,
logLevel: LogLevel.Debug, logLevel: LogLevel.Debug,
...
}, },
}), },
], withDefaultFeatures()
// ... )
}) ) as Injector;
export class AppModule {}
```
And call the method `checkAuth()` from your `app.component.ts`. The method `checkAuth()` is needed to process the redirect from your Security Token Service and set the correct states. This method must be used to ensure the correct functioning of the library. const oidcSecurityService = injector.get(OidcSecurityService);
```ts oidcSecurityService.checkAuth().subscribe((result) => {
import { Component, OnInit, inject } from '@angular/core'; console.debug('checkAuth result: ', result);
import { OidcSecurityService } from 'oidc-client-rx';
@Component({
/*...*/
})
export class AppComponent implements OnInit {
private readonly oidcSecurityService = inject(OidcSecurityService);
ngOnInit() {
this.oidcSecurityService
.checkAuth()
.subscribe((loginResponse: LoginResponse) => {
const { isAuthenticated, userData, accessToken, idToken, configId } =
loginResponse;
/*...*/
});
}
login() {
this.oidcSecurityService.authorize();
}
logout() {
this.oidcSecurityService
.logoff()
.subscribe((result) => console.log(result));
}
}
```
### Using the access token
You can get the access token by calling the method `getAccessToken()` on the `OidcSecurityService`
```ts
const token = this.oidcSecurityService.getAccessToken().subscribe(...);
```
And then you can use it in the HttpHeaders
```ts
import { HttpHeaders } from '@angular/common/http';
const token = this.oidcSecurityServices.getAccessToken().subscribe((token) => {
const httpOptions = {
headers: new HttpHeaders({
Authorization: 'Bearer ' + token,
}),
};
}); });
const isAuthenticated$ = oidcSecurityService.isAuthenticated$;
``` ```
You can use the built in interceptor to add the accesstokens to your request ### More Examples
```ts - [React + TanStack Router](https://github.com/lonelyhentxi/oidc-client-rx/tree/main/examples/react-tanstack-router)
AuthModule.forRoot({
config: {
// ...
secureRoutes: ['https://my-secure-url.com/', 'https://my-second-secure-url.com/'],
},
}),
```
```ts
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
],
```
## Versions
Current Version is Version 19.x
- [Info about Version 18](https://github.com/damienbod/oidc-client-rx/tree/version-18)
- [Info about Version 17](https://github.com/damienbod/oidc-client-rx/tree/version-17)
- [Info about Version 16](https://github.com/damienbod/oidc-client-rx/tree/version-16)
- [Info about Version 15](https://github.com/damienbod/oidc-client-rx/tree/version-15)
- [Info about Version 14](https://github.com/damienbod/oidc-client-rx/tree/version-14)
- [Info about Version 13](https://github.com/damienbod/oidc-client-rx/tree/version-13)
- [Info about Version 12](https://github.com/damienbod/oidc-client-rx/tree/version-12)
- [Info about Version 11](https://github.com/damienbod/oidc-client-rx/tree/version-11)
- [Info about Version 10](https://github.com/damienbod/oidc-client-rx/tree/version-10)
## License ## License
[MIT](https://choosealicense.com/licenses/mit/) [MIT](https://choosealicense.com/licenses/mit/)
## Authors
- [@DamienBod](https://www.github.com/damienbod)
- [@FabianGosebrink](https://www.github.com/FabianGosebrink)

BIN
assets/logo-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -1,25 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"extends": ["ultracite"],
"linter": {
"rules": {
"style": {
"noNonNullAssertion": "off"
},
"suspicious": {
"noExplicitAny": "off"
},
"correctness": {
"noUnusedImports": {
"fix": "none",
"level": "warn"
}
}
}
},
"files": {
"ignore": [
".vscode/*.json"
]
}
}

41
biome.jsonc Normal file
View File

@@ -0,0 +1,41 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"extends": ["ultracite"],
"linter": {
"rules": {
"style": {
"noNonNullAssertion": "off",
"noParameterAssign": "off",
"useFilenamingConvention": "off",
"noParameterProperties": "off"
},
"suspicious": {
"noExplicitAny": "off"
},
"complexity": {
"noForEach": "off"
},
"correctness": {
"noUnusedImports": {
"fix": "none",
"level": "warn"
}
},
"nursery": {
"noEnum": "off",
"useConsistentMemberAccessibility": "off"
}
}
},
"files": {
"ignore": [".vscode/*.json"]
},
"overrides": [
{
"include": ["src/**/*.spec.ts", "src/test.ts", "test"],
"javascript": {
"globals": ["describe", "beforeEach", "it", "expect"]
}
}
]
}

View File

@@ -0,0 +1,13 @@
# Local
.DS_Store
*.local
*.log*
# Dist
node_modules
dist/
# IDE
.vscode/*
!.vscode/extensions.json
.idea

View File

@@ -0,0 +1,29 @@
# Rsbuild project
## Setup
Install the dependencies:
```bash
pnpm install
```
## Get started
Start the dev server:
```bash
pnpm dev
```
Build the app for production:
```bash
pnpm build
```
Preview the production build locally:
```bash
pnpm preview
```

View File

@@ -0,0 +1,31 @@
{
"name": "react-tanstack-router",
"private": true,
"version": "1.0.0",
"scripts": {
"dev": "rsbuild dev",
"build": "rsbuild build",
"preview": "rsbuild preview"
},
"dependencies": {
"@abraham/reflection": "^0.12.0",
"@outposts/injection-js": "^2.5.1",
"@tanstack/react-router": "^1.99.6",
"@tanstack/router-devtools": "^1.99.6",
"autoprefixer": "^10.4.20",
"observable-hooks": "^4.2.4",
"oidc-client-rx": "workspace:*",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^3.0.0"
},
"devDependencies": {
"@rsbuild/core": "^1.2.3",
"@rsbuild/plugin-react": "^1.1.0",
"@tanstack/router-plugin": "^1.99.6",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"postcss": "^8.5.1",
"typescript": "^5.7.3"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -0,0 +1,12 @@
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { TanStackRouterRspack } from '@tanstack/router-plugin/rspack';
export default defineConfig({
plugins: [pluginReact()],
tools: {
rspack: {
plugins: [TanStackRouterRspack()],
},
},
});

View File

@@ -0,0 +1 @@
/// <reference types="@rsbuild/core/types" />

View File

@@ -0,0 +1,87 @@
import '@abraham/reflection'; // or 'reflect-metadata' | 'core-js/es7/reflect'
import { type Injector, ReflectiveInjector } from '@outposts/injection-js';
import { RouterProvider, createRouter } from '@tanstack/react-router';
import {
LogLevel,
OidcSecurityService,
provideAuth,
withDefaultFeatures,
} from 'oidc-client-rx';
import { withTanstackRouter } from 'oidc-client-rx/adapters/@tanstack/react-router';
import {
InjectorContextVoidInjector,
InjectorProvider,
} from 'oidc-client-rx/adapters/react';
import React from 'react';
import ReactDOM from 'react-dom/client';
import { routeTree } from './routeTree.gen';
import './style.css';
// Set up a Router instance
const router = createRouter({
routeTree,
defaultPreload: 'intent',
scrollRestoration: true,
context: {
injector: InjectorContextVoidInjector,
oidcSecurityService: {} as OidcSecurityService,
},
});
// Register things for typesafety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}
const injector = ReflectiveInjector.resolveAndCreate(
provideAuth(
{
config: {
authority: 'https://k9bor3.logto.app/oidc',
redirectUrl: `${window.location.origin}/auth/callback`,
postLogoutRedirectUri: window.location.origin,
clientId: 'zz5vo27wtvtjf36srwtbp',
scope: 'openid offline_access',
responseType: 'code',
silentRenew: true,
useRefreshToken: true,
logLevel: LogLevel.Debug,
autoUserInfo: true,
renewUserInfoAfterTokenRenew: true,
customParamsAuthRequest: {
prompt: 'consent',
},
},
},
withDefaultFeatures(
// the after feature will replace the before same type feature
// so the following line can be ignored
{ router: { enabled: false } }
),
withTanstackRouter(router)
)
) as Injector;
// if needed, check when init
const oidcSecurityService = injector.get(OidcSecurityService);
oidcSecurityService.checkAuth().subscribe();
const rootEl = document.getElementById('root');
if (rootEl) {
const root = ReactDOM.createRoot(rootEl);
root.render(
<React.StrictMode>
<InjectorProvider injector={injector}>
<RouterProvider
router={router}
context={{ injector, oidcSecurityService }}
/>
</InjectorProvider>
</React.StrictMode>
);
}

View File

@@ -0,0 +1,111 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
// Import Routes
import { Route as rootRoute } from './routes/__root';
import { Route as AuthCallbackImport } from './routes/auth/callback';
import { Route as IndexImport } from './routes/index';
// Create/Update Routes
const IndexRoute = IndexImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRoute,
} as any);
const AuthCallbackRoute = AuthCallbackImport.update({
id: '/auth/callback',
path: '/auth/callback',
getParentRoute: () => rootRoute,
} as any);
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/';
path: '/';
fullPath: '/';
preLoaderRoute: typeof IndexImport;
parentRoute: typeof rootRoute;
};
'/auth/callback': {
id: '/auth/callback';
path: '/auth/callback';
fullPath: '/auth/callback';
preLoaderRoute: typeof AuthCallbackImport;
parentRoute: typeof rootRoute;
};
}
}
// Create and export the route tree
export interface FileRoutesByFullPath {
'/': typeof IndexRoute;
'/auth/callback': typeof AuthCallbackRoute;
}
export interface FileRoutesByTo {
'/': typeof IndexRoute;
'/auth/callback': typeof AuthCallbackRoute;
}
export interface FileRoutesById {
__root__: typeof rootRoute;
'/': typeof IndexRoute;
'/auth/callback': typeof AuthCallbackRoute;
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath;
fullPaths: '/' | '/auth/callback';
fileRoutesByTo: FileRoutesByTo;
to: '/' | '/auth/callback';
id: '__root__' | '/' | '/auth/callback';
fileRoutesById: FileRoutesById;
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute;
AuthCallbackRoute: typeof AuthCallbackRoute;
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AuthCallbackRoute: AuthCallbackRoute,
};
export const routeTree = rootRoute
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>();
/* ROUTE_MANIFEST_START
{
"routes": {
"__root__": {
"filePath": "__root.tsx",
"children": [
"/",
"/auth/callback"
]
},
"/": {
"filePath": "index.tsx"
},
"/auth/callback": {
"filePath": "auth/callback.tsx"
}
}
}
ROUTE_MANIFEST_END */

View File

@@ -0,0 +1,38 @@
import type { Injector } from '@outposts/injection-js';
import {
Link,
Outlet,
createRootRouteWithContext,
} from '@tanstack/react-router';
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
import type { OidcSecurityService } from 'oidc-client-rx';
export interface RouterContext {
injector: Injector;
oidcSecurityService: OidcSecurityService;
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootComponent,
});
function RootComponent() {
return (
<>
<div className="flex gap-2 p-2 text-lg">
<Link
to="/"
activeProps={{
className: 'font-bold',
}}
activeOptions={{ exact: true }}
>
Home
</Link>{' '}
</div>
<hr />
<Outlet />
<TanStackRouterDevtools position="bottom-right" />
</>
);
}

View File

@@ -0,0 +1,13 @@
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/auth/callback')({
component: AuthCallbackComponent,
});
function AuthCallbackComponent() {
return (
<div className="p-2">
<h3>Auth Callback: validating...</h3>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { createFileRoute } from '@tanstack/react-router';
import { useObservableEagerState } from 'observable-hooks';
import { useOidcClient } from 'oidc-client-rx/adapters/react';
import { useCallback } from 'react';
export const Route = createFileRoute('/')({
component: HomeComponent,
});
function HomeComponent() {
const { oidcSecurityService } = useOidcClient();
const { isAuthenticated } = useObservableEagerState(
oidcSecurityService.isAuthenticated$
);
const handleLogin = useCallback(() => {
oidcSecurityService.authorize().subscribe();
}, [oidcSecurityService]);
const handleLogout = useCallback(() => {
oidcSecurityService.logoff().subscribe();
}, [oidcSecurityService]);
return (
<div className="p-2 text-center">
<h1>Welcome OIDC-CLIENT-RX DEMO of react-tanstack-router</h1>
<p>Is authenticated? {isAuthenticated ? 'True' : 'False'}</p>
{isAuthenticated ? (
<button onClick={handleLogout} type="button">
Click to Logout
</button>
) : (
<button onClick={handleLogin} type="button">
Click to Login
</button>
)}
</div>
);
}

View File

@@ -0,0 +1,13 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
color-scheme: light dark;
}
* {
@apply border-gray-200 dark:border-gray-800;
}
body {
@apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
}

View File

@@ -0,0 +1,4 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{js,jsx,ts,tsx}', './index.html'],
};

View File

@@ -0,0 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"rootDir": ".",
"lib": ["ES2021", "DOM", "DOM.Iterable"],
"useDefineForClassFields": true,
"resolveJsonModule": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"emitDeclarationOnly": true,
"noEmit": true,
"outDir": "./dist",
"declarationDir": "./dist",
"jsx": "preserve"
},
"include": ["src"]
}

View File

@@ -0,0 +1,4 @@
{
"routesDirectory": "./src/routes",
"generatedRouteTree": "./src/routeTree.gen.ts"
}

View File

@@ -1,50 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma'),
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true, // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(
__dirname,
'../../coverage/oidc-client-rx'
),
subdir: '.',
reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'lcov' }],
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
customLaunchers: {
ChromeHeadlessNoSandbox: {
base: 'ChromeHeadless',
flags: ['--no-sandbox'],
},
},
singleRun: false,
restartOnFileChange: true,
});
};

View File

@@ -1,41 +0,0 @@
# EXAMPLE USAGE
# Refer for explanation to following link:
# https://github.com/evilmartians/lefthook/blob/master/docs/full_guide.md
#
pre-push:
commands:
fix-prettier:
tags: frontend security
glob: '*.{js,ts}'
run: npm run fix-prettier {staged_files}
pre-commit:
parallel: true
commands:
check-blockwords:
run: npm run check-blockwords
lint:
run: npm run lint-lib
#
# pre-commit:
# parallel: true
# commands:
# eslint:
# glob: "*.{js,ts}"
# run: yarn eslint {staged_files}
# rubocop:
# tags: backend style
# glob: "*.rb"
# exclude: "application.rb|routes.rb"
# run: bundle exec rubocop --force-exclusion {all_files}
# govet:
# tags: backend style
# files: git ls-files -m
# glob: "*.go"
# run: go vet {files}
# scripts:
# "hello.js":
# runner: node
# "any.go":
# runner: go run

View File

@@ -1,4 +0,0 @@
/**
* @license oidc-client-rx
* MIT license
*/

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 damienbod
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

21
licenses/angular.LICENSE Normal file
View File

@@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2010-2025 Google LLC. https://angular.dev/license
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,5 +1,6 @@
{ {
"name": "oidc-client-rx", "name": "oidc-client-rx",
"version": "0.1.0-alpha.8",
"homepage": "https://github.com/lonelyhentxi/oidc-client-rx", "homepage": "https://github.com/lonelyhentxi/oidc-client-rx",
"author": "lonelyhentxi", "author": "lonelyhentxi",
"description": "ReactiveX enhanced OIDC and OAuth2 protocol support for browser-based JavaScript applications", "description": "ReactiveX enhanced OIDC and OAuth2 protocol support for browser-based JavaScript applications",
@@ -10,48 +11,103 @@
"bugs": { "bugs": {
"url": "https://github.com/lonelyhentxi/oidc-client-rx/issues" "url": "https://github.com/lonelyhentxi/oidc-client-rx/issues"
}, },
"version": "0.1.0",
"type": "module", "type": "module",
"exports": { "exports": {
".": { ".": {
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./dist/index.js", "import": "./dist/index.js",
"require": "./dist/index.cjs" "require": "./dist/index.cjs"
},
"./adapters/react": {
"types": "./dist/adapters/react/index.d.ts",
"import": "./dist/adapters/react/index.js",
"require": "./dist/adapters/react.cjs"
},
"./adapters/solid-js": {
"types": "./dist/adapters/solid-js/index.d.ts",
"import": "./dist/adapters/solid-js/index.js",
"require": "./dist/adapters/solid-js.cjs"
},
"./adapters/@tanstack/react-router": {
"types": "./dist/adapters/@tanstack/react-router.d.ts",
"import": "./dist/adapters/@tanstack/react-router.js",
"require": "./dist/adapters/@tanstack/react-router.cjs"
},
"./adapters/@tanstack/solid-router": {
"types": "./dist/adapters/@tanstack/solid-router.d.ts",
"import": "./dist/adapters/@tanstack/solid-router.js",
"require": "./dist/adapters/@tanstack/solid-router.cjs"
} }
}, },
"main": "./dist/index.cjs", "main": "./dist/index.cjs",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"files": [ "files": ["dist", "licenses", "LICENSE", "README.md"],
"dist"
],
"scripts": { "scripts": {
"build": "rslib build", "build": "rslib build",
"dev": "rslib build --watch", "dev": "rslib build --watch",
"test": "vitest --code-coverage", "test": "vitest --coverage",
"test-ci": "vitest --watch=false --browsers=ChromeHeadlessNoSandbox --code-coverage", "test-ci": "vitest --watch=false --coverage",
"pack": "npm run build && npm pack ./dist", "prepublishOnly": "npm run build",
"publish": "npm run build && npm publish ./dist",
"coverage": "vitest run --coverage",
"lint": "ultracite lint", "lint": "ultracite lint",
"format": "ultracite format" "format": "ultracite format",
"cli": "tsx scripts/cli.ts"
}, },
"dependencies": { "dependencies": {
"@ngify/http": "^2.0.4", "@ngify/http": "^2.0.4",
"injection-js": "git+https://github.com/mgechev/injection-js.git#81a10e0", "@outposts/injection-js": "^2.5.1",
"rxjs": ">=7.4.0" "rfc4648": "^1.5.0"
},
"peerDependencies": {
"@tanstack/react-router": "*",
"@tanstack/solid-router": "*",
"react": ">=16.8.0",
"rxjs": "^7.4.0||>=8.0.0",
"solid-js": "^1"
}, },
"devDependencies": { "devDependencies": {
"@evilmartians/lefthook": "^1.0.3", "@biomejs/biome": "1.9.4",
"@biomejs/js-api": "0.7.1",
"@biomejs/wasm-nodejs": "^1.9.4",
"@playwright/test": "^1.49.1", "@playwright/test": "^1.49.1",
"@rslib/core": "^0.3.1", "@rslib/core": "^0.5.3",
"@types/jasmine": "^4.0.0", "@swc/core": "^1.10.12",
"@types/node": "^22.10.1", "@tanstack/react-router": "^1.112.11",
"@vitest/coverage-v8": "^3.0.1", "@tanstack/solid-router": "^1.112.11",
"rfc4648": "^1.5.0", "@types/lodash-es": "^4.17.12",
"@types/node": "^22.12.0",
"@types/react": "^19.0.8",
"@vitest/coverage-v8": "^3.0.4",
"commander": "^13.1.0",
"happy-dom": "^17.1.0",
"lodash-es": "^4.17.21",
"oxc-parser": "^0.54.0",
"oxc-walker": "^0.2.2",
"playwright": "^1.50.0",
"react": "^19.0.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.4.0",
"solid-js": "^1.9.5",
"tsx": "^4.19.2",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"ultracite": "^4.1.15", "ultracite": "^4.1.15",
"vitest": "^3.0.1" "unplugin-swc": "^1.5.1",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.4"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"@tanstack/react-router": {
"optional": true
},
"@tanstack/solid-router": {
"optional": true
},
"solid-js": {
"optional": true
}
}, },
"keywords": [ "keywords": [
"rxjs", "rxjs",
@@ -68,6 +124,19 @@
"certified", "certified",
"oauth", "oauth",
"authorization", "authorization",
"reactivex" "reactivex",
] "injection-js",
"injection"
],
"pnpm": {
"onlyBuiltDependencies": [
"@biomejs/biome",
"@swc/core",
"core-js",
"edgedriver",
"esbuild",
"geckodriver",
"msw"
]
}
} }

View File

@@ -1,5 +1,7 @@
import { defineConfig, devices } from '@playwright/test'; import { defineConfig, devices } from '@playwright/test';
// TODO
/** /**
* Read environment variables from file. * Read environment variables from file.
* https://github.com/motdotla/dotenv * https://github.com/motdotla/dotenv

5819
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
packages:
- 'examples/*'

View File

@@ -2,17 +2,40 @@ import { defineConfig } from '@rslib/core';
export default defineConfig({ export default defineConfig({
source: { source: {
tsconfigPath: './tsconfig.lib.json' tsconfigPath: './tsconfig.lib.json',
}, },
lib: [ lib: [
{ {
format: 'esm', format: 'esm',
syntax: 'es2021', syntax: 'es2021',
dts: true, bundle: false,
dts: {
bundle: false,
build: false,
distPath: './dist',
},
source: {
entry: {
index: ['src/**/*.ts', '!**/*.spec.ts', '!src/testing/**/*'],
},
},
}, },
{ {
format: 'cjs', format: 'cjs',
syntax: 'es2021', syntax: 'es2021',
dts: false,
bundle: true,
source: {
entry: {
index: './src/index.ts',
'adapters/react': './src/adapters/react/index.ts',
'adapters/solid-js': './src/adapters/solid-js/index.ts',
'adapters/@tanstack/react-router':
'./src/adapters/@tanstack/react-router.ts',
'adapters/@tanstack/solid-router':
'./src/adapters/@tanstack/solid-router.ts',
},
},
}, },
], ],
output: { output: {

19
scripts/cli.ts Normal file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env node
import { Command } from 'commander';
import { rewriteAllObservableSubscribeTofirstValueFrom } from './code-transform';
const program = new Command();
program
.version('1.0.0')
.description('A CLI tool to help develop oidc-client-rx');
program
.command('rewrite <pattern>')
.description('Rewrite files matching the given glob pattern')
.action(async (pattern: string) => {
await rewriteAllObservableSubscribeTofirstValueFrom(pattern);
});
program.parse(process.argv);

View File

@@ -0,0 +1,82 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { Biome, Distribution } from '@biomejs/js-api';
import { rewriteObservableSubscribeTofirstValueFrom } from './code-transform';
describe('rewriteSpecObservableSubscribeTofirstValueFrom', () => {
it('should transform simple example valid string', async () => {
const actual = await rewriteObservableSubscribeTofirstValueFrom(
'index.ts',
`refreshSessionIframeService
.refreshSessionWithIframe(allConfigs[0]!, allConfigs)
.subscribe((result) => {
expect(
result
).toHaveBeenCalledExactlyOnceWith(
'a-url',
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({
distribution: Distribution.NODE,
});
assert.equal(
biome.formatContent(actual, { filePath: 'index.ts' }).content,
biome.formatContent(expect, { filePath: 'index.ts' }).content
);
});
it('should rewrite complex exmaple to valid string', async () => {
const actual = await rewriteObservableSubscribeTofirstValueFrom(
'index.ts',
`codeFlowCallbackService
.authenticatedCallbackWithCode('some-url4', config, [config])
.subscribe({
error: (err: any) => {
expect(resetSilentRenewRunningSpy).toHaveBeenCalled();
expect(resetCodeFlowInProgressSpy).toHaveBeenCalled();
expect(stopPeriodicallTokenCheckSpy).toHaveBeenCalled();
expect(err).toBeTruthy();
},
next: (abc) => {
expect(abc).toBeTruthy();
},
complete () {
expect.fail('complete')
}
});`
);
const expect = `
try {
const abc = await firstValueFrom(codeFlowCallbackService.authenticatedCallbackWithCode('some-url4', config, [config]));
expect(abc).toBeTruthy();
} catch (err: any) {
if (err instanceof EmptyError) {
expect.fail('complete')
} else {
expect(resetSilentRenewRunningSpy).toHaveBeenCalled();
expect(resetCodeFlowInProgressSpy).toHaveBeenCalled();
expect(stopPeriodicallTokenCheckSpy).toHaveBeenCalled();
expect(err).toBeTruthy();
}
}
`;
const biome = await Biome.create({
distribution: Distribution.NODE,
});
assert.equal(
biome.formatContent(actual, { filePath: 'index.ts' }).content,
biome.formatContent(expect, { filePath: 'index.ts' }).content
);
});
});

173
scripts/code-transform.ts Normal file
View File

@@ -0,0 +1,173 @@
import assert from 'node:assert/strict';
import fsp from 'node:fs/promises';
import {
type ArrowFunctionExpression,
// biome-ignore lint/suspicious/noShadowRestrictedNames: <explanation>
type Function,
type MagicString,
type Statement,
parseSync,
} from 'oxc-parser';
import { walk } from 'oxc-walker';
function sourceTextFromNode(
context: { magicString?: MagicString },
node: { start: number; end: number }
): string {
const magicString = context.magicString;
assert(magicString, 'magicString should be defined');
const start = node.start;
const end = node.end;
return magicString.getSourceText(start, end);
}
export async function rewriteObservableSubscribeTofirstValueFrom(
filename: string,
content?: string
) {
const code = content ?? (await fsp.readFile(filename, 'utf-8'));
const parsedResult = parseSync('index.ts', code);
const magicString = parsedResult.magicString;
walk(parsedResult, {
leave(node, _, context) {
const transformExprs = <T extends Statement[]>(
children: T
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
): T => {
const newChildren: T = [] as any as T;
for (const child of children) {
if (
child.type === 'ExpressionStatement' &&
child.expression.type === 'CallExpression' &&
child.expression.callee.type === 'StaticMemberExpression' &&
child.expression.callee.property.name === 'subscribe'
) {
let next: ArrowFunctionExpression | Function | undefined;
let error: ArrowFunctionExpression | Function | undefined;
let complete: ArrowFunctionExpression | Function | undefined;
if (child.expression.arguments[0]?.type === 'ObjectExpression') {
const obj = child.expression.arguments[0];
for (const prop of obj.properties) {
if (
prop.type === 'ObjectProperty' &&
prop.key.type === 'Identifier' &&
(prop.value.type === 'FunctionExpression' ||
prop.value.type === 'ArrowFunctionExpression')
) {
if (prop.key.name === 'next') {
next = prop.value;
} else if (prop.key.name === 'error') {
error = prop.value;
} else if (prop.key.name === 'complete') {
complete = prop.value;
}
}
}
} else if (
child.expression.arguments.find(
(arg) =>
arg.type === 'FunctionExpression' ||
arg.type === 'ArrowFunctionExpression'
)
) {
const args: Array<
Function | ArrowFunctionExpression | undefined
> = child.expression.arguments.map((arg) =>
arg.type === 'FunctionExpression' ||
arg.type === 'ArrowFunctionExpression'
? arg
: undefined
);
next = args[0];
error = args[1];
complete = args[2];
}
let newContent = `await firstValueFrom(${sourceTextFromNode(context, child.expression.callee.object)});`;
if (next) {
const nextParam =
next?.params?.items?.[0]?.type === 'FormalParameter'
? sourceTextFromNode(context, next.params.items[0])
: undefined;
if (nextParam) {
newContent = `const ${nextParam} = ${newContent}`;
}
newContent += (next.body?.statements || [])
.map((s) => sourceTextFromNode(context, s))
.join('\n');
}
if (error || complete) {
const errorParam =
error?.params?.items?.[0]?.type === 'FormalParameter' &&
error.params.items[0].pattern.type === 'Identifier'
? sourceTextFromNode(context, error.params.items[0])
: 'err';
const errorParamName =
error?.params?.items?.[0]?.type === 'FormalParameter' &&
error.params.items[0].pattern.type === 'Identifier'
? error.params.items[0].pattern.name
: 'err';
let errorBody = '';
if (error) {
errorBody += (error.body?.statements || [])
.map((s) => sourceTextFromNode(context, s))
.join('\n');
}
if (complete) {
const completBody = `if (${errorParamName} instanceof EmptyError) { ${(complete.body?.statements || []).map((s) => sourceTextFromNode(context, s)).join('\n')}}`;
if (errorBody) {
errorBody = `${completBody} else { ${errorBody} }`;
} else {
errorBody = completBody;
}
}
newContent = `try { ${newContent} } catch (${errorParam}) { ${errorBody} }`;
}
const newNodes = parseSync('index.html', newContent).program.body;
magicString.remove(child.start, child.end);
magicString.appendLeft(child.start, newContent);
newChildren.push(...newNodes);
} else {
newChildren.push(child as any);
}
}
return newChildren;
};
if ('body' in node && Array.isArray(node.body) && node.body.length > 0) {
const children = node.body;
node.body = transformExprs(children as any)!;
} else if (
'body' in node &&
node.body &&
'type' in node.body &&
node.body.type === 'FunctionBody'
) {
const children = node.body.statements;
node.body.statements = transformExprs(children)!;
}
},
});
const result = magicString.toString();
return result;
}
export async function rewriteAllObservableSubscribeTofirstValueFrom(
pattern: string | string[]
) {
const files = fsp.glob(pattern);
for await (const file of files) {
const result = await rewriteObservableSubscribeTofirstValueFrom(file);
await fsp.writeFile(file, result, 'utf-8');
}
}

View File

@@ -0,0 +1,41 @@
import { InjectionToken, inject } from '@outposts/injection-js';
import type { AnyRouter } from '@tanstack/react-router';
import type { AuthFeature } from '../../features';
import { AbstractRouter, ROUTER_ABS_PATH_PATTERN } from '../../router';
export type TanStackRouter = AnyRouter;
export const TANSTACK_ROUTER = new InjectionToken<TanStackRouter>(
'TANSTACK_ROUTER'
);
export class TanStackRouterAdapter implements AbstractRouter<string> {
private router = inject(TANSTACK_ROUTER);
navigateByUrl(url: string): void {
this.router.navigate({
href: ROUTER_ABS_PATH_PATTERN.test(url) ? url : `/${url}`,
});
}
getCurrentNavigation() {
return {
extractedUrl: this.router.state.location.href,
};
}
}
export function withTanstackRouter(router: TanStackRouter): AuthFeature {
return {
ɵproviders: [
{
provide: TANSTACK_ROUTER,
useValue: router,
},
{
provide: AbstractRouter,
useClass: TanStackRouterAdapter,
},
],
};
}

View File

@@ -0,0 +1,41 @@
import { InjectionToken, inject } from '@outposts/injection-js';
import type { AnyRouter } from '@tanstack/solid-router';
import type { AuthFeature } from '../../features';
import { AbstractRouter, ROUTER_ABS_PATH_PATTERN } from '../../router';
export type TanStackRouter = AnyRouter;
export const TANSTACK_ROUTER = new InjectionToken<TanStackRouter>(
'TANSTACK_ROUTER'
);
export class TanStackRouterAdapter implements AbstractRouter<string> {
private router = inject(TANSTACK_ROUTER);
navigateByUrl(url: string): void {
this.router.navigate({
href: ROUTER_ABS_PATH_PATTERN.test(url) ? url : `/${url}`,
});
}
getCurrentNavigation() {
return {
extractedUrl: this.router.state.location.href,
};
}
}
export function withTanstackRouter(router: TanStackRouter): AuthFeature {
return {
ɵproviders: [
{
provide: TANSTACK_ROUTER,
useValue: router,
},
{
provide: AbstractRouter,
useClass: TanStackRouterAdapter,
},
],
};
}

View File

@@ -0,0 +1,47 @@
import type { InjectionToken, Injector, Type } from '@outposts/injection-js';
import {
type PropsWithChildren,
createContext,
createElement,
useContext,
useMemo,
} from 'react';
import { OidcSecurityService } from '../../oidc.security.service';
export const InjectorContextVoidInjector: Injector = {
get: <T>(_token: Type<T> | InjectionToken<T>, _notFoundValue?: T): T => {
throw new Error('Please wrap with a InjectorContext.Provider first');
},
};
export const InjectorContext = createContext<Injector>(
InjectorContextVoidInjector
);
export function InjectorProvider({
injector,
...props
}: PropsWithChildren<{ injector: Injector }>) {
return createElement(InjectorContext, {
...props,
value: injector,
});
}
export function useInjector() {
return useContext(InjectorContext);
}
export function useOidcClient() {
const injector = useInjector();
const oidcSecurityService = useMemo(
() => injector.get(OidcSecurityService),
[injector]
);
return {
injector,
oidcSecurityService,
};
}

View File

@@ -0,0 +1,43 @@
import type { InjectionToken, Injector, Type } from '@outposts/injection-js';
import {
type FlowProps,
createContext,
createMemo,
mergeProps,
splitProps,
useContext,
} from 'solid-js';
import { OidcSecurityService } from '../../oidc.security.service';
export const InjectorContextVoidInjector: Injector = {
get: <T>(_token: Type<T> | InjectionToken<T>, _notFoundValue?: T): T => {
throw new Error('Please wrap with a InjectorContext.Provider first');
},
};
export const InjectorContext = createContext<Injector>(
InjectorContextVoidInjector
);
export function InjectorProvider(props: FlowProps<{ injector: Injector }>) {
const [local, others] = splitProps(props, ['injector']);
const providerProps = mergeProps(others, { value: local.injector });
return InjectorContext.Provider(providerProps);
}
export function useInjector() {
return useContext(InjectorContext);
}
export function useOidcClient() {
const injector = useInjector();
const oidcSecurityService = createMemo(() =>
injector.get(OidcSecurityService)
);
return {
injector,
oidcSecurityService: oidcSecurityService(),
};
}

View File

@@ -1,35 +1,25 @@
import { TestBed } from '@/testing';
import { import {
HttpHeaders, type DefaultHttpTestingController,
provideHttpClient, HTTP_CLIENT_TEST_CONTROLLER,
withInterceptorsFromDi,
} from '@angular/common/http';
import {
HttpTestingController,
provideHttpClientTesting, provideHttpClientTesting,
} from '@angular/common/http/testing'; } from '@/testing/http';
import { TestBed, waitForAsync } from '@angular/core/testing'; import { HttpHeaders } from '@ngify/http';
import { ReplaySubject, firstValueFrom, share } from 'rxjs';
import { DataService } from './data.service'; import { DataService } from './data.service';
import { HttpBaseService } from './http-base.service'; import { HttpBaseService } from './http-base.service';
describe('Data Service', () => { describe('Data Service', () => {
let dataService: DataService; let dataService: DataService;
let httpMock: HttpTestingController; let httpMock: DefaultHttpTestingController;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [],
providers: [ providers: [DataService, HttpBaseService, provideHttpClientTesting()],
DataService,
HttpBaseService,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
}); });
});
beforeEach(() => {
dataService = TestBed.inject(DataService); dataService = TestBed.inject(DataService);
httpMock = TestBed.inject(HttpTestingController); httpMock = TestBed.inject(HTTP_CLIENT_TEST_CONTROLLER);
}); });
it('should create', () => { it('should create', () => {
@@ -37,14 +27,20 @@ describe('Data Service', () => {
}); });
describe('get', () => { describe('get', () => {
it('get call sets the accept header', waitForAsync(() => { it('get call sets the accept header', async () => {
const url = 'testurl'; const url = 'testurl';
dataService const test$ = dataService.get(url, { configId: 'configId1' }).pipe(
.get(url, { configId: 'configId1' }) share({
.subscribe((data: unknown) => { connector: () => new ReplaySubject(1),
expect(data).toBe('bodyData'); resetOnError: false,
}); resetOnComplete: false,
resetOnRefCountZero: false,
})
);
test$.subscribe();
const req = httpMock.expectOne(url); const req = httpMock.expectOne(url);
expect(req.request.method).toBe('GET'); expect(req.request.method).toBe('GET');
@@ -52,37 +48,55 @@ describe('Data Service', () => {
req.flush('bodyData'); req.flush('bodyData');
httpMock.verify(); const data = await firstValueFrom(test$);
})); expect(data).toBe('bodyData');
it('get call with token the accept header and the token', waitForAsync(() => { httpMock.verify();
});
it('get call with token the accept header and the token', async () => {
const url = 'testurl'; const url = 'testurl';
const token = 'token'; const token = 'token';
dataService const test$ = dataService.get(url, { configId: 'configId1' }, token).pipe(
.get(url, { configId: 'configId1' }, token) share({
.subscribe((data: unknown) => { connector: () => new ReplaySubject(1),
expect(data).toBe('bodyData'); resetOnError: false,
}); resetOnComplete: false,
resetOnRefCountZero: false,
})
);
test$.subscribe();
const req = httpMock.expectOne(url); const req = httpMock.expectOne(url);
expect(req.request.method).toBe('GET'); expect(req.request.method).toBe('GET');
expect(req.request.headers.get('Accept')).toBe('application/json'); expect(req.request.headers.get('Accept')).toBe('application/json');
expect(req.request.headers.get('Authorization')).toBe('Bearer ' + token); expect(req.request.headers.get('Authorization')).toBe(`Bearer ${token}`);
req.flush('bodyData'); req.flush('bodyData');
httpMock.verify(); const data = await firstValueFrom(test$);
})); expect(data).toBe('bodyData');
it('call without ngsw-bypass param by default', waitForAsync(() => { httpMock.verify();
});
it('call without ngsw-bypass param by default', async () => {
const url = 'testurl'; const url = 'testurl';
dataService const test$ = dataService.get(url, { configId: 'configId1' }).pipe(
.get(url, { configId: 'configId1' }) share({
.subscribe((data: unknown) => { connector: () => new ReplaySubject(1),
expect(data).toBe('bodyData'); resetOnError: false,
}); resetOnComplete: false,
resetOnRefCountZero: false,
})
);
test$.subscribe();
const req = httpMock.expectOne(url); const req = httpMock.expectOne(url);
expect(req.request.method).toBe('GET'); expect(req.request.method).toBe('GET');
@@ -91,36 +105,67 @@ describe('Data Service', () => {
req.flush('bodyData'); req.flush('bodyData');
httpMock.verify(); const data = await firstValueFrom(test$);
})); expect(data).toBe('bodyData');
it('call with ngsw-bypass param', waitForAsync(() => { httpMock.verify();
});
it('call with ngsw-bypass param', async () => {
const url = 'testurl'; const url = 'testurl';
dataService const test$ = dataService
.get(url, { configId: 'configId1', ngswBypass: true }) .get(url, {
.subscribe((data: unknown) => { configId: 'configId1',
expect(data).toBe('bodyData'); ngswBypass: true,
}); })
const req = httpMock.expectOne(url + '?ngsw-bypass='); .pipe(
share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: false,
})
);
test$.subscribe();
const req = httpMock.expectOne(`${url}?ngsw-bypass=`);
expect(req.request.method).toBe('GET'); expect(req.request.method).toBe('GET');
expect(req.request.headers.get('Accept')).toBe('application/json'); expect(req.request.headers.get('Accept')).toBe('application/json');
expect(req.request.params.get('ngsw-bypass')).toBe('');
// @TODO: should make a issue to ngify
// expect(req.request.params.('ngsw-bypass')).toBe('');
expect(req.request.params.has('ngsw-bypass')).toBeTruthy();
req.flush('bodyData'); req.flush('bodyData');
const data = await firstValueFrom(test$);
expect(data).toBe('bodyData');
httpMock.verify(); httpMock.verify();
})); });
}); });
describe('post', () => { describe('post', () => {
it('call sets the accept header when no other params given', waitForAsync(() => { it('call sets the accept header when no other params given', async () => {
const url = 'testurl'; const url = 'testurl';
dataService const test$ = dataService
.post(url, { some: 'thing' }, { configId: 'configId1' }) .post(url, { some: 'thing' }, { configId: 'configId1' })
.subscribe(); .pipe(
share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: false,
})
);
test$.subscribe();
const req = httpMock.expectOne(url); const req = httpMock.expectOne(url);
expect(req.request.method).toBe('POST'); expect(req.request.method).toBe('POST');
@@ -128,18 +173,30 @@ describe('Data Service', () => {
req.flush('bodyData'); req.flush('bodyData');
httpMock.verify(); await firstValueFrom(test$);
}));
it('call sets custom headers ONLY (No ACCEPT header) when custom headers are given', waitForAsync(() => { await httpMock.verify();
});
it('call sets custom headers ONLY (No ACCEPT header) when custom headers are given', async () => {
const url = 'testurl'; const url = 'testurl';
let headers = new HttpHeaders(); let headers = new HttpHeaders();
headers = headers.set('X-MyHeader', 'Genesis'); headers = headers.set('X-MyHeader', 'Genesis');
dataService const test$ = dataService
.post(url, { some: 'thing' }, { configId: 'configId1' }, headers) .post(url, { some: 'thing' }, { configId: 'configId1' }, headers)
.subscribe(); .pipe(
share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: false,
})
);
test$.subscribe();
const req = httpMock.expectOne(url); const req = httpMock.expectOne(url);
expect(req.request.method).toBe('POST'); expect(req.request.method).toBe('POST');
@@ -148,15 +205,27 @@ describe('Data Service', () => {
req.flush('bodyData'); req.flush('bodyData');
httpMock.verify(); await firstValueFrom(test$);
}));
it('call without ngsw-bypass param by default', waitForAsync(() => { httpMock.verify();
});
it('call without ngsw-bypass param by default', async () => {
const url = 'testurl'; const url = 'testurl';
dataService const test$ = dataService
.post(url, { some: 'thing' }, { configId: 'configId1' }) .post(url, { some: 'thing' }, { configId: 'configId1' })
.subscribe(); .pipe(
share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: false,
})
);
test$.subscribe();
const req = httpMock.expectOne(url); const req = httpMock.expectOne(url);
expect(req.request.method).toBe('POST'); expect(req.request.method).toBe('POST');
@@ -165,28 +234,46 @@ describe('Data Service', () => {
req.flush('bodyData'); req.flush('bodyData');
httpMock.verify(); await firstValueFrom(test$);
}));
it('call with ngsw-bypass param', waitForAsync(() => { httpMock.verify();
});
it('call with ngsw-bypass param', async () => {
const url = 'testurl'; const url = 'testurl';
dataService const test$ = dataService
.post( .post(
url, url,
{ some: 'thing' }, { some: 'thing' },
{ configId: 'configId1', ngswBypass: true } { configId: 'configId1', ngswBypass: true }
) )
.subscribe(); .pipe(
const req = httpMock.expectOne(url + '?ngsw-bypass='); share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: false,
})
);
test$.subscribe();
const req = httpMock.expectOne(`${url}?ngsw-bypass=`);
expect(req.request.method).toBe('POST'); expect(req.request.method).toBe('POST');
expect(req.request.headers.get('Accept')).toBe('application/json'); expect(req.request.headers.get('Accept')).toBe('application/json');
expect(req.request.params.get('ngsw-bypass')).toBe('');
// @TODO: should make a issue to ngify
// expect(req.request.params.('ngsw-bypass')).toBe('');
expect(req.request.params.has('ngsw-bypass')).toBeTruthy();
req.flush('bodyData'); req.flush('bodyData');
await firstValueFrom(test$);
httpMock.verify(); httpMock.verify();
})); });
}); });
}); });

View File

@@ -1,7 +1,8 @@
import { HttpHeaders, HttpParams } from '@ngify/http'; import { HttpHeaders } from '@ngify/http';
import { Injectable, inject } from 'injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import { Observable } from 'rxjs'; import type { Observable } from 'rxjs';
import { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
import { HttpParams } from '../http';
import { HttpBaseService } from './http-base.service'; import { HttpBaseService } from './http-base.service';
const NGSW_CUSTOM_PARAM = 'ngsw-bypass'; const NGSW_CUSTOM_PARAM = 'ngsw-bypass';
@@ -41,10 +42,10 @@ export class DataService {
headers = headers.set('Accept', 'application/json'); headers = headers.set('Accept', 'application/json');
if (!!token) { if (token) {
headers = headers.set( headers = headers.set(
'Authorization', 'Authorization',
'Bearer ' + decodeURIComponent(token) `Bearer ${decodeURIComponent(token)}`
); );
} }

View File

@@ -1,22 +1,29 @@
import { HttpClient } from '@ngify/http'; import { Injectable, inject } from '@outposts/injection-js';
import { Injectable, inject } from 'injection-js'; import type { Observable } from 'rxjs';
import { Observable } from 'rxjs'; import { HTTP_CLIENT, type HttpHeaders, type HttpParams } from '../http';
@Injectable() @Injectable()
export class HttpBaseService { export class HttpBaseService {
constructor() {} private readonly http = inject(HTTP_CLIENT);
private readonly http = inject(HttpClient); get<T>(
url: string,
get<T>(url: string, params?: { [key: string]: unknown }): Observable<T> { options: { headers?: HttpHeaders; params?: HttpParams } = {}
return this.http.get<T>(url, params); ): Observable<T> {
return this.http.get<T>(url, {
...options,
params: options.params?.toNgify(),
});
} }
post<T>( post<T>(
url: string, url: string,
body: unknown, body: unknown,
params?: { [key: string]: unknown } options: { headers?: HttpHeaders; params?: HttpParams } = {}
): Observable<T> { ): Observable<T> {
return this.http.post<T>(url, body, params); return this.http.post<T>(url, body as any, {
...options,
params: options.params?.toNgify(),
});
} }
} }

View File

@@ -1,4 +1,4 @@
import { PassedInitialConfig, createStaticLoader } from './auth-config'; import { type PassedInitialConfig, createStaticLoader } from './auth-config';
describe('AuthConfig', () => { describe('AuthConfig', () => {
describe('createStaticLoader', () => { describe('createStaticLoader', () => {

View File

@@ -1,9 +1,9 @@
import { InjectionToken, Provider } from 'injection-js'; import { InjectionToken, type Provider } from '@outposts/injection-js';
import { import {
StsConfigLoader, type StsConfigLoader,
StsConfigStaticLoader, StsConfigStaticLoader,
} from './config/loader/config-loader'; } from './config/loader/config-loader';
import { OpenIdConfiguration } from './config/openid-configuration'; import type { OpenIdConfiguration } from './config/openid-configuration';
export interface PassedInitialConfig { export interface PassedInitialConfig {
config?: OpenIdConfiguration | OpenIdConfiguration[]; config?: OpenIdConfiguration | OpenIdConfiguration[];

View File

@@ -1,13 +1,14 @@
import { TestBed } from '@angular/core/testing'; import { TestBed, mockImplementationWhenArgsEqual } from '@/testing';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { mockProvider } from '../../test/auto-mock'; import { vi } from 'vitest';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
import { EventTypes } from '../public-events/event-types'; import { EventTypes } from '../public-events/event-types';
import { PublicEventsService } from '../public-events/public-events.service'; import { PublicEventsService } from '../public-events/public-events.service';
import { StoragePersistenceService } from '../storage/storage-persistence.service'; import { StoragePersistenceService } from '../storage/storage-persistence.service';
import { mockProvider } from '../testing/mock';
import { PlatformProvider } from '../utils/platform-provider/platform.provider'; import { PlatformProvider } from '../utils/platform-provider/platform.provider';
import { TokenValidationService } from '../validation/token-validation.service'; import { TokenValidationService } from '../validation/token-validation.service';
import { ValidationResult } from '../validation/validation-result'; import type { ValidationResult } from '../validation/validation-result';
import { AuthStateService } from './auth-state.service'; import { AuthStateService } from './auth-state.service';
describe('Auth State Service', () => { describe('Auth State Service', () => {
@@ -27,9 +28,6 @@ describe('Auth State Service', () => {
mockProvider(StoragePersistenceService), mockProvider(StoragePersistenceService),
], ],
}); });
});
beforeEach(() => {
authStateService = TestBed.inject(AuthStateService); authStateService = TestBed.inject(AuthStateService);
storagePersistenceService = TestBed.inject(StoragePersistenceService); storagePersistenceService = TestBed.inject(StoragePersistenceService);
eventsService = TestBed.inject(PublicEventsService); eventsService = TestBed.inject(PublicEventsService);
@@ -40,13 +38,13 @@ describe('Auth State Service', () => {
expect(authStateService).toBeTruthy(); expect(authStateService).toBeTruthy();
}); });
it('public authorize$ is observable$', () => { it('authorize$ is observable$', () => {
expect(authStateService.authenticated$).toEqual(jasmine.any(Observable)); expect(authStateService.authenticated$).toBeInstanceOf(Observable);
}); });
describe('setAuthorizedAndFireEvent', () => { describe('setAuthorizedAndFireEvent', () => {
it('throws correct event with single config', () => { it('throws correct event with single config', () => {
const spy = spyOn( const spy = vi.spyOn(
(authStateService as any).authenticatedInternal$, (authStateService as any).authenticatedInternal$,
'next' 'next'
); );
@@ -55,7 +53,7 @@ describe('Auth State Service', () => {
{ configId: 'configId1' }, { configId: 'configId1' },
]); ]);
expect(spy).toHaveBeenCalledOnceWith({ expect(spy).toHaveBeenCalledExactlyOnceWith({
isAuthenticated: true, isAuthenticated: true,
allConfigsAuthenticated: [ allConfigsAuthenticated: [
{ configId: 'configId1', isAuthenticated: true }, { configId: 'configId1', isAuthenticated: true },
@@ -64,7 +62,7 @@ describe('Auth State Service', () => {
}); });
it('throws correct event with multiple configs', () => { it('throws correct event with multiple configs', () => {
const spy = spyOn( const spy = vi.spyOn(
(authStateService as any).authenticatedInternal$, (authStateService as any).authenticatedInternal$,
'next' 'next'
); );
@@ -74,7 +72,7 @@ describe('Auth State Service', () => {
{ configId: 'configId2' }, { configId: 'configId2' },
]); ]);
expect(spy).toHaveBeenCalledOnceWith({ expect(spy).toHaveBeenCalledExactlyOnceWith({
isAuthenticated: false, isAuthenticated: false,
allConfigsAuthenticated: [ allConfigsAuthenticated: [
{ configId: 'configId1', isAuthenticated: false }, { configId: 'configId1', isAuthenticated: false },
@@ -86,26 +84,34 @@ describe('Auth State Service', () => {
it('throws correct event with multiple configs, one is authenticated', () => { it('throws correct event with multiple configs, one is authenticated', () => {
const allConfigs = [{ configId: 'configId1' }, { configId: 'configId2' }]; const allConfigs = [{ configId: 'configId1' }, { configId: 'configId2' }];
spyOn(storagePersistenceService, 'getAccessToken') mockImplementationWhenArgsEqual(
.withArgs(allConfigs[0]) mockImplementationWhenArgsEqual(
.and.returnValue('someAccessToken') vi.spyOn(storagePersistenceService, 'getAccessToken'),
.withArgs(allConfigs[1]) [allConfigs[0]!],
.and.returnValue(''); () => 'someAccessToken'
),
[allConfigs[1]!],
() => ''
);
spyOn(storagePersistenceService, 'getIdToken') mockImplementationWhenArgsEqual(
.withArgs(allConfigs[0]) mockImplementationWhenArgsEqual(
.and.returnValue('someIdToken') vi.spyOn(storagePersistenceService, 'getIdToken'),
.withArgs(allConfigs[1]) [allConfigs[0]!],
.and.returnValue(''); () => 'someIdToken'
),
[allConfigs[1]!],
() => ''
);
const spy = spyOn( const spy = vi.spyOn(
(authStateService as any).authenticatedInternal$, (authStateService as any).authenticatedInternal$,
'next' 'next'
); );
authStateService.setAuthenticatedAndFireEvent(allConfigs); authStateService.setAuthenticatedAndFireEvent(allConfigs);
expect(spy).toHaveBeenCalledOnceWith({ expect(spy).toHaveBeenCalledExactlyOnceWith({
isAuthenticated: false, isAuthenticated: false,
allConfigsAuthenticated: [ allConfigsAuthenticated: [
{ configId: 'configId1', isAuthenticated: true }, { configId: 'configId1', isAuthenticated: true },
@@ -117,17 +123,20 @@ describe('Auth State Service', () => {
describe('setUnauthorizedAndFireEvent', () => { describe('setUnauthorizedAndFireEvent', () => {
it('persist AuthState In Storage', () => { it('persist AuthState In Storage', () => {
const spy = spyOn(storagePersistenceService, 'resetAuthStateInStorage'); const spy = vi.spyOn(
storagePersistenceService,
'resetAuthStateInStorage'
);
authStateService.setUnauthenticatedAndFireEvent( authStateService.setUnauthenticatedAndFireEvent(
{ configId: 'configId1' }, { configId: 'configId1' },
[{ configId: 'configId1' }] [{ configId: 'configId1' }]
); );
expect(spy).toHaveBeenCalledOnceWith({ configId: 'configId1' }); expect(spy).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' });
}); });
it('throws correct event with single config', () => { it('throws correct event with single config', () => {
const spy = spyOn( const spy = vi.spyOn(
(authStateService as any).authenticatedInternal$, (authStateService as any).authenticatedInternal$,
'next' 'next'
); );
@@ -137,7 +146,7 @@ describe('Auth State Service', () => {
[{ configId: 'configId1' }] [{ configId: 'configId1' }]
); );
expect(spy).toHaveBeenCalledOnceWith({ expect(spy).toHaveBeenCalledExactlyOnceWith({
isAuthenticated: false, isAuthenticated: false,
allConfigsAuthenticated: [ allConfigsAuthenticated: [
{ configId: 'configId1', isAuthenticated: false }, { configId: 'configId1', isAuthenticated: false },
@@ -146,7 +155,7 @@ describe('Auth State Service', () => {
}); });
it('throws correct event with multiple configs', () => { it('throws correct event with multiple configs', () => {
const spy = spyOn( const spy = vi.spyOn(
(authStateService as any).authenticatedInternal$, (authStateService as any).authenticatedInternal$,
'next' 'next'
); );
@@ -156,7 +165,7 @@ describe('Auth State Service', () => {
[{ configId: 'configId1' }, { configId: 'configId2' }] [{ configId: 'configId1' }, { configId: 'configId2' }]
); );
expect(spy).toHaveBeenCalledOnceWith({ expect(spy).toHaveBeenCalledExactlyOnceWith({
isAuthenticated: false, isAuthenticated: false,
allConfigsAuthenticated: [ allConfigsAuthenticated: [
{ configId: 'configId1', isAuthenticated: false }, { configId: 'configId1', isAuthenticated: false },
@@ -166,19 +175,27 @@ describe('Auth State Service', () => {
}); });
it('throws correct event with multiple configs, one is authenticated', () => { it('throws correct event with multiple configs, one is authenticated', () => {
spyOn(storagePersistenceService, 'getAccessToken') mockImplementationWhenArgsEqual(
.withArgs({ configId: 'configId1' }) mockImplementationWhenArgsEqual(
.and.returnValue('someAccessToken') vi.spyOn(storagePersistenceService, 'getAccessToken'),
.withArgs({ configId: 'configId2' }) [{ configId: 'configId1' }],
.and.returnValue(''); () => 'someAccessToken'
),
[{ configId: 'configId2' }],
() => ''
);
spyOn(storagePersistenceService, 'getIdToken') mockImplementationWhenArgsEqual(
.withArgs({ configId: 'configId1' }) mockImplementationWhenArgsEqual(
.and.returnValue('someIdToken') vi.spyOn(storagePersistenceService, 'getIdToken'),
.withArgs({ configId: 'configId2' }) [{ configId: 'configId1' }],
.and.returnValue(''); () => 'someIdToken'
),
[{ configId: 'configId2' }],
() => ''
);
const spy = spyOn( const spy = vi.spyOn(
(authStateService as any).authenticatedInternal$, (authStateService as any).authenticatedInternal$,
'next' 'next'
); );
@@ -188,7 +205,7 @@ describe('Auth State Service', () => {
[{ configId: 'configId1' }, { configId: 'configId2' }] [{ configId: 'configId1' }, { configId: 'configId2' }]
); );
expect(spy).toHaveBeenCalledOnceWith({ expect(spy).toHaveBeenCalledExactlyOnceWith({
isAuthenticated: false, isAuthenticated: false,
allConfigsAuthenticated: [ allConfigsAuthenticated: [
{ configId: 'configId1', isAuthenticated: true }, { configId: 'configId1', isAuthenticated: true },
@@ -200,24 +217,27 @@ describe('Auth State Service', () => {
describe('updateAndPublishAuthState', () => { describe('updateAndPublishAuthState', () => {
it('calls eventsService', () => { it('calls eventsService', () => {
spyOn(eventsService, 'fireEvent'); vi.spyOn(eventsService, 'fireEvent');
authStateService.updateAndPublishAuthState({ const arg = {
isAuthenticated: false, isAuthenticated: false,
isRenewProcess: false, isRenewProcess: false,
validationResult: {} as ValidationResult, validationResult: {} as ValidationResult,
}); };
expect(eventsService.fireEvent).toHaveBeenCalledOnceWith( authStateService.updateAndPublishAuthState(arg);
expect(eventsService.fireEvent).toHaveBeenCalledOnce();
expect(eventsService.fireEvent).toHaveBeenCalledExactlyOnceWith(
EventTypes.NewAuthenticationResult, EventTypes.NewAuthenticationResult,
jasmine.any(Object) arg
); );
}); });
}); });
describe('setAuthorizationData', () => { describe('setAuthorizationData', () => {
it('stores accessToken', () => { it('stores accessToken', () => {
const spy = spyOn(storagePersistenceService, 'write'); const spy = vi.spyOn(storagePersistenceService, 'write');
const authResult = { const authResult = {
id_token: 'idtoken', id_token: 'idtoken',
access_token: 'accesstoken', access_token: 'accesstoken',
@@ -237,18 +257,19 @@ describe('Auth State Service', () => {
[{ configId: 'configId1' }] [{ configId: 'configId1' }]
); );
expect(spy).toHaveBeenCalledTimes(2); expect(spy).toHaveBeenCalledTimes(2);
expect(spy.calls.allArgs()).toEqual([ expect(spy.mock.calls).toEqual([
['authzData', 'accesstoken', { configId: 'configId1' }], ['authzData', 'accesstoken', { configId: 'configId1' }],
[ [
'access_token_expires_at', 'access_token_expires_at',
jasmine.any(Number), expect.any(Number),
{ configId: 'configId1' }, { configId: 'configId1' },
], ],
]); ]);
}); });
it('does not crash and store accessToken when authResult is null', () => { it('does not crash and store accessToken when authResult is null', () => {
const spy = spyOn(storagePersistenceService, 'write'); const spy = vi.spyOn(storagePersistenceService, 'write');
// biome-ignore lint/suspicious/noEvolvingTypes: <explanation>
const authResult = null; const authResult = null;
authStateService.setAuthorizationData( authStateService.setAuthorizationData(
@@ -262,7 +283,7 @@ describe('Auth State Service', () => {
}); });
it('calls setAuthenticatedAndFireEvent() method', () => { it('calls setAuthenticatedAndFireEvent() method', () => {
const spy = spyOn(authStateService, 'setAuthenticatedAndFireEvent'); const spy = vi.spyOn(authStateService, 'setAuthenticatedAndFireEvent');
const authResult = { const authResult = {
id_token: 'idtoken', id_token: 'idtoken',
access_token: 'accesstoken', access_token: 'accesstoken',
@@ -288,28 +309,29 @@ describe('Auth State Service', () => {
describe('getAccessToken', () => { describe('getAccessToken', () => {
it('isAuthorized is false returns null', () => { it('isAuthorized is false returns null', () => {
spyOn(storagePersistenceService, 'getAccessToken').and.returnValue(''); vi.spyOn(storagePersistenceService, 'getAccessToken').mockReturnValue('');
spyOn(storagePersistenceService, 'getIdToken').and.returnValue(''); vi.spyOn(storagePersistenceService, 'getIdToken').mockReturnValue('');
const result = authStateService.getAccessToken({ configId: 'configId1' }); const result = authStateService.getAccessToken({ configId: 'configId1' });
expect(result).toBe(''); expect(result).toBe('');
}); });
it('returns false if storagePersistenceService returns something falsy but authorized', () => { it('returns false if storagePersistenceService returns something falsy but authorized', () => {
spyOn(authStateService, 'isAuthenticated').and.returnValue(true); vi.spyOn(authStateService, 'isAuthenticated').mockReturnValue(true);
spyOn(storagePersistenceService, 'getAccessToken').and.returnValue(''); vi.spyOn(storagePersistenceService, 'getAccessToken').mockReturnValue('');
const result = authStateService.getAccessToken({ configId: 'configId1' }); const result = authStateService.getAccessToken({ configId: 'configId1' });
expect(result).toBe(''); expect(result).toBe('');
}); });
it('isAuthorized is true returns decodeURIComponent(token)', () => { it('isAuthorized is true returns decodeURIComponent(token)', () => {
spyOn(storagePersistenceService, 'getAccessToken').and.returnValue( vi.spyOn(storagePersistenceService, 'getAccessToken').mockReturnValue(
'HenloLegger' 'HenloLegger'
); );
spyOn(storagePersistenceService, 'getIdToken').and.returnValue( vi.spyOn(storagePersistenceService, 'getIdToken').mockReturnValue(
'HenloFuriend' 'HenloFuriend'
); );
const result = authStateService.getAccessToken({ configId: 'configId1' }); const result = authStateService.getAccessToken({ configId: 'configId1' });
expect(result).toBe(decodeURIComponent('HenloLegger')); expect(result).toBe(decodeURIComponent('HenloLegger'));
@@ -318,12 +340,14 @@ describe('Auth State Service', () => {
describe('getAuthenticationResult', () => { describe('getAuthenticationResult', () => {
it('isAuthorized is false returns null', () => { it('isAuthorized is false returns null', () => {
spyOn(storagePersistenceService, 'getAccessToken').and.returnValue(''); vi.spyOn(storagePersistenceService, 'getAccessToken').mockReturnValue('');
spyOn(storagePersistenceService, 'getIdToken').and.returnValue(''); vi.spyOn(storagePersistenceService, 'getIdToken').mockReturnValue('');
spyOn(storagePersistenceService, 'getAuthenticationResult') mockImplementationWhenArgsEqual(
.withArgs({ configId: 'configId1' }) vi.spyOn(storagePersistenceService, 'getAuthenticationResult'),
.and.returnValue({}); [{ configId: 'configId1' }],
() => ({})
);
const result = authStateService.getAuthenticationResult({ const result = authStateService.getAuthenticationResult({
configId: 'configId1', configId: 'configId1',
@@ -333,10 +357,13 @@ describe('Auth State Service', () => {
}); });
it('returns false if storagePersistenceService returns something falsy but authorized', () => { it('returns false if storagePersistenceService returns something falsy but authorized', () => {
spyOn(authStateService, 'isAuthenticated').and.returnValue(true); vi.spyOn(authStateService, 'isAuthenticated').mockReturnValue(true);
spyOn(storagePersistenceService, 'getAuthenticationResult')
.withArgs({ configId: 'configId1' }) mockImplementationWhenArgsEqual(
.and.returnValue({}); vi.spyOn(storagePersistenceService, 'getAuthenticationResult'),
[{ configId: 'configId1' }],
() => ({})
);
const result = authStateService.getAuthenticationResult({ const result = authStateService.getAuthenticationResult({
configId: 'configId1', configId: 'configId1',
@@ -346,15 +373,18 @@ describe('Auth State Service', () => {
}); });
it('isAuthorized is true returns object', () => { it('isAuthorized is true returns object', () => {
spyOn(storagePersistenceService, 'getAccessToken').and.returnValue( vi.spyOn(storagePersistenceService, 'getAccessToken').mockReturnValue(
'HenloLegger' 'HenloLegger'
); );
spyOn(storagePersistenceService, 'getIdToken').and.returnValue( vi.spyOn(storagePersistenceService, 'getIdToken').mockReturnValue(
'HenloFuriend' 'HenloFuriend'
); );
spyOn(storagePersistenceService, 'getAuthenticationResult')
.withArgs({ configId: 'configId1' }) mockImplementationWhenArgsEqual(
.and.returnValue({ scope: 'HenloFuriend' }); vi.spyOn(storagePersistenceService, 'getAuthenticationResult'),
[{ configId: 'configId1' }],
() => ({ scope: 'HenloFuriend' })
);
const result = authStateService.getAuthenticationResult({ const result = authStateService.getAuthenticationResult({
configId: 'configId1', configId: 'configId1',
@@ -366,18 +396,18 @@ describe('Auth State Service', () => {
describe('getIdToken', () => { describe('getIdToken', () => {
it('isAuthorized is false returns null', () => { it('isAuthorized is false returns null', () => {
spyOn(storagePersistenceService, 'getAccessToken').and.returnValue(''); vi.spyOn(storagePersistenceService, 'getAccessToken').mockReturnValue('');
spyOn(storagePersistenceService, 'getIdToken').and.returnValue(''); vi.spyOn(storagePersistenceService, 'getIdToken').mockReturnValue('');
const result = authStateService.getIdToken({ configId: 'configId1' }); const result = authStateService.getIdToken({ configId: 'configId1' });
expect(result).toBe(''); expect(result).toBe('');
}); });
it('isAuthorized is true returns decodeURIComponent(token)', () => { it('isAuthorized is true returns decodeURIComponent(token)', () => {
spyOn(storagePersistenceService, 'getAccessToken').and.returnValue( vi.spyOn(storagePersistenceService, 'getAccessToken').mockReturnValue(
'HenloLegger' 'HenloLegger'
); );
spyOn(storagePersistenceService, 'getIdToken').and.returnValue( vi.spyOn(storagePersistenceService, 'getIdToken').mockReturnValue(
'HenloFuriend' 'HenloFuriend'
); );
const result = authStateService.getIdToken({ configId: 'configId1' }); const result = authStateService.getIdToken({ configId: 'configId1' });
@@ -388,8 +418,8 @@ describe('Auth State Service', () => {
describe('getRefreshToken', () => { describe('getRefreshToken', () => {
it('isAuthorized is false returns null', () => { it('isAuthorized is false returns null', () => {
spyOn(storagePersistenceService, 'getAccessToken').and.returnValue(''); vi.spyOn(storagePersistenceService, 'getAccessToken').mockReturnValue('');
spyOn(storagePersistenceService, 'getIdToken').and.returnValue(''); vi.spyOn(storagePersistenceService, 'getIdToken').mockReturnValue('');
const result = authStateService.getRefreshToken({ const result = authStateService.getRefreshToken({
configId: 'configId1', configId: 'configId1',
}); });
@@ -398,13 +428,13 @@ describe('Auth State Service', () => {
}); });
it('isAuthorized is true returns decodeURIComponent(token)', () => { it('isAuthorized is true returns decodeURIComponent(token)', () => {
spyOn(storagePersistenceService, 'getAccessToken').and.returnValue( vi.spyOn(storagePersistenceService, 'getAccessToken').mockReturnValue(
'HenloLegger' 'HenloLegger'
); );
spyOn(storagePersistenceService, 'getIdToken').and.returnValue( vi.spyOn(storagePersistenceService, 'getIdToken').mockReturnValue(
'HenloFuriend' 'HenloFuriend'
); );
spyOn(storagePersistenceService, 'getRefreshToken').and.returnValue( vi.spyOn(storagePersistenceService, 'getRefreshToken').mockReturnValue(
'HenloRefreshLegger' 'HenloRefreshLegger'
); );
const result = authStateService.getRefreshToken({ const result = authStateService.getRefreshToken({
@@ -417,105 +447,105 @@ describe('Auth State Service', () => {
describe('areAuthStorageTokensValid', () => { describe('areAuthStorageTokensValid', () => {
it('isAuthorized is false returns false', () => { it('isAuthorized is false returns false', () => {
spyOn(storagePersistenceService, 'getAccessToken').and.returnValue(''); vi.spyOn(storagePersistenceService, 'getAccessToken').mockReturnValue('');
spyOn(storagePersistenceService, 'getIdToken').and.returnValue(''); vi.spyOn(storagePersistenceService, 'getIdToken').mockReturnValue('');
const result = authStateService.areAuthStorageTokensValid({ const result = authStateService.areAuthStorageTokensValid({
configId: 'configId1', configId: 'configId1',
}); });
expect(result).toBeFalse(); expect(result).toBeFalsy();
}); });
it('isAuthorized is true and id_token is expired returns true', () => { it('isAuthorized is true and id_token is expired returns true', () => {
spyOn(storagePersistenceService, 'getAccessToken').and.returnValue( vi.spyOn(storagePersistenceService, 'getAccessToken').mockReturnValue(
'HenloLegger' 'HenloLegger'
); );
spyOn(storagePersistenceService, 'getIdToken').and.returnValue( vi.spyOn(storagePersistenceService, 'getIdToken').mockReturnValue(
'HenloFuriend' 'HenloFuriend'
); );
spyOn( vi.spyOn(
authStateService as any, authStateService as any,
'hasIdTokenExpiredAndRenewCheckIsEnabled' 'hasIdTokenExpiredAndRenewCheckIsEnabled'
).and.returnValue(true); ).mockReturnValue(true);
spyOn( vi.spyOn(
authStateService as any, authStateService as any,
'hasAccessTokenExpiredIfExpiryExists' 'hasAccessTokenExpiredIfExpiryExists'
).and.returnValue(false); ).mockReturnValue(false);
const result = authStateService.areAuthStorageTokensValid({ const result = authStateService.areAuthStorageTokensValid({
configId: 'configId1', configId: 'configId1',
}); });
expect(result).toBeFalse(); expect(result).toBeFalsy();
}); });
it('isAuthorized is true and access_token is expired returns true', () => { it('isAuthorized is true and access_token is expired returns true', () => {
spyOn(storagePersistenceService, 'getAccessToken').and.returnValue( vi.spyOn(storagePersistenceService, 'getAccessToken').mockReturnValue(
'HenloLegger' 'HenloLegger'
); );
spyOn(storagePersistenceService, 'getIdToken').and.returnValue( vi.spyOn(storagePersistenceService, 'getIdToken').mockReturnValue(
'HenloFuriend' 'HenloFuriend'
); );
spyOn( vi.spyOn(
authStateService as any, authStateService as any,
'hasIdTokenExpiredAndRenewCheckIsEnabled' 'hasIdTokenExpiredAndRenewCheckIsEnabled'
).and.returnValue(false); ).mockReturnValue(false);
spyOn( vi.spyOn(
authStateService as any, authStateService as any,
'hasAccessTokenExpiredIfExpiryExists' 'hasAccessTokenExpiredIfExpiryExists'
).and.returnValue(true); ).mockReturnValue(true);
const result = authStateService.areAuthStorageTokensValid({ const result = authStateService.areAuthStorageTokensValid({
configId: 'configId1', configId: 'configId1',
}); });
expect(result).toBeFalse(); expect(result).toBeFalsy();
}); });
it('isAuthorized is true and id_token is not expired returns true', () => { it('isAuthorized is true and id_token is not expired returns true', () => {
spyOn(storagePersistenceService, 'getAccessToken').and.returnValue( vi.spyOn(storagePersistenceService, 'getAccessToken').mockReturnValue(
'HenloLegger' 'HenloLegger'
); );
spyOn(storagePersistenceService, 'getIdToken').and.returnValue( vi.spyOn(storagePersistenceService, 'getIdToken').mockReturnValue(
'HenloFuriend' 'HenloFuriend'
); );
spyOn( vi.spyOn(
authStateService as any, authStateService as any,
'hasIdTokenExpiredAndRenewCheckIsEnabled' 'hasIdTokenExpiredAndRenewCheckIsEnabled'
).and.returnValue(false); ).mockReturnValue(false);
spyOn( vi.spyOn(
authStateService as any, authStateService as any,
'hasAccessTokenExpiredIfExpiryExists' 'hasAccessTokenExpiredIfExpiryExists'
).and.returnValue(false); ).mockReturnValue(false);
const result = authStateService.areAuthStorageTokensValid({ const result = authStateService.areAuthStorageTokensValid({
configId: 'configId1', configId: 'configId1',
}); });
expect(result).toBeTrue(); expect(result).toBeTruthy();
}); });
it('authState is AuthorizedState.Authorized and id_token is not expired fires event', () => { it('authState is AuthorizedState.Authorized and id_token is not expired fires event', () => {
spyOn(storagePersistenceService, 'getAccessToken').and.returnValue( vi.spyOn(storagePersistenceService, 'getAccessToken').mockReturnValue(
'HenloLegger' 'HenloLegger'
); );
spyOn(storagePersistenceService, 'getIdToken').and.returnValue( vi.spyOn(storagePersistenceService, 'getIdToken').mockReturnValue(
'HenloFuriend' 'HenloFuriend'
); );
spyOn( vi.spyOn(
authStateService as any, authStateService as any,
'hasIdTokenExpiredAndRenewCheckIsEnabled' 'hasIdTokenExpiredAndRenewCheckIsEnabled'
).and.returnValue(false); ).mockReturnValue(false);
spyOn( vi.spyOn(
authStateService as any, authStateService as any,
'hasAccessTokenExpiredIfExpiryExists' 'hasAccessTokenExpiredIfExpiryExists'
).and.returnValue(false); ).mockReturnValue(false);
const result = authStateService.areAuthStorageTokensValid({ const result = authStateService.areAuthStorageTokensValid({
configId: 'configId1', configId: 'configId1',
}); });
expect(result).toBeTrue(); expect(result).toBeTruthy();
}); });
}); });
@@ -527,56 +557,65 @@ describe('Auth State Service', () => {
triggerRefreshWhenIdTokenExpired: true, triggerRefreshWhenIdTokenExpired: true,
}; };
spyOn(storagePersistenceService, 'getIdToken') mockImplementationWhenArgsEqual(
.withArgs(config) vi.spyOn(storagePersistenceService, 'getIdToken'),
.and.returnValue('idToken'); [config],
const spy = spyOn( () => 'idToken'
tokenValidationService, );
'hasIdTokenExpired' const spy = vi
).and.callFake((_a, _b) => true); .spyOn(tokenValidationService, 'hasIdTokenExpired')
.mockImplementation((_a, _b) => true);
authStateService.hasIdTokenExpiredAndRenewCheckIsEnabled(config); authStateService.hasIdTokenExpiredAndRenewCheckIsEnabled(config);
expect(spy).toHaveBeenCalledOnceWith('idToken', config, 30); expect(spy).toHaveBeenCalledExactlyOnceWith('idToken', config, 30);
}); });
it('fires event if idToken is expired', () => { it('fires event if idToken is expired', () => {
spyOn(tokenValidationService, 'hasIdTokenExpired').and.callFake( vi.spyOn(tokenValidationService, 'hasIdTokenExpired').mockImplementation(
(_a, _b) => true (_a, _b) => true
); );
const spy = spyOn(eventsService, 'fireEvent'); const spy = vi.spyOn(eventsService, 'fireEvent');
const config = { const config = {
configId: 'configId1', configId: 'configId1',
renewTimeBeforeTokenExpiresInSeconds: 30, renewTimeBeforeTokenExpiresInSeconds: 30,
triggerRefreshWhenIdTokenExpired: true, triggerRefreshWhenIdTokenExpired: true,
}; };
spyOn(storagePersistenceService, 'read') mockImplementationWhenArgsEqual(
.withArgs('authnResult', config) vi.spyOn(storagePersistenceService, 'read'),
.and.returnValue('idToken'); ['authnResult', config],
() => 'idToken'
);
const result = const result =
authStateService.hasIdTokenExpiredAndRenewCheckIsEnabled(config); authStateService.hasIdTokenExpiredAndRenewCheckIsEnabled(config);
expect(result).toBe(true); expect(result).toBe(true);
expect(spy).toHaveBeenCalledOnceWith(EventTypes.IdTokenExpired, true); expect(spy).toHaveBeenCalledExactlyOnceWith(
EventTypes.IdTokenExpired,
true
);
}); });
it('does NOT fire event if idToken is NOT expired', () => { it('does NOT fire event if idToken is NOT expired', () => {
spyOn(tokenValidationService, 'hasIdTokenExpired').and.callFake( vi.spyOn(tokenValidationService, 'hasIdTokenExpired').mockImplementation(
(_a, _b) => false (_a, _b) => false
); );
const spy = spyOn(eventsService, 'fireEvent'); const spy = vi.spyOn(eventsService, 'fireEvent');
const config = { const config = {
configId: 'configId1', configId: 'configId1',
renewTimeBeforeTokenExpiresInSeconds: 30, renewTimeBeforeTokenExpiresInSeconds: 30,
}; };
spyOn(storagePersistenceService, 'read') mockImplementationWhenArgsEqual(
.withArgs('authnResult', config) vi.spyOn(storagePersistenceService, 'read'),
.and.returnValue('idToken'); ['authnResult', config],
() => 'idToken'
);
const result = const result =
authStateService.hasIdTokenExpiredAndRenewCheckIsEnabled(config); authStateService.hasIdTokenExpiredAndRenewCheckIsEnabled(config);
@@ -595,41 +634,45 @@ describe('Auth State Service', () => {
renewTimeBeforeTokenExpiresInSeconds: 5, renewTimeBeforeTokenExpiresInSeconds: 5,
}; };
spyOn(storagePersistenceService, 'read') mockImplementationWhenArgsEqual(
.withArgs('access_token_expires_at', config) vi.spyOn(storagePersistenceService, 'read'),
.and.returnValue(date); ['access_token_expires_at', config],
const spy = spyOn( () => date
tokenValidationService, );
'validateAccessTokenNotExpired' const spy = vi
).and.returnValue(validateAccessTokenNotExpiredResult); .spyOn(tokenValidationService, 'validateAccessTokenNotExpired')
.mockReturnValue(validateAccessTokenNotExpiredResult);
const result = const result =
authStateService.hasAccessTokenExpiredIfExpiryExists(config); authStateService.hasAccessTokenExpiredIfExpiryExists(config);
expect(spy).toHaveBeenCalledOnceWith(date, config, 5); expect(spy).toHaveBeenCalledExactlyOnceWith(date, config, 5);
expect(result).toEqual(expectedResult); expect(result).toEqual(expectedResult);
}); });
it('throws event when token is expired', () => { it('throws event when token is expired', () => {
const validateAccessTokenNotExpiredResult = false; const validateAccessTokenNotExpiredResult = false;
const expectedResult = !validateAccessTokenNotExpiredResult; const expectedResult = !validateAccessTokenNotExpiredResult;
// spyOn(configurationProvider, 'getOpenIDConfiguration').and.returnValue({ renewTimeBeforeTokenExpiresInSeconds: 5 }); // vi.spyOn(configurationProvider, 'getOpenIDConfiguration').mockReturnValue({ renewTimeBeforeTokenExpiresInSeconds: 5 });
const date = new Date(new Date().toUTCString()); const date = new Date(new Date().toUTCString());
const config = { const config = {
configId: 'configId1', configId: 'configId1',
renewTimeBeforeTokenExpiresInSeconds: 5, renewTimeBeforeTokenExpiresInSeconds: 5,
}; };
spyOn(eventsService, 'fireEvent'); vi.spyOn(eventsService, 'fireEvent');
spyOn(storagePersistenceService, 'read') mockImplementationWhenArgsEqual(
.withArgs('access_token_expires_at', config) vi.spyOn(storagePersistenceService, 'read'),
.and.returnValue(date); ['access_token_expires_at', config],
spyOn( () => date
);
vi.spyOn(
tokenValidationService, tokenValidationService,
'validateAccessTokenNotExpired' 'validateAccessTokenNotExpired'
).and.returnValue(validateAccessTokenNotExpiredResult); ).mockReturnValue(validateAccessTokenNotExpiredResult);
authStateService.hasAccessTokenExpiredIfExpiryExists(config); authStateService.hasAccessTokenExpiredIfExpiryExists(config);
expect(eventsService.fireEvent).toHaveBeenCalledOnceWith( expect(eventsService.fireEvent).toHaveBeenCalledExactlyOnceWith(
EventTypes.TokenExpired, EventTypes.TokenExpired,
expectedResult expectedResult
); );

View File

@@ -1,15 +1,15 @@
import { Injectable, inject } from 'injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import { BehaviorSubject, Observable, throwError } from 'rxjs'; import { BehaviorSubject, type Observable, throwError } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators'; import { distinctUntilChanged } from 'rxjs/operators';
import { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
import { AuthResult } from '../flows/callback-context'; import type { AuthResult } from '../flows/callback-context';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
import { EventTypes } from '../public-events/event-types'; import { EventTypes } from '../public-events/event-types';
import { PublicEventsService } from '../public-events/public-events.service'; import { PublicEventsService } from '../public-events/public-events.service';
import { StoragePersistenceService } from '../storage/storage-persistence.service'; import { StoragePersistenceService } from '../storage/storage-persistence.service';
import { TokenValidationService } from '../validation/token-validation.service'; import { TokenValidationService } from '../validation/token-validation.service';
import { AuthenticatedResult } from './auth-result'; import type { AuthenticatedResult } from './auth-result';
import { AuthStateResult } from './auth-state'; import type { AuthStateResult } from './auth-state';
const DEFAULT_AUTHRESULT = { const DEFAULT_AUTHRESULT = {
isAuthenticated: false, isAuthenticated: false,
@@ -257,9 +257,8 @@ export class AuthStateService {
private decodeURIComponentSafely(token: string): string { private decodeURIComponentSafely(token: string): string {
if (token) { if (token) {
return decodeURIComponent(token); return decodeURIComponent(token);
} else {
return '';
} }
return '';
} }
private persistAccessTokenExpirationTime( private persistAccessTokenExpirationTime(
@@ -293,7 +292,7 @@ export class AuthStateService {
}; };
} }
return this.checkAllConfigsIfTheyAreAuthenticated(allConfigs); return this.checkallConfigsIfTheyAreAuthenticated(allConfigs);
} }
private composeUnAuthenticatedResult( private composeUnAuthenticatedResult(
@@ -310,10 +309,10 @@ export class AuthStateService {
}; };
} }
return this.checkAllConfigsIfTheyAreAuthenticated(allConfigs); return this.checkallConfigsIfTheyAreAuthenticated(allConfigs);
} }
private checkAllConfigsIfTheyAreAuthenticated( private checkallConfigsIfTheyAreAuthenticated(
allConfigs: OpenIdConfiguration[] allConfigs: OpenIdConfiguration[]
): AuthenticatedResult { ): AuthenticatedResult {
const allConfigsAuthenticated = allConfigs.map((config) => ({ const allConfigsAuthenticated = allConfigs.map((config) => ({

View File

@@ -1,4 +1,4 @@
import { ValidationResult } from '../validation/validation-result'; import type { ValidationResult } from '../validation/validation-result';
export interface AuthStateResult { export interface AuthStateResult {
isAuthenticated: boolean; isAuthenticated: boolean;

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,15 @@
import { inject, Injectable } from 'injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import { forkJoin, Observable, of, throwError } from 'rxjs'; import { type Observable, forkJoin, of, throwError } from 'rxjs';
import { catchError, map, switchMap, tap } from 'rxjs/operators'; import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { AutoLoginService } from '../auto-login/auto-login.service'; import { AutoLoginService } from '../auto-login/auto-login.service';
import { CallbackService } from '../callback/callback.service'; import { CallbackService } from '../callback/callback.service';
import { PeriodicallyTokenCheckService } from '../callback/periodically-token-check.service'; import { PeriodicallyTokenCheckService } from '../callback/periodically-token-check.service';
import { RefreshSessionService } from '../callback/refresh-session.service'; import { RefreshSessionService } from '../callback/refresh-session.service';
import { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
import { CheckSessionService } from '../iframe/check-session.service'; import { CheckSessionService } from '../iframe/check-session.service';
import { SilentRenewService } from '../iframe/silent-renew.service'; import { SilentRenewService } from '../iframe/silent-renew.service';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
import { LoginResponse } from '../login/login-response'; import type { LoginResponse } from '../login/login-response';
import { PopUpService } from '../login/popup/popup.service'; import { PopUpService } from '../login/popup/popup.service';
import { EventTypes } from '../public-events/event-types'; import { EventTypes } from '../public-events/event-types';
import { PublicEventsService } from '../public-events/public-events.service'; import { PublicEventsService } from '../public-events/public-events.service';
@@ -57,7 +57,7 @@ export class CheckAuthService {
const stateParamFromUrl = const stateParamFromUrl =
this.currentUrlService.getStateParamFromCurrentUrl(url); this.currentUrlService.getStateParamFromCurrentUrl(url);
return Boolean(stateParamFromUrl) return stateParamFromUrl
? this.getConfigurationWithUrlState([configuration], stateParamFromUrl) ? this.getConfigurationWithUrlState([configuration], stateParamFromUrl)
: configuration; : configuration;
} }

View File

@@ -1,61 +0,0 @@
import { TestBed, waitForAsync } from '@angular/core/testing';
import { of } from 'rxjs';
import { mockProvider } from '../test/auto-mock';
import { PASSED_CONFIG } from './auth-config';
import { AuthModule } from './auth.module';
import { ConfigurationService } from './config/config.service';
import {
StsConfigHttpLoader,
StsConfigLoader,
StsConfigStaticLoader,
} from './config/loader/config-loader';
describe('AuthModule', () => {
describe('APP_CONFIG', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [AuthModule.forRoot({ config: { authority: 'something' } })],
providers: [mockProvider(ConfigurationService)],
}).compileComponents();
}));
it('should create', () => {
expect(AuthModule).toBeDefined();
expect(AuthModule.forRoot({})).toBeDefined();
});
it('should provide config', () => {
const config = TestBed.inject(PASSED_CONFIG);
expect(config).toEqual({ config: { authority: 'something' } });
});
it('should create StsConfigStaticLoader if config is passed', () => {
const configLoader = TestBed.inject(StsConfigLoader);
expect(configLoader instanceof StsConfigStaticLoader).toBe(true);
});
});
describe('StsConfigHttpLoader', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
AuthModule.forRoot({
loader: {
provide: StsConfigLoader,
useFactory: () => new StsConfigHttpLoader(of({})),
},
}),
],
providers: [mockProvider(ConfigurationService)],
}).compileComponents();
}));
it('should create StsConfigStaticLoader if config is passed', () => {
const configLoader = TestBed.inject(StsConfigLoader);
expect(configLoader instanceof StsConfigHttpLoader).toBe(true);
});
});
});

View File

@@ -1,25 +0,0 @@
import { CommonModule } from '@angular/common';
import {
provideHttpClient,
withInterceptorsFromDi,
} from '@ngify/http';
import { ModuleWithProviders, NgModule } from 'injection-js';
import { PassedInitialConfig } from './auth-config';
import { _provideAuth } from './provide-auth';
@NgModule({
declarations: [],
exports: [],
imports: [CommonModule],
providers: [provideHttpClient(withInterceptorsFromDi())],
})
export class AuthModule {
static forRoot(
passedConfig: PassedInitialConfig
): ModuleWithProviders<AuthModule> {
return {
ngModule: AuthModule,
providers: [..._provideAuth(passedConfig)],
};
}
}

View File

@@ -1,17 +1,17 @@
import { TestBed, waitForAsync } from '@angular/core/testing'; import { type MockRouter, TestBed, mockRouterProvider } from '@/testing';
import { import {
ActivatedRouteSnapshot, AbstractRouter,
Router, type ActivatedRouteSnapshot,
RouterStateSnapshot, type RouterStateSnapshot,
} from '@angular/router'; } from 'oidc-client-rx';
import { RouterTestingModule } from '@angular/router/testing'; import { firstValueFrom, of } from 'rxjs';
import { of } from 'rxjs'; import { vi } from 'vitest';
import { mockProvider } from '../../test/auto-mock';
import { AuthStateService } from '../auth-state/auth-state.service'; import { AuthStateService } from '../auth-state/auth-state.service';
import { CheckAuthService } from '../auth-state/check-auth.service'; import { CheckAuthService } from '../auth-state/check-auth.service';
import { ConfigurationService } from '../config/config.service'; import { ConfigurationService } from '../config/config.service';
import { LoginService } from '../login/login.service'; import { LoginService } from '../login/login.service';
import { StoragePersistenceService } from '../storage/storage-persistence.service'; import { StoragePersistenceService } from '../storage/storage-persistence.service';
import { mockProvider } from '../testing/mock';
import { import {
AutoLoginPartialRoutesGuard, AutoLoginPartialRoutesGuard,
autoLoginPartialRoutesGuard, autoLoginPartialRoutesGuard,
@@ -19,11 +19,13 @@ import {
} from './auto-login-partial-routes.guard'; } from './auto-login-partial-routes.guard';
import { AutoLoginService } from './auto-login.service'; import { AutoLoginService } from './auto-login.service';
describe(`AutoLoginPartialRoutesGuard`, () => { describe('AutoLoginPartialRoutesGuard', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [RouterTestingModule], imports: [],
providers: [ providers: [
AutoLoginPartialRoutesGuard,
mockRouterProvider(),
AutoLoginService, AutoLoginService,
mockProvider(AuthStateService), mockProvider(AuthStateService),
mockProvider(LoginService), mockProvider(LoginService),
@@ -41,7 +43,7 @@ describe(`AutoLoginPartialRoutesGuard`, () => {
let storagePersistenceService: StoragePersistenceService; let storagePersistenceService: StoragePersistenceService;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let autoLoginService: AutoLoginService; let autoLoginService: AutoLoginService;
let router: Router; let router: MockRouter;
beforeEach(() => { beforeEach(() => {
authStateService = TestBed.inject(AuthStateService); authStateService = TestBed.inject(AuthStateService);
@@ -49,15 +51,16 @@ describe(`AutoLoginPartialRoutesGuard`, () => {
storagePersistenceService = TestBed.inject(StoragePersistenceService); storagePersistenceService = TestBed.inject(StoragePersistenceService);
configurationService = TestBed.inject(ConfigurationService); configurationService = TestBed.inject(ConfigurationService);
spyOn(configurationService, 'getOpenIDConfiguration').and.returnValue( vi.spyOn(configurationService, 'getOpenIDConfiguration').mockReturnValue(
of({ configId: 'configId1' }) of({ configId: 'configId1' })
); );
guard = TestBed.inject(AutoLoginPartialRoutesGuard); guard = TestBed.inject(AutoLoginPartialRoutesGuard);
autoLoginService = TestBed.inject(AutoLoginService); autoLoginService = TestBed.inject(AutoLoginService);
router = TestBed.inject(Router); router = TestBed.inject(AbstractRouter);
}); });
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => { afterEach(() => {
storagePersistenceService.clear({}); storagePersistenceService.clear({});
}); });
@@ -67,281 +70,263 @@ describe(`AutoLoginPartialRoutesGuard`, () => {
}); });
describe('canActivate', () => { describe('canActivate', () => {
it('should save current route and call `login` if not authenticated already', waitForAsync(() => { it('should save current route and call `login` if not authenticated already', async () => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false false
); );
const checkSavedRedirectRouteAndNavigateSpy = spyOn( const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService, autoLoginService,
'checkSavedRedirectRouteAndNavigate' 'checkSavedRedirectRouteAndNavigate'
); );
const saveRedirectRouteSpy = spyOn( const saveRedirectRouteSpy = vi.spyOn(
autoLoginService, autoLoginService,
'saveRedirectRoute' 'saveRedirectRoute'
); );
const loginSpy = spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
guard await firstValueFrom(
.canActivate( guard.canActivate(
{} as ActivatedRouteSnapshot, {} as ActivatedRouteSnapshot,
{ url: 'some-url1' } as RouterStateSnapshot { url: 'some-url1' } as RouterStateSnapshot
) )
.subscribe(() => { );
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith( expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
'some-url1' 'some-url1'
); );
expect(loginSpy).toHaveBeenCalledOnceWith({ expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
configId: 'configId1', configId: 'configId1',
}); });
expect( expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
checkSavedRedirectRouteAndNavigateSpy });
).not.toHaveBeenCalled();
});
}));
it('should save current route and call `login` if not authenticated already and add custom params', waitForAsync(() => { it('should save current route and call `login` if not authenticated already and add custom params', async () => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false false
); );
const checkSavedRedirectRouteAndNavigateSpy = spyOn( const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService, autoLoginService,
'checkSavedRedirectRouteAndNavigate' 'checkSavedRedirectRouteAndNavigate'
); );
const saveRedirectRouteSpy = spyOn( const saveRedirectRouteSpy = vi.spyOn(
autoLoginService, autoLoginService,
'saveRedirectRoute' 'saveRedirectRoute'
); );
const loginSpy = spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
guard await firstValueFrom(
.canActivate( guard.canActivate(
{ data: { custom: 'param' } } as unknown as ActivatedRouteSnapshot, { data: { custom: 'param' } } as unknown as ActivatedRouteSnapshot,
{ url: 'some-url1' } as RouterStateSnapshot { url: 'some-url1' } as RouterStateSnapshot
) )
.subscribe(() => { );
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith( expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
'some-url1' 'some-url1'
); );
expect(loginSpy).toHaveBeenCalledOnceWith( expect(loginSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
{ customParams: { custom: 'param' } } { customParams: { custom: 'param' } }
); );
expect( expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
checkSavedRedirectRouteAndNavigateSpy });
).not.toHaveBeenCalled();
});
}));
it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', waitForAsync(() => { it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
true true
); );
const checkSavedRedirectRouteAndNavigateSpy = spyOn( const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService, autoLoginService,
'checkSavedRedirectRouteAndNavigate' 'checkSavedRedirectRouteAndNavigate'
); );
const saveRedirectRouteSpy = spyOn( const saveRedirectRouteSpy = vi.spyOn(
autoLoginService, autoLoginService,
'saveRedirectRoute' 'saveRedirectRoute'
); );
const loginSpy = spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
guard await firstValueFrom(
.canActivate( guard.canActivate(
{} as ActivatedRouteSnapshot, {} as ActivatedRouteSnapshot,
{ url: 'some-url1' } as RouterStateSnapshot { url: 'some-url1' } as RouterStateSnapshot
) )
.subscribe(() => { );
expect(saveRedirectRouteSpy).not.toHaveBeenCalled(); expect(saveRedirectRouteSpy).not.toHaveBeenCalled();
expect(loginSpy).not.toHaveBeenCalled(); expect(loginSpy).not.toHaveBeenCalled();
expect( expect(
checkSavedRedirectRouteAndNavigateSpy checkSavedRedirectRouteAndNavigateSpy
).toHaveBeenCalledOnceWith({ configId: 'configId1' }); ).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' });
}); });
}));
}); });
describe('canActivateChild', () => { describe('canActivateChild', () => {
it('should save current route and call `login` if not authenticated already', waitForAsync(() => { it('should save current route and call `login` if not authenticated already', async () => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false false
); );
const checkSavedRedirectRouteAndNavigateSpy = spyOn( const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService, autoLoginService,
'checkSavedRedirectRouteAndNavigate' 'checkSavedRedirectRouteAndNavigate'
); );
const saveRedirectRouteSpy = spyOn( const saveRedirectRouteSpy = vi.spyOn(
autoLoginService, autoLoginService,
'saveRedirectRoute' 'saveRedirectRoute'
); );
const loginSpy = spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
guard await firstValueFrom(
.canActivateChild( guard.canActivateChild(
{} as ActivatedRouteSnapshot, {} as ActivatedRouteSnapshot,
{ url: 'some-url1' } as RouterStateSnapshot { url: 'some-url1' } as RouterStateSnapshot
) )
.subscribe(() => { );
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith( expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
'some-url1' 'some-url1'
); );
expect(loginSpy).toHaveBeenCalledOnceWith({ expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
configId: 'configId1', configId: 'configId1',
}); });
expect( expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
checkSavedRedirectRouteAndNavigateSpy });
).not.toHaveBeenCalled();
});
}));
it('should save current route and call `login` if not authenticated already with custom params', waitForAsync(() => { it('should save current route and call `login` if not authenticated already with custom params', async () => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false false
); );
const checkSavedRedirectRouteAndNavigateSpy = spyOn( const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService, autoLoginService,
'checkSavedRedirectRouteAndNavigate' 'checkSavedRedirectRouteAndNavigate'
); );
const saveRedirectRouteSpy = spyOn( const saveRedirectRouteSpy = vi.spyOn(
autoLoginService, autoLoginService,
'saveRedirectRoute' 'saveRedirectRoute'
); );
const loginSpy = spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
guard await firstValueFrom(
.canActivateChild( guard.canActivateChild(
{ data: { custom: 'param' } } as unknown as ActivatedRouteSnapshot, { data: { custom: 'param' } } as unknown as ActivatedRouteSnapshot,
{ url: 'some-url1' } as RouterStateSnapshot { url: 'some-url1' } as RouterStateSnapshot
) )
.subscribe(() => { );
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith( expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
'some-url1' 'some-url1'
); );
expect(loginSpy).toHaveBeenCalledOnceWith( expect(loginSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
{ customParams: { custom: 'param' } } { customParams: { custom: 'param' } }
); );
expect( expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
checkSavedRedirectRouteAndNavigateSpy });
).not.toHaveBeenCalled();
});
}));
it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', waitForAsync(() => { it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
true true
); );
const checkSavedRedirectRouteAndNavigateSpy = spyOn( const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService, autoLoginService,
'checkSavedRedirectRouteAndNavigate' 'checkSavedRedirectRouteAndNavigate'
); );
const saveRedirectRouteSpy = spyOn( const saveRedirectRouteSpy = vi.spyOn(
autoLoginService, autoLoginService,
'saveRedirectRoute' 'saveRedirectRoute'
); );
const loginSpy = spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
guard await firstValueFrom(
.canActivateChild( guard.canActivateChild(
{} as ActivatedRouteSnapshot, {} as ActivatedRouteSnapshot,
{ url: 'some-url1' } as RouterStateSnapshot { url: 'some-url1' } as RouterStateSnapshot
) )
.subscribe(() => { );
expect(saveRedirectRouteSpy).not.toHaveBeenCalled(); expect(saveRedirectRouteSpy).not.toHaveBeenCalled();
expect(loginSpy).not.toHaveBeenCalled(); expect(loginSpy).not.toHaveBeenCalled();
expect( expect(
checkSavedRedirectRouteAndNavigateSpy checkSavedRedirectRouteAndNavigateSpy
).toHaveBeenCalledOnceWith({ configId: 'configId1' }); ).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' });
}); });
}));
}); });
describe('canLoad', () => { describe('canLoad', () => {
it('should save current route (empty) and call `login` if not authenticated already', waitForAsync(() => { it('should save current route (empty) and call `login` if not authenticated already', async () => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false false
); );
const checkSavedRedirectRouteAndNavigateSpy = spyOn( const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService, autoLoginService,
'checkSavedRedirectRouteAndNavigate' 'checkSavedRedirectRouteAndNavigate'
); );
const saveRedirectRouteSpy = spyOn( const saveRedirectRouteSpy = vi.spyOn(
autoLoginService, autoLoginService,
'saveRedirectRoute' 'saveRedirectRoute'
); );
const loginSpy = spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
guard.canLoad().subscribe(() => { await firstValueFrom(guard.canLoad());
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith( expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
'' ''
); );
expect(loginSpy).toHaveBeenCalledOnceWith({ configId: 'configId1' }); expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); configId: 'configId1',
}); });
})); expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
});
it('should save current route (with router extractedUrl) and call `login` if not authenticated already', waitForAsync(() => { it('should save current route (with router extractedUrl) and call `login` if not authenticated already', async () => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false false
); );
const checkSavedRedirectRouteAndNavigateSpy = spyOn( const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService, autoLoginService,
'checkSavedRedirectRouteAndNavigate' 'checkSavedRedirectRouteAndNavigate'
); );
const saveRedirectRouteSpy = spyOn( const saveRedirectRouteSpy = vi.spyOn(
autoLoginService, autoLoginService,
'saveRedirectRoute' 'saveRedirectRoute'
); );
const loginSpy = spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
spyOn(router, 'getCurrentNavigation').and.returnValue({ vi.spyOn(router, 'getCurrentNavigation').mockReturnValue({
extractedUrl: router.parseUrl( extractedUrl: router.parseUrl(
'some-url12/with/some-param?queryParam=true' 'some-url12/with/some-param?queryParam=true'
), ),
extras: {},
id: 1,
initialUrl: router.parseUrl(''),
previousNavigation: null,
trigger: 'imperative',
}); });
guard.canLoad().subscribe(() => { await firstValueFrom(guard.canLoad());
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith( expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
'some-url12/with/some-param?queryParam=true' 'some-url12/with/some-param?queryParam=true'
); );
expect(loginSpy).toHaveBeenCalledOnceWith({ configId: 'configId1' }); expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); configId: 'configId1',
}); });
})); expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
});
it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', waitForAsync(() => { it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
true true
); );
const checkSavedRedirectRouteAndNavigateSpy = spyOn( const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService, autoLoginService,
'checkSavedRedirectRouteAndNavigate' 'checkSavedRedirectRouteAndNavigate'
); );
const saveRedirectRouteSpy = spyOn( const saveRedirectRouteSpy = vi.spyOn(
autoLoginService, autoLoginService,
'saveRedirectRoute' 'saveRedirectRoute'
); );
const loginSpy = spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
guard.canLoad().subscribe(() => { await firstValueFrom(guard.canLoad());
expect(saveRedirectRouteSpy).not.toHaveBeenCalled(); expect(saveRedirectRouteSpy).not.toHaveBeenCalled();
expect(loginSpy).not.toHaveBeenCalled(); expect(loginSpy).not.toHaveBeenCalled();
expect( expect(
checkSavedRedirectRouteAndNavigateSpy checkSavedRedirectRouteAndNavigateSpy
).toHaveBeenCalledOnceWith({ configId: 'configId1' }); ).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' });
}); });
}));
}); });
}); });
@@ -352,7 +337,7 @@ describe(`AutoLoginPartialRoutesGuard`, () => {
let storagePersistenceService: StoragePersistenceService; let storagePersistenceService: StoragePersistenceService;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
let autoLoginService: AutoLoginService; let autoLoginService: AutoLoginService;
let router: Router; let router: MockRouter;
beforeEach(() => { beforeEach(() => {
authStateService = TestBed.inject(AuthStateService); authStateService = TestBed.inject(AuthStateService);
@@ -360,98 +345,97 @@ describe(`AutoLoginPartialRoutesGuard`, () => {
storagePersistenceService = TestBed.inject(StoragePersistenceService); storagePersistenceService = TestBed.inject(StoragePersistenceService);
configurationService = TestBed.inject(ConfigurationService); configurationService = TestBed.inject(ConfigurationService);
spyOn(configurationService, 'getOpenIDConfiguration').and.returnValue( vi.spyOn(
of({ configId: 'configId1' }) configurationService,
); 'getOpenIDConfiguration'
).mockReturnValue(of({ configId: 'configId1' }));
autoLoginService = TestBed.inject(AutoLoginService); autoLoginService = TestBed.inject(AutoLoginService);
router = TestBed.inject(Router); router = TestBed.inject(AbstractRouter);
}); });
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => { afterEach(() => {
storagePersistenceService.clear({}); storagePersistenceService.clear({});
}); });
it('should save current route (empty) and call `login` if not authenticated already', waitForAsync(() => { it('should save current route (empty) and call `login` if not authenticated already', async () => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false false
); );
const checkSavedRedirectRouteAndNavigateSpy = spyOn( const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService, autoLoginService,
'checkSavedRedirectRouteAndNavigate' 'checkSavedRedirectRouteAndNavigate'
); );
const saveRedirectRouteSpy = spyOn( const saveRedirectRouteSpy = vi.spyOn(
autoLoginService, autoLoginService,
'saveRedirectRoute' 'saveRedirectRoute'
); );
const loginSpy = spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
const guard$ = TestBed.runInInjectionContext( const guard$ = TestBed.runInInjectionContext(
autoLoginPartialRoutesGuard autoLoginPartialRoutesGuard
); );
guard$.subscribe(() => { await firstValueFrom(guard$);
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith( expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
'' ''
); );
expect(loginSpy).toHaveBeenCalledOnceWith({ configId: 'configId1' }); expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); configId: 'configId1',
}); });
})); expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
});
it('should save current route (with router extractedUrl) and call `login` if not authenticated already', waitForAsync(() => { it('should save current route (with router extractedUrl) and call `login` if not authenticated already', async () => {
spyOn(router, 'getCurrentNavigation').and.returnValue({ vi.spyOn(router, 'getCurrentNavigation').mockReturnValue({
extractedUrl: router.parseUrl( extractedUrl: router.parseUrl(
'some-url12/with/some-param?queryParam=true' 'some-url12/with/some-param?queryParam=true'
), ),
extras: {},
id: 1,
initialUrl: router.parseUrl(''),
previousNavigation: null,
trigger: 'imperative',
}); });
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false false
); );
const checkSavedRedirectRouteAndNavigateSpy = spyOn( const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService, autoLoginService,
'checkSavedRedirectRouteAndNavigate' 'checkSavedRedirectRouteAndNavigate'
); );
const saveRedirectRouteSpy = spyOn( const saveRedirectRouteSpy = vi.spyOn(
autoLoginService, autoLoginService,
'saveRedirectRoute' 'saveRedirectRoute'
); );
const loginSpy = spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
const guard$ = TestBed.runInInjectionContext( const guard$ = TestBed.runInInjectionContext(
autoLoginPartialRoutesGuard autoLoginPartialRoutesGuard
); );
guard$.subscribe(() => { await firstValueFrom(guard$);
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith( expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
'some-url12/with/some-param?queryParam=true' 'some-url12/with/some-param?queryParam=true'
); );
expect(loginSpy).toHaveBeenCalledOnceWith({ configId: 'configId1' }); expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); configId: 'configId1',
}); });
})); expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
});
it('should save current route and call `login` if not authenticated already and add custom params', waitForAsync(() => { it('should save current route and call `login` if not authenticated already and add custom params', async () => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false false
); );
const checkSavedRedirectRouteAndNavigateSpy = spyOn( const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService, autoLoginService,
'checkSavedRedirectRouteAndNavigate' 'checkSavedRedirectRouteAndNavigate'
); );
const saveRedirectRouteSpy = spyOn( const saveRedirectRouteSpy = vi.spyOn(
autoLoginService, autoLoginService,
'saveRedirectRoute' 'saveRedirectRoute'
); );
const loginSpy = spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
const guard$ = TestBed.runInInjectionContext(() => const guard$ = TestBed.runInInjectionContext(() =>
autoLoginPartialRoutesGuard({ autoLoginPartialRoutesGuard({
@@ -459,45 +443,43 @@ describe(`AutoLoginPartialRoutesGuard`, () => {
} as unknown as ActivatedRouteSnapshot) } as unknown as ActivatedRouteSnapshot)
); );
guard$.subscribe(() => { await firstValueFrom(guard$);
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith( expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
'' ''
); );
expect(loginSpy).toHaveBeenCalledOnceWith( expect(loginSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
{ customParams: { custom: 'param' } } { customParams: { custom: 'param' } }
); );
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
}); });
}));
it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', waitForAsync(() => { it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
true true
); );
const checkSavedRedirectRouteAndNavigateSpy = spyOn( const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService, autoLoginService,
'checkSavedRedirectRouteAndNavigate' 'checkSavedRedirectRouteAndNavigate'
); );
const saveRedirectRouteSpy = spyOn( const saveRedirectRouteSpy = vi.spyOn(
autoLoginService, autoLoginService,
'saveRedirectRoute' 'saveRedirectRoute'
); );
const loginSpy = spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
const guard$ = TestBed.runInInjectionContext( const guard$ = TestBed.runInInjectionContext(
autoLoginPartialRoutesGuard autoLoginPartialRoutesGuard
); );
guard$.subscribe(() => { await firstValueFrom(guard$);
expect(saveRedirectRouteSpy).not.toHaveBeenCalled(); expect(saveRedirectRouteSpy).not.toHaveBeenCalled();
expect(loginSpy).not.toHaveBeenCalled(); expect(loginSpy).not.toHaveBeenCalled();
expect( expect(
checkSavedRedirectRouteAndNavigateSpy checkSavedRedirectRouteAndNavigateSpy
).toHaveBeenCalledOnceWith({ configId: 'configId1' }); ).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' });
}); });
}));
}); });
describe('autoLoginPartialRoutesGuardWithConfig', () => { describe('autoLoginPartialRoutesGuardWithConfig', () => {
@@ -513,44 +495,47 @@ describe(`AutoLoginPartialRoutesGuard`, () => {
storagePersistenceService = TestBed.inject(StoragePersistenceService); storagePersistenceService = TestBed.inject(StoragePersistenceService);
configurationService = TestBed.inject(ConfigurationService); configurationService = TestBed.inject(ConfigurationService);
spyOn(configurationService, 'getOpenIDConfiguration').and.callFake( vi.spyOn(
(configId) => of({ configId }) configurationService,
); 'getOpenIDConfiguration'
).mockImplementation((configId) => of({ configId }));
autoLoginService = TestBed.inject(AutoLoginService); autoLoginService = TestBed.inject(AutoLoginService);
}); });
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => { afterEach(() => {
storagePersistenceService.clear({}); storagePersistenceService.clear({});
}); });
it('should save current route (empty) and call `login` if not authenticated already', waitForAsync(() => { it('should save current route (empty) and call `login` if not authenticated already', async () => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false false
); );
const checkSavedRedirectRouteAndNavigateSpy = spyOn( const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService, autoLoginService,
'checkSavedRedirectRouteAndNavigate' 'checkSavedRedirectRouteAndNavigate'
); );
const saveRedirectRouteSpy = spyOn( const saveRedirectRouteSpy = vi.spyOn(
autoLoginService, autoLoginService,
'saveRedirectRoute' 'saveRedirectRoute'
); );
const loginSpy = spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
const guard$ = TestBed.runInInjectionContext( const guard$ = TestBed.runInInjectionContext(
autoLoginPartialRoutesGuardWithConfig('configId1') autoLoginPartialRoutesGuardWithConfig('configId1')
); );
guard$.subscribe(() => { await firstValueFrom(guard$);
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith( expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
'' ''
); );
expect(loginSpy).toHaveBeenCalledOnceWith({ configId: 'configId1' }); expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); configId: 'configId1',
}); });
})); expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
});
}); });
}); });
}); });

View File

@@ -1,15 +1,16 @@
import { inject, Injectable } from 'injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import { import { type Observable, of } from 'rxjs';
ActivatedRouteSnapshot, import { switchMap } from 'rxjs/operators';
Router, import type { AuthOptions } from '../auth-options';
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { AuthOptions } from '../auth-options';
import { AuthStateService } from '../auth-state/auth-state.service'; import { AuthStateService } from '../auth-state/auth-state.service';
import { ConfigurationService } from '../config/config.service'; import { ConfigurationService } from '../config/config.service';
import { injectAbstractType } from '../injection';
import { LoginService } from '../login/login.service'; import { LoginService } from '../login/login.service';
import {
AbstractRouter,
type ActivatedRouteSnapshot,
type RouterStateSnapshot,
} from '../router';
import { AutoLoginService } from './auto-login.service'; import { AutoLoginService } from './auto-login.service';
@Injectable() @Injectable()
@@ -22,7 +23,7 @@ export class AutoLoginPartialRoutesGuard {
private readonly configurationService = inject(ConfigurationService); private readonly configurationService = inject(ConfigurationService);
private readonly router = inject(Router); private readonly router = injectAbstractType(AbstractRouter);
canLoad(): Observable<boolean> { canLoad(): Observable<boolean> {
const url = const url =
@@ -79,14 +80,14 @@ export class AutoLoginPartialRoutesGuard {
export function autoLoginPartialRoutesGuard( export function autoLoginPartialRoutesGuard(
route?: ActivatedRouteSnapshot, route?: ActivatedRouteSnapshot,
state?: RouterStateSnapshot, _state?: RouterStateSnapshot,
configId?: string configId?: string
): Observable<boolean> { ): Observable<boolean> {
const configurationService = inject(ConfigurationService); const configurationService = inject(ConfigurationService);
const authStateService = inject(AuthStateService); const authStateService = inject(AuthStateService);
const loginService = inject(LoginService); const loginService = inject(LoginService);
const autoLoginService = inject(AutoLoginService); const autoLoginService = inject(AutoLoginService);
const router = inject(Router); const router = injectAbstractType(AbstractRouter);
const authOptions: AuthOptions | undefined = route?.data const authOptions: AuthOptions | undefined = route?.data
? { customParams: route.data } ? { customParams: route.data }
: undefined; : undefined;
@@ -125,7 +126,7 @@ function checkAuth(
configId?: string configId?: string
): Observable<boolean> { ): Observable<boolean> {
return configurationService.getOpenIDConfiguration(configId).pipe( return configurationService.getOpenIDConfiguration(configId).pipe(
map((configuration) => { switchMap((configuration) => {
const isAuthenticated = const isAuthenticated =
authStateService.areAuthStorageTokensValid(configuration); authStateService.areAuthStorageTokensValid(configuration);
@@ -136,13 +137,16 @@ function checkAuth(
if (!isAuthenticated) { if (!isAuthenticated) {
autoLoginService.saveRedirectRoute(configuration, url); autoLoginService.saveRedirectRoute(configuration, url);
if (authOptions) { if (authOptions) {
loginService.login(configuration, authOptions); return loginService
} else { .login(configuration, authOptions)
loginService.login(configuration); .pipe(switchMap(() => of(isAuthenticated)));
} }
return loginService
.login(configuration)
.pipe(switchMap(() => of(isAuthenticated)));
} }
return isAuthenticated; return of(isAuthenticated);
}) })
); );
} }

View File

@@ -1,24 +1,25 @@
import { TestBed } from '@angular/core/testing'; import { TestBed, mockRouterProvider } from '@/testing';
import { Router } from '@angular/router'; import { AbstractRouter } from 'oidc-client-rx/router';
import { RouterTestingModule } from '@angular/router/testing'; import { vi } from 'vitest';
import { mockProvider } from '../../test/auto-mock';
import { StoragePersistenceService } from '../storage/storage-persistence.service'; import { StoragePersistenceService } from '../storage/storage-persistence.service';
import { mockProvider } from '../testing/mock';
import { AutoLoginService } from './auto-login.service'; import { AutoLoginService } from './auto-login.service';
describe('AutoLoginService ', () => { describe('AutoLoginService ', () => {
let autoLoginService: AutoLoginService; let autoLoginService: AutoLoginService;
let storagePersistenceService: StoragePersistenceService; let storagePersistenceService: StoragePersistenceService;
let router: Router; let router: AbstractRouter;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [RouterTestingModule], imports: [],
providers: [AutoLoginService, mockProvider(StoragePersistenceService)], providers: [
mockRouterProvider(),
AutoLoginService,
mockProvider(StoragePersistenceService),
],
}); });
}); router = TestBed.inject(AbstractRouter);
beforeEach(() => {
router = TestBed.inject(Router);
autoLoginService = TestBed.inject(AutoLoginService); autoLoginService = TestBed.inject(AutoLoginService);
storagePersistenceService = TestBed.inject(StoragePersistenceService); storagePersistenceService = TestBed.inject(StoragePersistenceService);
}); });
@@ -29,11 +30,11 @@ describe('AutoLoginService ', () => {
describe('checkSavedRedirectRouteAndNavigate', () => { describe('checkSavedRedirectRouteAndNavigate', () => {
it('if not route is saved, router and delete are not called', () => { it('if not route is saved, router and delete are not called', () => {
const deleteSpy = spyOn(storagePersistenceService, 'remove'); const deleteSpy = vi.spyOn(storagePersistenceService, 'remove');
const routerSpy = spyOn(router, 'navigateByUrl'); const routerSpy = vi.spyOn(router, 'navigateByUrl');
const readSpy = spyOn(storagePersistenceService, 'read').and.returnValue( const readSpy = vi
null .spyOn(storagePersistenceService, 'read')
); .mockReturnValue(null);
autoLoginService.checkSavedRedirectRouteAndNavigate({ autoLoginService.checkSavedRedirectRouteAndNavigate({
configId: 'configId1', configId: 'configId1',
@@ -41,27 +42,27 @@ describe('AutoLoginService ', () => {
expect(deleteSpy).not.toHaveBeenCalled(); expect(deleteSpy).not.toHaveBeenCalled();
expect(routerSpy).not.toHaveBeenCalled(); expect(routerSpy).not.toHaveBeenCalled();
expect(readSpy).toHaveBeenCalledOnceWith('redirect', { expect(readSpy).toHaveBeenCalledExactlyOnceWith('redirect', {
configId: 'configId1', configId: 'configId1',
}); });
}); });
it('if route is saved, router and delete are called', () => { it('if route is saved, router and delete are called', () => {
const deleteSpy = spyOn(storagePersistenceService, 'remove'); const deleteSpy = vi.spyOn(storagePersistenceService, 'remove');
const routerSpy = spyOn(router, 'navigateByUrl'); const routerSpy = vi.spyOn(router, 'navigateByUrl');
const readSpy = spyOn(storagePersistenceService, 'read').and.returnValue( const readSpy = vi
'saved-route' .spyOn(storagePersistenceService, 'read')
); .mockReturnValue('saved-route');
autoLoginService.checkSavedRedirectRouteAndNavigate({ autoLoginService.checkSavedRedirectRouteAndNavigate({
configId: 'configId1', configId: 'configId1',
}); });
expect(deleteSpy).toHaveBeenCalledOnceWith('redirect', { expect(deleteSpy).toHaveBeenCalledExactlyOnceWith('redirect', {
configId: 'configId1', configId: 'configId1',
}); });
expect(routerSpy).toHaveBeenCalledOnceWith('saved-route'); expect(routerSpy).toHaveBeenCalledExactlyOnceWith('saved-route');
expect(readSpy).toHaveBeenCalledOnceWith('redirect', { expect(readSpy).toHaveBeenCalledExactlyOnceWith('redirect', {
configId: 'configId1', configId: 'configId1',
}); });
}); });
@@ -69,16 +70,20 @@ describe('AutoLoginService ', () => {
describe('saveRedirectRoute', () => { describe('saveRedirectRoute', () => {
it('calls storageService with correct params', () => { it('calls storageService with correct params', () => {
const writeSpy = spyOn(storagePersistenceService, 'write'); const writeSpy = vi.spyOn(storagePersistenceService, 'write');
autoLoginService.saveRedirectRoute( autoLoginService.saveRedirectRoute(
{ configId: 'configId1' }, { configId: 'configId1' },
'some-route' 'some-route'
); );
expect(writeSpy).toHaveBeenCalledOnceWith('redirect', 'some-route', { expect(writeSpy).toHaveBeenCalledExactlyOnceWith(
configId: 'configId1', 'redirect',
}); 'some-route',
{
configId: 'configId1',
}
);
}); });
}); });
}); });

View File

@@ -1,6 +1,7 @@
import { inject, Injectable } from 'injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import { Router } from '@angular/router'; import type { OpenIdConfiguration } from '../config/openid-configuration';
import { OpenIdConfiguration } from '../config/openid-configuration'; import { injectAbstractType } from '../injection';
import { AbstractRouter } from '../router';
import { StoragePersistenceService } from '../storage/storage-persistence.service'; import { StoragePersistenceService } from '../storage/storage-persistence.service';
const STORAGE_KEY = 'redirect'; const STORAGE_KEY = 'redirect';
@@ -9,7 +10,7 @@ const STORAGE_KEY = 'redirect';
export class AutoLoginService { export class AutoLoginService {
private readonly storageService = inject(StoragePersistenceService); private readonly storageService = inject(StoragePersistenceService);
private readonly router = inject(Router); private readonly router = injectAbstractType(AbstractRouter);
checkSavedRedirectRouteAndNavigate(config: OpenIdConfiguration | null): void { checkSavedRedirectRouteAndNavigate(config: OpenIdConfiguration | null): void {
if (!config) { if (!config) {

View File

@@ -1,7 +1,8 @@
import { TestBed, waitForAsync } from '@angular/core/testing'; import { TestBed } from '@/testing';
import { Observable, of } from 'rxjs'; import { Observable, firstValueFrom, of } from 'rxjs';
import { mockProvider } from '../../test/auto-mock'; import { vi } from 'vitest';
import { CallbackContext } from '../flows/callback-context'; import type { CallbackContext } from '../flows/callback-context';
import { mockProvider } from '../testing/mock';
import { FlowHelper } from '../utils/flowHelper/flow-helper.service'; import { FlowHelper } from '../utils/flowHelper/flow-helper.service';
import { UrlService } from '../utils/url/url.service'; import { UrlService } from '../utils/url/url.service';
import { CallbackService } from './callback.service'; import { CallbackService } from './callback.service';
@@ -26,9 +27,6 @@ describe('CallbackService ', () => {
mockProvider(CodeFlowCallbackService), mockProvider(CodeFlowCallbackService),
], ],
}); });
});
beforeEach(() => {
callbackService = TestBed.inject(CallbackService); callbackService = TestBed.inject(CallbackService);
flowHelper = TestBed.inject(FlowHelper); flowHelper = TestBed.inject(FlowHelper);
implicitFlowCallbackService = TestBed.inject(ImplicitFlowCallbackService); implicitFlowCallbackService = TestBed.inject(ImplicitFlowCallbackService);
@@ -38,10 +36,13 @@ describe('CallbackService ', () => {
describe('isCallback', () => { describe('isCallback', () => {
it('calls urlService.isCallbackFromSts with passed url', () => { it('calls urlService.isCallbackFromSts with passed url', () => {
const urlServiceSpy = spyOn(urlService, 'isCallbackFromSts'); const urlServiceSpy = vi.spyOn(urlService, 'isCallbackFromSts');
callbackService.isCallback('anyUrl'); callbackService.isCallback('anyUrl');
expect(urlServiceSpy).toHaveBeenCalledOnceWith('anyUrl', undefined); expect(urlServiceSpy).toHaveBeenCalledExactlyOnceWith(
'anyUrl',
undefined
);
}); });
}); });
@@ -52,93 +53,95 @@ describe('CallbackService ', () => {
}); });
describe('handleCallbackAndFireEvents', () => { describe('handleCallbackAndFireEvents', () => {
it('calls authorizedCallbackWithCode if current flow is code flow', waitForAsync(() => { it('calls authorizedCallbackWithCode if current flow is code flow', async () => {
spyOn(flowHelper, 'isCurrentFlowCodeFlow').and.returnValue(true); vi.spyOn(flowHelper, 'isCurrentFlowCodeFlow').mockReturnValue(true);
const authorizedCallbackWithCodeSpy = spyOn( const authorizedCallbackWithCodeSpy = vi
codeFlowCallbackService, .spyOn(codeFlowCallbackService, 'authenticatedCallbackWithCode')
'authenticatedCallbackWithCode' .mockReturnValue(of({} as CallbackContext));
).and.returnValue(of({} as CallbackContext));
callbackService await firstValueFrom(
.handleCallbackAndFireEvents('anyUrl', { configId: 'configId1' }, [ callbackService.handleCallbackAndFireEvents(
'anyUrl',
{ configId: 'configId1' }, { configId: 'configId1' },
]) [{ configId: 'configId1' }]
.subscribe(() => { )
expect(authorizedCallbackWithCodeSpy).toHaveBeenCalledOnceWith( );
'anyUrl',
{ configId: 'configId1' },
[{ configId: 'configId1' }]
);
});
}));
it('calls authorizedImplicitFlowCallback without hash if current flow is implicit flow and callbackurl does not include a hash', waitForAsync(() => { expect(authorizedCallbackWithCodeSpy).toHaveBeenCalledExactlyOnceWith(
spyOn(flowHelper, 'isCurrentFlowCodeFlow').and.returnValue(false); 'anyUrl',
spyOn(flowHelper, 'isCurrentFlowAnyImplicitFlow').and.returnValue(true); { configId: 'configId1' },
const authorizedCallbackWithCodeSpy = spyOn( [{ configId: 'configId1' }]
implicitFlowCallbackService, );
'authenticatedImplicitFlowCallback' });
).and.returnValue(of({} as CallbackContext));
callbackService it('calls authorizedImplicitFlowCallback without hash if current flow is implicit flow and callbackurl does not include a hash', async () => {
.handleCallbackAndFireEvents('anyUrl', { configId: 'configId1' }, [ vi.spyOn(flowHelper, 'isCurrentFlowCodeFlow').mockReturnValue(false);
vi.spyOn(flowHelper, 'isCurrentFlowAnyImplicitFlow').mockReturnValue(
true
);
const authorizedCallbackWithCodeSpy = vi
.spyOn(implicitFlowCallbackService, 'authenticatedImplicitFlowCallback')
.mockReturnValue(of({} as CallbackContext));
await firstValueFrom(
callbackService.handleCallbackAndFireEvents(
'anyUrl',
{ configId: 'configId1' }, { configId: 'configId1' },
]) [{ configId: 'configId1' }]
.subscribe(() => { )
expect(authorizedCallbackWithCodeSpy).toHaveBeenCalledWith( );
{ configId: 'configId1' }, expect(authorizedCallbackWithCodeSpy.mock.calls).toEqual([
[{ configId: 'configId1' }] [{ configId: 'configId1' }, [{ configId: 'configId1' }]],
); ]);
}); });
}));
it('calls authorizedImplicitFlowCallback with hash if current flow is implicit flow and callbackurl does include a hash', waitForAsync(() => { it('calls authorizedImplicitFlowCallback with hash if current flow is implicit flow and callbackurl does include a hash', async () => {
spyOn(flowHelper, 'isCurrentFlowCodeFlow').and.returnValue(false); vi.spyOn(flowHelper, 'isCurrentFlowCodeFlow').mockReturnValue(false);
spyOn(flowHelper, 'isCurrentFlowAnyImplicitFlow').and.returnValue(true); vi.spyOn(flowHelper, 'isCurrentFlowAnyImplicitFlow').mockReturnValue(
const authorizedCallbackWithCodeSpy = spyOn( true
implicitFlowCallbackService, );
'authenticatedImplicitFlowCallback' const authorizedCallbackWithCodeSpy = vi
).and.returnValue(of({} as CallbackContext)); .spyOn(implicitFlowCallbackService, 'authenticatedImplicitFlowCallback')
.mockReturnValue(of({} as CallbackContext));
callbackService await firstValueFrom(
.handleCallbackAndFireEvents( callbackService.handleCallbackAndFireEvents(
'anyUrlWithAHash#some-string', 'anyUrlWithAHash#some-string',
{ configId: 'configId1' }, { configId: 'configId1' },
[{ configId: 'configId1' }] [{ configId: 'configId1' }]
) )
.subscribe(() => { );
expect(authorizedCallbackWithCodeSpy).toHaveBeenCalledWith(
{ configId: 'configId1' },
[{ configId: 'configId1' }],
'some-string'
);
});
}));
it('emits callbackinternal no matter which flow it is', waitForAsync(() => { expect(authorizedCallbackWithCodeSpy.mock.calls).toEqual([
const callbackSpy = spyOn( [{ configId: 'configId1' }, [{ configId: 'configId1' }], 'some-string'],
]);
});
it('emits callbackinternal no matter which flow it is', async () => {
const callbackSpy = vi.spyOn(
(callbackService as any).stsCallbackInternal$, (callbackService as any).stsCallbackInternal$,
'next' 'next'
); );
spyOn(flowHelper, 'isCurrentFlowCodeFlow').and.returnValue(true); vi.spyOn(flowHelper, 'isCurrentFlowCodeFlow').mockReturnValue(true);
const authenticatedCallbackWithCodeSpy = spyOn( const authenticatedCallbackWithCodeSpy = vi
codeFlowCallbackService, .spyOn(codeFlowCallbackService, 'authenticatedCallbackWithCode')
'authenticatedCallbackWithCode' .mockReturnValue(of({} as CallbackContext));
).and.returnValue(of({} as CallbackContext));
callbackService await firstValueFrom(
.handleCallbackAndFireEvents('anyUrl', { configId: 'configId1' }, [ callbackService.handleCallbackAndFireEvents(
'anyUrl',
{ configId: 'configId1' }, { configId: 'configId1' },
]) [{ configId: 'configId1' }]
.subscribe(() => { )
expect(authenticatedCallbackWithCodeSpy).toHaveBeenCalledOnceWith( );
'anyUrl',
{ configId: 'configId1' }, expect(authenticatedCallbackWithCodeSpy).toHaveBeenCalledExactlyOnceWith(
[{ configId: 'configId1' }] 'anyUrl',
); { configId: 'configId1' },
expect(callbackSpy).toHaveBeenCalled(); [{ configId: 'configId1' }]
}); );
})); expect(callbackSpy).toHaveBeenCalled();
});
}); });
}); });

View File

@@ -1,8 +1,8 @@
import { inject, Injectable } from 'injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
import { CallbackContext } from '../flows/callback-context'; import type { CallbackContext } from '../flows/callback-context';
import { FlowHelper } from '../utils/flowHelper/flow-helper.service'; import { FlowHelper } from '../utils/flowHelper/flow-helper.service';
import { UrlService } from '../utils/url/url.service'; import { UrlService } from '../utils/url/url.service';
import { CodeFlowCallbackService } from './code-flow-callback.service'; import { CodeFlowCallbackService } from './code-flow-callback.service';

View File

@@ -1,11 +1,11 @@
import { TestBed, waitForAsync } from '@angular/core/testing'; import { TestBed, mockRouterProvider } from '@/testing';
import { Router } from '@angular/router'; import { AbstractRouter } from 'oidc-client-rx';
import { RouterTestingModule } from '@angular/router/testing'; import { firstValueFrom, of, throwError } from 'rxjs';
import { of, throwError } from 'rxjs'; import { vi } from 'vitest';
import { mockProvider } from '../../test/auto-mock'; import type { CallbackContext } from '../flows/callback-context';
import { CallbackContext } from '../flows/callback-context';
import { FlowsDataService } from '../flows/flows-data.service'; import { FlowsDataService } from '../flows/flows-data.service';
import { FlowsService } from '../flows/flows.service'; import { FlowsService } from '../flows/flows.service';
import { mockProvider } from '../testing/mock';
import { CodeFlowCallbackService } from './code-flow-callback.service'; import { CodeFlowCallbackService } from './code-flow-callback.service';
import { IntervalService } from './interval.service'; import { IntervalService } from './interval.service';
@@ -14,26 +14,24 @@ describe('CodeFlowCallbackService ', () => {
let intervalService: IntervalService; let intervalService: IntervalService;
let flowsService: FlowsService; let flowsService: FlowsService;
let flowsDataService: FlowsDataService; let flowsDataService: FlowsDataService;
let router: Router; let router: AbstractRouter;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [RouterTestingModule], imports: [],
providers: [ providers: [
mockRouterProvider(),
CodeFlowCallbackService, CodeFlowCallbackService,
mockProvider(FlowsService), mockProvider(FlowsService),
mockProvider(FlowsDataService), mockProvider(FlowsDataService),
mockProvider(IntervalService), mockProvider(IntervalService),
], ],
}); });
});
beforeEach(() => {
codeFlowCallbackService = TestBed.inject(CodeFlowCallbackService); codeFlowCallbackService = TestBed.inject(CodeFlowCallbackService);
intervalService = TestBed.inject(IntervalService); intervalService = TestBed.inject(IntervalService);
flowsDataService = TestBed.inject(FlowsDataService); flowsDataService = TestBed.inject(FlowsDataService);
flowsService = TestBed.inject(FlowsService); flowsService = TestBed.inject(FlowsService);
router = TestBed.inject(Router); router = TestBed.inject(AbstractRouter);
}); });
it('should create', () => { it('should create', () => {
@@ -42,11 +40,10 @@ describe('CodeFlowCallbackService ', () => {
describe('authenticatedCallbackWithCode', () => { describe('authenticatedCallbackWithCode', () => {
it('calls flowsService.processCodeFlowCallback with correct url', () => { it('calls flowsService.processCodeFlowCallback with correct url', () => {
const spy = spyOn( const spy = vi
flowsService, .spyOn(flowsService, 'processCodeFlowCallback')
'processCodeFlowCallback' .mockReturnValue(of({} as CallbackContext));
).and.returnValue(of({} as CallbackContext)); //spyOn(configurationProvider, 'getOpenIDConfiguration').mockReturnValue({ triggerAuthorizationResultEvent: true });
//spyOn(configurationProvider, 'getOpenIDConfiguration').and.returnValue({ triggerAuthorizationResultEvent: true });
const config = { const config = {
configId: 'configId1', configId: 'configId1',
@@ -58,10 +55,12 @@ describe('CodeFlowCallbackService ', () => {
config, config,
[config] [config]
); );
expect(spy).toHaveBeenCalledOnceWith('some-url1', config, [config]); expect(spy).toHaveBeenCalledExactlyOnceWith('some-url1', config, [
config,
]);
}); });
it('does only call resetCodeFlowInProgress if triggerAuthorizationResultEvent is true and isRenewProcess is true', waitForAsync(() => { it('does only call resetCodeFlowInProgress if triggerAuthorizationResultEvent is true and isRenewProcess is true', async () => {
const callbackContext = { const callbackContext = {
code: '', code: '',
refreshToken: '', refreshToken: '',
@@ -73,27 +72,34 @@ describe('CodeFlowCallbackService ', () => {
validationResult: null, validationResult: null,
existingIdToken: '', existingIdToken: '',
}; };
const spy = spyOn( const spy = vi
flowsService, .spyOn(flowsService, 'processCodeFlowCallback')
'processCodeFlowCallback' .mockReturnValue(of(callbackContext));
).and.returnValue(of(callbackContext)); const flowsDataSpy = vi.spyOn(
const flowsDataSpy = spyOn(flowsDataService, 'resetCodeFlowInProgress'); flowsDataService,
const routerSpy = spyOn(router, 'navigateByUrl'); 'resetCodeFlowInProgress'
);
const routerSpy = vi.spyOn(router, 'navigateByUrl');
const config = { const config = {
configId: 'configId1', configId: 'configId1',
triggerAuthorizationResultEvent: true, triggerAuthorizationResultEvent: true,
}; };
codeFlowCallbackService await firstValueFrom(
.authenticatedCallbackWithCode('some-url2', config, [config]) codeFlowCallbackService.authenticatedCallbackWithCode(
.subscribe(() => { 'some-url2',
expect(spy).toHaveBeenCalledOnceWith('some-url2', config, [config]); config,
expect(routerSpy).not.toHaveBeenCalled(); [config]
expect(flowsDataSpy).toHaveBeenCalled(); )
}); );
})); expect(spy).toHaveBeenCalledExactlyOnceWith('some-url2', config, [
config,
]);
expect(routerSpy).not.toHaveBeenCalled();
expect(flowsDataSpy).toHaveBeenCalled();
});
it('calls router and resetCodeFlowInProgress if triggerAuthorizationResultEvent is false and isRenewProcess is false', waitForAsync(() => { it('calls router and resetCodeFlowInProgress if triggerAuthorizationResultEvent is false and isRenewProcess is false', async () => {
const callbackContext = { const callbackContext = {
code: '', code: '',
refreshToken: '', refreshToken: '',
@@ -105,40 +111,47 @@ describe('CodeFlowCallbackService ', () => {
validationResult: null, validationResult: null,
existingIdToken: '', existingIdToken: '',
}; };
const spy = spyOn( const spy = vi
flowsService, .spyOn(flowsService, 'processCodeFlowCallback')
'processCodeFlowCallback' .mockReturnValue(of(callbackContext));
).and.returnValue(of(callbackContext)); const flowsDataSpy = vi.spyOn(
const flowsDataSpy = spyOn(flowsDataService, 'resetCodeFlowInProgress');
const routerSpy = spyOn(router, 'navigateByUrl');
const config = {
configId: 'configId1',
triggerAuthorizationResultEvent: false,
postLoginRoute: 'postLoginRoute',
};
codeFlowCallbackService
.authenticatedCallbackWithCode('some-url3', config, [config])
.subscribe(() => {
expect(spy).toHaveBeenCalledOnceWith('some-url3', config, [config]);
expect(routerSpy).toHaveBeenCalledOnceWith('postLoginRoute');
expect(flowsDataSpy).toHaveBeenCalled();
});
}));
it('resetSilentRenewRunning, resetCodeFlowInProgress and stopPeriodicallTokenCheck in case of error', waitForAsync(() => {
spyOn(flowsService, 'processCodeFlowCallback').and.returnValue(
throwError(() => new Error('error'))
);
const resetSilentRenewRunningSpy = spyOn(
flowsDataService,
'resetSilentRenewRunning'
);
const resetCodeFlowInProgressSpy = spyOn(
flowsDataService, flowsDataService,
'resetCodeFlowInProgress' 'resetCodeFlowInProgress'
); );
const stopPeriodicallTokenCheckSpy = spyOn( const routerSpy = vi.spyOn(router, 'navigateByUrl');
const config = {
configId: 'configId1',
triggerAuthorizationResultEvent: false,
postLoginRoute: 'postLoginRoute',
};
await firstValueFrom(
codeFlowCallbackService.authenticatedCallbackWithCode(
'some-url3',
config,
[config]
)
);
expect(spy).toHaveBeenCalledExactlyOnceWith('some-url3', config, [
config,
]);
expect(routerSpy).toHaveBeenCalledExactlyOnceWith('postLoginRoute');
expect(flowsDataSpy).toHaveBeenCalled();
});
it('resetSilentRenewRunning, resetCodeFlowInProgress and stopPeriodicallTokenCheck in case of error', async () => {
vi.spyOn(flowsService, 'processCodeFlowCallback').mockReturnValue(
throwError(() => new Error('error'))
);
const resetSilentRenewRunningSpy = vi.spyOn(
flowsDataService,
'resetSilentRenewRunning'
);
const resetCodeFlowInProgressSpy = vi.spyOn(
flowsDataService,
'resetCodeFlowInProgress'
);
const stopPeriodicallTokenCheckSpy = vi.spyOn(
intervalService, intervalService,
'stopPeriodicTokenCheck' 'stopPeriodicTokenCheck'
); );
@@ -149,33 +162,37 @@ describe('CodeFlowCallbackService ', () => {
postLoginRoute: 'postLoginRoute', postLoginRoute: 'postLoginRoute',
}; };
codeFlowCallbackService try {
.authenticatedCallbackWithCode('some-url4', config, [config]) await firstValueFrom(
.subscribe({ codeFlowCallbackService.authenticatedCallbackWithCode(
error: (err) => { 'some-url4',
expect(resetSilentRenewRunningSpy).toHaveBeenCalled(); config,
expect(resetCodeFlowInProgressSpy).toHaveBeenCalled(); [config]
expect(stopPeriodicallTokenCheckSpy).toHaveBeenCalled(); )
expect(err).toBeTruthy(); );
}, } catch (err: any) {
}); expect(resetSilentRenewRunningSpy).toHaveBeenCalled();
})); expect(resetCodeFlowInProgressSpy).toHaveBeenCalled();
expect(stopPeriodicallTokenCheckSpy).toHaveBeenCalled();
expect(err).toBeTruthy();
}
});
it(`navigates to unauthorizedRoute in case of error and in case of error and it(`navigates to unauthorizedRoute in case of error and in case of error and
triggerAuthorizationResultEvent is false`, waitForAsync(() => { triggerAuthorizationResultEvent is false`, async () => {
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false); vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(false);
spyOn(flowsService, 'processCodeFlowCallback').and.returnValue( vi.spyOn(flowsService, 'processCodeFlowCallback').mockReturnValue(
throwError(() => new Error('error')) throwError(() => new Error('error'))
); );
const resetSilentRenewRunningSpy = spyOn( const resetSilentRenewRunningSpy = vi.spyOn(
flowsDataService, flowsDataService,
'resetSilentRenewRunning' 'resetSilentRenewRunning'
); );
const stopPeriodicallTokenCheckSpy = spyOn( const stopPeriodicallTokenCheckSpy = vi.spyOn(
intervalService, intervalService,
'stopPeriodicTokenCheck' 'stopPeriodicTokenCheck'
); );
const routerSpy = spyOn(router, 'navigateByUrl'); const routerSpy = vi.spyOn(router, 'navigateByUrl');
const config = { const config = {
configId: 'configId1', configId: 'configId1',
@@ -183,16 +200,20 @@ describe('CodeFlowCallbackService ', () => {
unauthorizedRoute: 'unauthorizedRoute', unauthorizedRoute: 'unauthorizedRoute',
}; };
codeFlowCallbackService try {
.authenticatedCallbackWithCode('some-url5', config, [config]) await firstValueFrom(
.subscribe({ codeFlowCallbackService.authenticatedCallbackWithCode(
error: (err) => { 'some-url5',
expect(resetSilentRenewRunningSpy).toHaveBeenCalled(); config,
expect(stopPeriodicallTokenCheckSpy).toHaveBeenCalled(); [config]
expect(err).toBeTruthy(); )
expect(routerSpy).toHaveBeenCalledOnceWith('unauthorizedRoute'); );
}, } catch (err: any) {
}); expect(resetSilentRenewRunningSpy).toHaveBeenCalled();
})); expect(stopPeriodicallTokenCheckSpy).toHaveBeenCalled();
expect(err).toBeTruthy();
expect(routerSpy).toHaveBeenCalledExactlyOnceWith('unauthorizedRoute');
}
});
}); });
}); });

View File

@@ -1,18 +1,19 @@
import { inject, Injectable } from 'injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import { Router } from '@angular/router'; import { type Observable, throwError } from 'rxjs';
import { Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators'; import { catchError, tap } from 'rxjs/operators';
import { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
import { CallbackContext } from '../flows/callback-context'; import type { CallbackContext } from '../flows/callback-context';
import { FlowsDataService } from '../flows/flows-data.service'; import { FlowsDataService } from '../flows/flows-data.service';
import { FlowsService } from '../flows/flows.service'; import { FlowsService } from '../flows/flows.service';
import { injectAbstractType } from '../injection';
import { AbstractRouter } from '../router';
import { IntervalService } from './interval.service'; import { IntervalService } from './interval.service';
@Injectable() @Injectable()
export class CodeFlowCallbackService { export class CodeFlowCallbackService {
private readonly flowsService = inject(FlowsService); private readonly flowsService = inject(FlowsService);
private readonly router = inject(Router); private readonly router = injectAbstractType(AbstractRouter);
private readonly flowsDataService = inject(FlowsDataService); private readonly flowsDataService = inject(FlowsDataService);

View File

@@ -1,11 +1,11 @@
import { TestBed, waitForAsync } from '@angular/core/testing'; import { TestBed, mockRouterProvider } from '@/testing';
import { Router } from '@angular/router'; import { AbstractRouter } from 'oidc-client-rx';
import { RouterTestingModule } from '@angular/router/testing'; import { firstValueFrom, of, throwError } from 'rxjs';
import { of, throwError } from 'rxjs'; import { vi } from 'vitest';
import { mockProvider } from '../../test/auto-mock'; import type { CallbackContext } from '../flows/callback-context';
import { CallbackContext } from '../flows/callback-context';
import { FlowsDataService } from '../flows/flows-data.service'; import { FlowsDataService } from '../flows/flows-data.service';
import { FlowsService } from '../flows/flows.service'; import { FlowsService } from '../flows/flows.service';
import { mockProvider } from '../testing/mock';
import { ImplicitFlowCallbackService } from './implicit-flow-callback.service'; import { ImplicitFlowCallbackService } from './implicit-flow-callback.service';
import { IntervalService } from './interval.service'; import { IntervalService } from './interval.service';
@@ -14,25 +14,24 @@ describe('ImplicitFlowCallbackService ', () => {
let intervalService: IntervalService; let intervalService: IntervalService;
let flowsService: FlowsService; let flowsService: FlowsService;
let flowsDataService: FlowsDataService; let flowsDataService: FlowsDataService;
let router: Router; let router: AbstractRouter;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [RouterTestingModule], imports: [],
providers: [ providers: [
ImplicitFlowCallbackService,
mockRouterProvider(),
mockProvider(FlowsService), mockProvider(FlowsService),
mockProvider(FlowsDataService), mockProvider(FlowsDataService),
mockProvider(IntervalService), mockProvider(IntervalService),
], ],
}); });
});
beforeEach(() => {
implicitFlowCallbackService = TestBed.inject(ImplicitFlowCallbackService); implicitFlowCallbackService = TestBed.inject(ImplicitFlowCallbackService);
intervalService = TestBed.inject(IntervalService); intervalService = TestBed.inject(IntervalService);
flowsDataService = TestBed.inject(FlowsDataService); flowsDataService = TestBed.inject(FlowsDataService);
flowsService = TestBed.inject(FlowsService); flowsService = TestBed.inject(FlowsService);
router = TestBed.inject(Router); router = TestBed.inject(AbstractRouter);
}); });
it('should create', () => { it('should create', () => {
@@ -41,10 +40,9 @@ describe('ImplicitFlowCallbackService ', () => {
describe('authorizedImplicitFlowCallback', () => { describe('authorizedImplicitFlowCallback', () => {
it('calls flowsService.processImplicitFlowCallback with hash if given', () => { it('calls flowsService.processImplicitFlowCallback with hash if given', () => {
const spy = spyOn( const spy = vi
flowsService, .spyOn(flowsService, 'processImplicitFlowCallback')
'processImplicitFlowCallback' .mockReturnValue(of({} as CallbackContext));
).and.returnValue(of({} as CallbackContext));
const config = { const config = {
configId: 'configId1', configId: 'configId1',
triggerAuthorizationResultEvent: true, triggerAuthorizationResultEvent: true,
@@ -56,10 +54,14 @@ describe('ImplicitFlowCallbackService ', () => {
'some-hash' 'some-hash'
); );
expect(spy).toHaveBeenCalledOnceWith(config, [config], 'some-hash'); expect(spy).toHaveBeenCalledExactlyOnceWith(
config,
[config],
'some-hash'
);
}); });
it('does nothing if triggerAuthorizationResultEvent is true and isRenewProcess is true', waitForAsync(() => { it('does nothing if triggerAuthorizationResultEvent is true and isRenewProcess is true', async () => {
const callbackContext = { const callbackContext = {
code: '', code: '',
refreshToken: '', refreshToken: '',
@@ -71,25 +73,31 @@ describe('ImplicitFlowCallbackService ', () => {
validationResult: null, validationResult: null,
existingIdToken: '', existingIdToken: '',
}; };
const spy = spyOn( const spy = vi
flowsService, .spyOn(flowsService, 'processImplicitFlowCallback')
'processImplicitFlowCallback' .mockReturnValue(of(callbackContext));
).and.returnValue(of(callbackContext)); const routerSpy = vi.spyOn(router, 'navigateByUrl');
const routerSpy = spyOn(router, 'navigateByUrl');
const config = { const config = {
configId: 'configId1', configId: 'configId1',
triggerAuthorizationResultEvent: true, triggerAuthorizationResultEvent: true,
}; };
implicitFlowCallbackService await firstValueFrom(
.authenticatedImplicitFlowCallback(config, [config], 'some-hash') implicitFlowCallbackService.authenticatedImplicitFlowCallback(
.subscribe(() => { config,
expect(spy).toHaveBeenCalledOnceWith(config, [config], 'some-hash'); [config],
expect(routerSpy).not.toHaveBeenCalled(); 'some-hash'
}); )
})); );
expect(spy).toHaveBeenCalledExactlyOnceWith(
config,
[config],
'some-hash'
);
expect(routerSpy).not.toHaveBeenCalled();
});
it('calls router if triggerAuthorizationResultEvent is false and isRenewProcess is false', waitForAsync(() => { it('calls router if triggerAuthorizationResultEvent is false and isRenewProcess is false', async () => {
const callbackContext = { const callbackContext = {
code: '', code: '',
refreshToken: '', refreshToken: '',
@@ -101,34 +109,40 @@ describe('ImplicitFlowCallbackService ', () => {
validationResult: null, validationResult: null,
existingIdToken: '', existingIdToken: '',
}; };
const spy = spyOn( const spy = vi
flowsService, .spyOn(flowsService, 'processImplicitFlowCallback')
'processImplicitFlowCallback' .mockReturnValue(of(callbackContext));
).and.returnValue(of(callbackContext)); const routerSpy = vi.spyOn(router, 'navigateByUrl');
const routerSpy = spyOn(router, 'navigateByUrl');
const config = { const config = {
configId: 'configId1', configId: 'configId1',
triggerAuthorizationResultEvent: false, triggerAuthorizationResultEvent: false,
postLoginRoute: 'postLoginRoute', postLoginRoute: 'postLoginRoute',
}; };
implicitFlowCallbackService await firstValueFrom(
.authenticatedImplicitFlowCallback(config, [config], 'some-hash') implicitFlowCallbackService.authenticatedImplicitFlowCallback(
.subscribe(() => { config,
expect(spy).toHaveBeenCalledOnceWith(config, [config], 'some-hash'); [config],
expect(routerSpy).toHaveBeenCalledOnceWith('postLoginRoute'); 'some-hash'
}); )
})); );
expect(spy).toHaveBeenCalledExactlyOnceWith(
config,
[config],
'some-hash'
);
expect(routerSpy).toHaveBeenCalledExactlyOnceWith('postLoginRoute');
});
it('resetSilentRenewRunning and stopPeriodicallyTokenCheck in case of error', waitForAsync(() => { it('resetSilentRenewRunning and stopPeriodicallyTokenCheck in case of error', async () => {
spyOn(flowsService, 'processImplicitFlowCallback').and.returnValue( vi.spyOn(flowsService, 'processImplicitFlowCallback').mockReturnValue(
throwError(() => new Error('error')) throwError(() => new Error('error'))
); );
const resetSilentRenewRunningSpy = spyOn( const resetSilentRenewRunningSpy = vi.spyOn(
flowsDataService, flowsDataService,
'resetSilentRenewRunning' 'resetSilentRenewRunning'
); );
const stopPeriodicallyTokenCheckSpy = spyOn( const stopPeriodicallyTokenCheckSpy = vi.spyOn(
intervalService, intervalService,
'stopPeriodicTokenCheck' 'stopPeriodicTokenCheck'
); );
@@ -138,48 +152,56 @@ describe('ImplicitFlowCallbackService ', () => {
postLoginRoute: 'postLoginRoute', postLoginRoute: 'postLoginRoute',
}; };
implicitFlowCallbackService try {
.authenticatedImplicitFlowCallback(config, [config], 'some-hash') await firstValueFrom(
.subscribe({ implicitFlowCallbackService.authenticatedImplicitFlowCallback(
error: (err) => { config,
expect(resetSilentRenewRunningSpy).toHaveBeenCalled(); [config],
expect(stopPeriodicallyTokenCheckSpy).toHaveBeenCalled(); 'some-hash'
expect(err).toBeTruthy(); )
}, );
}); } catch (err: any) {
})); expect(resetSilentRenewRunningSpy).toHaveBeenCalled();
expect(stopPeriodicallyTokenCheckSpy).toHaveBeenCalled();
expect(err).toBeTruthy();
}
});
it(`navigates to unauthorizedRoute in case of error and in case of error and it(`navigates to unauthorizedRoute in case of error and in case of error and
triggerAuthorizationResultEvent is false`, waitForAsync(() => { triggerAuthorizationResultEvent is false`, async () => {
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false); vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(false);
spyOn(flowsService, 'processImplicitFlowCallback').and.returnValue( vi.spyOn(flowsService, 'processImplicitFlowCallback').mockReturnValue(
throwError(() => new Error('error')) throwError(() => new Error('error'))
); );
const resetSilentRenewRunningSpy = spyOn( const resetSilentRenewRunningSpy = vi.spyOn(
flowsDataService, flowsDataService,
'resetSilentRenewRunning' 'resetSilentRenewRunning'
); );
const stopPeriodicallTokenCheckSpy = spyOn( const stopPeriodicallTokenCheckSpy = vi.spyOn(
intervalService, intervalService,
'stopPeriodicTokenCheck' 'stopPeriodicTokenCheck'
); );
const routerSpy = spyOn(router, 'navigateByUrl'); const routerSpy = vi.spyOn(router, 'navigateByUrl');
const config = { const config = {
configId: 'configId1', configId: 'configId1',
triggerAuthorizationResultEvent: false, triggerAuthorizationResultEvent: false,
unauthorizedRoute: 'unauthorizedRoute', unauthorizedRoute: 'unauthorizedRoute',
}; };
implicitFlowCallbackService try {
.authenticatedImplicitFlowCallback(config, [config], 'some-hash') await firstValueFrom(
.subscribe({ implicitFlowCallbackService.authenticatedImplicitFlowCallback(
error: (err) => { config,
expect(resetSilentRenewRunningSpy).toHaveBeenCalled(); [config],
expect(stopPeriodicallTokenCheckSpy).toHaveBeenCalled(); 'some-hash'
expect(err).toBeTruthy(); )
expect(routerSpy).toHaveBeenCalledOnceWith('unauthorizedRoute'); );
}, } catch (err: any) {
}); expect(resetSilentRenewRunningSpy).toHaveBeenCalled();
})); expect(stopPeriodicallTokenCheckSpy).toHaveBeenCalled();
expect(err).toBeTruthy();
expect(routerSpy).toHaveBeenCalledExactlyOnceWith('unauthorizedRoute');
}
});
}); });
}); });

View File

@@ -1,18 +1,19 @@
import { inject, Injectable } from 'injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import { Router } from '@angular/router'; import { type Observable, throwError } from 'rxjs';
import { Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators'; import { catchError, tap } from 'rxjs/operators';
import { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
import { CallbackContext } from '../flows/callback-context'; import type { CallbackContext } from '../flows/callback-context';
import { FlowsDataService } from '../flows/flows-data.service'; import { FlowsDataService } from '../flows/flows-data.service';
import { FlowsService } from '../flows/flows.service'; import { FlowsService } from '../flows/flows.service';
import { injectAbstractType } from '../injection';
import { AbstractRouter } from '../router';
import { IntervalService } from './interval.service'; import { IntervalService } from './interval.service';
@Injectable() @Injectable()
export class ImplicitFlowCallbackService { export class ImplicitFlowCallbackService {
private readonly flowsService = inject(FlowsService); private readonly flowsService = inject(FlowsService);
private readonly router = inject(Router); private readonly router = injectAbstractType(AbstractRouter);
private readonly flowsDataService = inject(FlowsDataService); private readonly flowsDataService = inject(FlowsDataService);

View File

@@ -1,13 +1,16 @@
import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { TestBed } from '@/testing';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { vi } from 'vitest';
import { IntervalService } from './interval.service'; import { IntervalService } from './interval.service';
describe('IntervalService', () => { describe('IntervalService', () => {
let intervalService: IntervalService; let intervalService: IntervalService;
beforeEach(() => { beforeEach(() => {
vi.useFakeTimers();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
IntervalService,
{ {
provide: Document, provide: Document,
useValue: { useValue: {
@@ -18,10 +21,12 @@ describe('IntervalService', () => {
}, },
], ],
}); });
intervalService = TestBed.inject(IntervalService);
}); });
beforeEach(() => { // biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
intervalService = TestBed.inject(IntervalService); afterEach(() => {
vi.useRealTimers();
}); });
it('should create', () => { it('should create', () => {
@@ -31,7 +36,7 @@ describe('IntervalService', () => {
describe('stopPeriodicTokenCheck', () => { describe('stopPeriodicTokenCheck', () => {
it('calls unsubscribe and sets to null', () => { it('calls unsubscribe and sets to null', () => {
intervalService.runTokenValidationRunning = new Subscription(); intervalService.runTokenValidationRunning = new Subscription();
const spy = spyOn( const spy = vi.spyOn(
intervalService.runTokenValidationRunning, intervalService.runTokenValidationRunning,
'unsubscribe' 'unsubscribe'
); );
@@ -44,7 +49,7 @@ describe('IntervalService', () => {
it('does nothing if `runTokenValidationRunning` is null', () => { it('does nothing if `runTokenValidationRunning` is null', () => {
intervalService.runTokenValidationRunning = new Subscription(); intervalService.runTokenValidationRunning = new Subscription();
const spy = spyOn( const spy = vi.spyOn(
intervalService.runTokenValidationRunning, intervalService.runTokenValidationRunning,
'unsubscribe' 'unsubscribe'
); );
@@ -57,20 +62,20 @@ describe('IntervalService', () => {
}); });
describe('startPeriodicTokenCheck', () => { describe('startPeriodicTokenCheck', () => {
it('starts check after correct milliseconds', fakeAsync(() => { it('starts check after correct milliseconds', async () => {
const periodicCheck = intervalService.startPeriodicTokenCheck(0.5); const periodicCheck = intervalService.startPeriodicTokenCheck(0.5);
const spy = jasmine.createSpy(); const spy = vi.fn();
const sub = periodicCheck.subscribe(() => { const sub = periodicCheck.subscribe(() => {
spy(); spy();
}); });
tick(500); await vi.advanceTimersByTimeAsync(500);
expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(1);
tick(500); await vi.advanceTimersByTimeAsync(500);
expect(spy).toHaveBeenCalledTimes(2); expect(spy).toHaveBeenCalledTimes(2);
sub.unsubscribe(); sub.unsubscribe();
})); });
}); });
}); });

View File

@@ -1,11 +1,9 @@
import { Injectable, NgZone, inject } from 'injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import { Observable, Subscription } from 'rxjs'; import { type Observable, type Subscription, interval } from 'rxjs';
import { DOCUMENT } from '../../dom'; import { DOCUMENT } from '../dom';
@Injectable() @Injectable()
export class IntervalService { export class IntervalService {
private readonly zone = inject(NgZone);
private readonly document = inject(DOCUMENT); private readonly document = inject(DOCUMENT);
runTokenValidationRunning: Subscription | null = null; runTokenValidationRunning: Subscription | null = null;
@@ -24,19 +22,6 @@ export class IntervalService {
startPeriodicTokenCheck(repeatAfterSeconds: number): Observable<unknown> { startPeriodicTokenCheck(repeatAfterSeconds: number): Observable<unknown> {
const millisecondsDelayBetweenTokenCheck = repeatAfterSeconds * 1000; const millisecondsDelayBetweenTokenCheck = repeatAfterSeconds * 1000;
return new Observable((subscriber) => { return interval(millisecondsDelayBetweenTokenCheck);
let intervalId: number | undefined;
this.zone.runOutsideAngular(() => {
intervalId = this.document?.defaultView?.setInterval(
() => this.zone.run(() => subscriber.next()),
millisecondsDelayBetweenTokenCheck
);
});
return (): void => {
clearInterval(intervalId);
};
});
} }
} }

View File

@@ -1,10 +1,10 @@
import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { TestBed } from '@/testing';
import { of, throwError } from 'rxjs'; import { ReplaySubject, firstValueFrom, of, share, throwError } from 'rxjs';
import { mockProvider } from '../../test/auto-mock'; import { vi } from 'vitest';
import { AuthStateService } from '../auth-state/auth-state.service'; import { AuthStateService } from '../auth-state/auth-state.service';
import { ConfigurationService } from '../config/config.service'; import { ConfigurationService } from '../config/config.service';
import { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
import { CallbackContext } from '../flows/callback-context'; import type { CallbackContext } from '../flows/callback-context';
import { FlowsDataService } from '../flows/flows-data.service'; import { FlowsDataService } from '../flows/flows-data.service';
import { ResetAuthDataService } from '../flows/reset-auth-data.service'; import { ResetAuthDataService } from '../flows/reset-auth-data.service';
import { RefreshSessionIframeService } from '../iframe/refresh-session-iframe.service'; import { RefreshSessionIframeService } from '../iframe/refresh-session-iframe.service';
@@ -12,6 +12,7 @@ import { LoggerService } from '../logging/logger.service';
import { EventTypes } from '../public-events/event-types'; import { EventTypes } from '../public-events/event-types';
import { PublicEventsService } from '../public-events/public-events.service'; import { PublicEventsService } from '../public-events/public-events.service';
import { StoragePersistenceService } from '../storage/storage-persistence.service'; import { StoragePersistenceService } from '../storage/storage-persistence.service';
import { mockProvider } from '../testing/mock';
import { UserService } from '../user-data/user.service'; import { UserService } from '../user-data/user.service';
import { FlowHelper } from '../utils/flowHelper/flow-helper.service'; import { FlowHelper } from '../utils/flowHelper/flow-helper.service';
import { IntervalService } from './interval.service'; import { IntervalService } from './interval.service';
@@ -32,9 +33,11 @@ describe('PeriodicallyTokenCheckService', () => {
let publicEventsService: PublicEventsService; let publicEventsService: PublicEventsService;
beforeEach(() => { beforeEach(() => {
vi.useFakeTimers();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [],
providers: [ providers: [
PeriodicallyTokenCheckService,
mockProvider(ResetAuthDataService), mockProvider(ResetAuthDataService),
FlowHelper, FlowHelper,
mockProvider(FlowsDataService), mockProvider(FlowsDataService),
@@ -49,9 +52,6 @@ describe('PeriodicallyTokenCheckService', () => {
mockProvider(ConfigurationService), mockProvider(ConfigurationService),
], ],
}); });
});
beforeEach(() => {
periodicallyTokenCheckService = TestBed.inject( periodicallyTokenCheckService = TestBed.inject(
PeriodicallyTokenCheckService PeriodicallyTokenCheckService
); );
@@ -68,14 +68,18 @@ describe('PeriodicallyTokenCheckService', () => {
publicEventsService = TestBed.inject(PublicEventsService); publicEventsService = TestBed.inject(PublicEventsService);
configurationService = TestBed.inject(ConfigurationService); configurationService = TestBed.inject(ConfigurationService);
spyOn(intervalService, 'startPeriodicTokenCheck').and.returnValue(of(null)); vi.spyOn(intervalService, 'startPeriodicTokenCheck').mockReturnValue(
of(null)
);
}); });
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => { afterEach(() => {
if (!!intervalService.runTokenValidationRunning?.unsubscribe) { if (intervalService?.runTokenValidationRunning?.unsubscribe) {
intervalService.runTokenValidationRunning.unsubscribe(); intervalService.runTokenValidationRunning.unsubscribe();
intervalService.runTokenValidationRunning = null; intervalService.runTokenValidationRunning = null;
} }
vi.useRealTimers();
}); });
it('should create', () => { it('should create', () => {
@@ -83,164 +87,200 @@ describe('PeriodicallyTokenCheckService', () => {
}); });
describe('startTokenValidationPeriodically', () => { describe('startTokenValidationPeriodically', () => {
it('returns if no config has silentrenew enabled', waitForAsync(() => { beforeEach(() => {
vi.useFakeTimers();
});
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => {
vi.useRealTimers();
});
it('returns if no config has silentrenew enabled', async () => {
const configs = [ const configs = [
{ silentRenew: false, configId: 'configId1' }, { silentRenew: false, configId: 'configId1' },
{ silentRenew: false, configId: 'configId2' }, { silentRenew: false, configId: 'configId2' },
]; ];
const result = const result = await firstValueFrom(
periodicallyTokenCheckService.startTokenValidationPeriodically( periodicallyTokenCheckService.startTokenValidationPeriodically(
configs, configs,
configs[0] configs[0]!
); )
);
expect(result).toBeUndefined(); expect(result).toBeUndefined();
})); });
it('returns if runTokenValidationRunning', waitForAsync(() => { it('returns if runTokenValidationRunning', async () => {
const configs = [{ silentRenew: true, configId: 'configId1' }]; const configs = [{ silentRenew: true, configId: 'configId1' }];
spyOn(intervalService, 'isTokenValidationRunning').and.returnValue(true); vi.spyOn(intervalService, 'isTokenValidationRunning').mockReturnValue(
true
);
const result = const result = await firstValueFrom(
periodicallyTokenCheckService.startTokenValidationPeriodically( periodicallyTokenCheckService.startTokenValidationPeriodically(
configs, configs,
configs[0] configs[0]!
); )
);
expect(result).toBeUndefined(); expect(result).toBeUndefined();
})); });
it('interval calls resetSilentRenewRunning when current flow is CodeFlowWithRefreshTokens', fakeAsync(() => { it('interval calls resetSilentRenewRunning when current flow is CodeFlowWithRefreshTokens', async () => {
const configs = [ const configs = [
{ silentRenew: true, configId: 'configId1', tokenRefreshInSeconds: 1 }, { silentRenew: true, configId: 'configId1', tokenRefreshInSeconds: 1 },
]; ];
spyOn( vi.spyOn(
periodicallyTokenCheckService as any, periodicallyTokenCheckService as any,
'shouldStartPeriodicallyCheckForConfig' 'shouldStartPeriodicallyCheckForConfig'
).and.returnValue(true); ).mockReturnValue(true);
const isCurrentFlowCodeFlowWithRefreshTokensSpy = spyOn( const isCurrentFlowCodeFlowWithRefreshTokensSpy = vi
flowHelper, .spyOn(flowHelper, 'isCurrentFlowCodeFlowWithRefreshTokens')
'isCurrentFlowCodeFlowWithRefreshTokens' .mockReturnValue(true);
).and.returnValue(true); const resetSilentRenewRunningSpy = vi.spyOn(
const resetSilentRenewRunningSpy = spyOn(
flowsDataService, flowsDataService,
'resetSilentRenewRunning' 'resetSilentRenewRunning'
); );
spyOn( vi.spyOn(
refreshSessionRefreshTokenService, refreshSessionRefreshTokenService,
'refreshSessionWithRefreshTokens' 'refreshSessionWithRefreshTokens'
).and.returnValue(of({} as CallbackContext)); ).mockReturnValue(of({} as CallbackContext));
spyOn(configurationService, 'getOpenIDConfiguration').and.returnValue( vi.spyOn(configurationService, 'getOpenIDConfiguration').mockReturnValue(
of(configs[0]) of(configs[0]!)
); );
periodicallyTokenCheckService.startTokenValidationPeriodically( periodicallyTokenCheckService.startTokenValidationPeriodically(
configs, configs,
configs[0] configs[0]!
); );
tick(1000); await vi.advanceTimersByTimeAsync(1000);
intervalService.runTokenValidationRunning?.unsubscribe(); intervalService.runTokenValidationRunning?.unsubscribe();
intervalService.runTokenValidationRunning = null; intervalService.runTokenValidationRunning = null;
expect(isCurrentFlowCodeFlowWithRefreshTokensSpy).toHaveBeenCalled(); expect(isCurrentFlowCodeFlowWithRefreshTokensSpy).toHaveBeenCalled();
expect(resetSilentRenewRunningSpy).toHaveBeenCalled(); expect(resetSilentRenewRunningSpy).toHaveBeenCalled();
})); });
it('interval calls resetSilentRenewRunning in case of error when current flow is CodeFlowWithRefreshTokens', fakeAsync(() => { it('interval calls resetSilentRenewRunning in case of error when current flow is CodeFlowWithRefreshTokens', async () => {
const configs = [ const configs = [
{ silentRenew: true, configId: 'configId1', tokenRefreshInSeconds: 1 }, { silentRenew: true, configId: 'configId1', tokenRefreshInSeconds: 1 },
]; ];
spyOn( vi.spyOn(
periodicallyTokenCheckService as any, periodicallyTokenCheckService as any,
'shouldStartPeriodicallyCheckForConfig' 'shouldStartPeriodicallyCheckForConfig'
).and.returnValue(true); ).mockReturnValue(true);
const resetSilentRenewRunning = spyOn( const resetSilentRenewRunning = vi.spyOn(
flowsDataService, flowsDataService,
'resetSilentRenewRunning' 'resetSilentRenewRunning'
); );
spyOn( vi.spyOn(
flowHelper, flowHelper,
'isCurrentFlowCodeFlowWithRefreshTokens' 'isCurrentFlowCodeFlowWithRefreshTokens'
).and.returnValue(true); ).mockReturnValue(true);
spyOn( vi.spyOn(
refreshSessionRefreshTokenService, refreshSessionRefreshTokenService,
'refreshSessionWithRefreshTokens' 'refreshSessionWithRefreshTokens'
).and.returnValue(throwError(() => new Error('error'))); ).mockReturnValue(throwError(() => new Error('error')));
spyOn(configurationService, 'getOpenIDConfiguration').and.returnValue( vi.spyOn(configurationService, 'getOpenIDConfiguration').mockReturnValue(
of(configs[0]) of(configs[0]!)
); );
periodicallyTokenCheckService.startTokenValidationPeriodically( try {
configs, const test$ = periodicallyTokenCheckService
configs[0] .startTokenValidationPeriodically(configs, configs[0]!)
); .pipe(
share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: true,
})
);
tick(1000); test$.subscribe();
expect( await vi.advanceTimersByTimeAsync(1000);
periodicallyTokenCheckService.startTokenValidationPeriodically
).toThrowError();
expect(resetSilentRenewRunning).toHaveBeenCalledOnceWith(configs[0]);
}));
it('interval throws silent renew failed event with data in case of an error', fakeAsync(() => { 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 () => {
const configs = [ const configs = [
{ silentRenew: true, configId: 'configId1', tokenRefreshInSeconds: 1 }, { silentRenew: true, configId: 'configId1', tokenRefreshInSeconds: 1 },
]; ];
spyOn( vi.spyOn(
periodicallyTokenCheckService as any, periodicallyTokenCheckService as any,
'shouldStartPeriodicallyCheckForConfig' 'shouldStartPeriodicallyCheckForConfig'
).and.returnValue(true); ).mockReturnValue(true);
spyOn(flowsDataService, 'resetSilentRenewRunning'); vi.spyOn(flowsDataService, 'resetSilentRenewRunning');
const publicEventsServiceSpy = spyOn(publicEventsService, 'fireEvent'); const publicEventsServiceSpy = vi.spyOn(publicEventsService, 'fireEvent');
spyOn( vi.spyOn(
flowHelper, flowHelper,
'isCurrentFlowCodeFlowWithRefreshTokens' 'isCurrentFlowCodeFlowWithRefreshTokens'
).and.returnValue(true); ).mockReturnValue(true);
spyOn( vi.spyOn(
refreshSessionRefreshTokenService, refreshSessionRefreshTokenService,
'refreshSessionWithRefreshTokens' 'refreshSessionWithRefreshTokens'
).and.returnValue(throwError(() => new Error('error'))); ).mockReturnValue(throwError(() => new Error('error')));
spyOn(configurationService, 'getOpenIDConfiguration').and.returnValue( vi.spyOn(configurationService, 'getOpenIDConfiguration').mockReturnValue(
of(configs[0]) of(configs[0]!)
); );
periodicallyTokenCheckService.startTokenValidationPeriodically( try {
configs, const test$ = periodicallyTokenCheckService
configs[0] .startTokenValidationPeriodically(configs, configs[0]!)
); .pipe(
share({
connector: () => new ReplaySubject(1),
resetOnComplete: false,
resetOnError: false,
resetOnRefCountZero: false,
})
);
tick(1000); test$.subscribe();
expect( await vi.advanceTimersByTimeAsync(1000);
periodicallyTokenCheckService.startTokenValidationPeriodically
).toThrowError();
expect(publicEventsServiceSpy.calls.allArgs()).toEqual([
[EventTypes.SilentRenewStarted],
[EventTypes.SilentRenewFailed, new Error('error')],
]);
}));
it('calls resetAuthorizationData and returns if no silent renew is configured', fakeAsync(() => { 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 () => {
const configs = [ const configs = [
{ silentRenew: true, configId: 'configId1', tokenRefreshInSeconds: 1 }, { silentRenew: true, configId: 'configId1', tokenRefreshInSeconds: 1 },
]; ];
spyOn( vi.spyOn(
periodicallyTokenCheckService as any, periodicallyTokenCheckService as any,
'shouldStartPeriodicallyCheckForConfig' 'shouldStartPeriodicallyCheckForConfig'
).and.returnValue(true); ).mockReturnValue(true);
const configSpy = spyOn(configurationService, 'getOpenIDConfiguration'); const configSpy = vi.spyOn(
configurationService,
'getOpenIDConfiguration'
);
const configWithoutSilentRenew = { const configWithoutSilentRenew = {
silentRenew: false, silentRenew: false,
configId: 'configId1', configId: 'configId1',
@@ -248,68 +288,70 @@ describe('PeriodicallyTokenCheckService', () => {
}; };
const configWithoutSilentRenew$ = of(configWithoutSilentRenew); const configWithoutSilentRenew$ = of(configWithoutSilentRenew);
configSpy.and.returnValue(configWithoutSilentRenew$); configSpy.mockReturnValue(configWithoutSilentRenew$);
const resetAuthorizationDataSpy = spyOn( const resetAuthorizationDataSpy = vi.spyOn(
resetAuthDataService, resetAuthDataService,
'resetAuthorizationData' 'resetAuthorizationData'
); );
periodicallyTokenCheckService.startTokenValidationPeriodically( periodicallyTokenCheckService.startTokenValidationPeriodically(
configs, configs,
configs[0] configs[0]!
); );
tick(1000); await vi.advanceTimersByTimeAsync(1000);
intervalService.runTokenValidationRunning?.unsubscribe(); intervalService.runTokenValidationRunning?.unsubscribe();
intervalService.runTokenValidationRunning = null; intervalService.runTokenValidationRunning = null;
expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1); expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1);
expect(resetAuthorizationDataSpy).toHaveBeenCalledOnceWith( expect(resetAuthorizationDataSpy).toHaveBeenCalledExactlyOnceWith(
configWithoutSilentRenew, configWithoutSilentRenew,
configs configs
); );
})); });
it('calls refreshSessionWithRefreshTokens if current flow is Code flow with refresh tokens', fakeAsync(() => { it('calls refreshSessionWithRefreshTokens if current flow is Code flow with refresh tokens', async () => {
spyOn( vi.spyOn(
flowHelper, flowHelper,
'isCurrentFlowCodeFlowWithRefreshTokens' 'isCurrentFlowCodeFlowWithRefreshTokens'
).and.returnValue(true); ).mockReturnValue(true);
spyOn( vi.spyOn(
periodicallyTokenCheckService as any, periodicallyTokenCheckService as any,
'shouldStartPeriodicallyCheckForConfig' 'shouldStartPeriodicallyCheckForConfig'
).and.returnValue(true); ).mockReturnValue(true);
spyOn(storagePersistenceService, 'read').and.returnValue({}); vi.spyOn(storagePersistenceService, 'read').mockReturnValue({});
const configs = [ const configs = [
{ configId: 'configId1', silentRenew: true, tokenRefreshInSeconds: 1 }, { configId: 'configId1', silentRenew: true, tokenRefreshInSeconds: 1 },
]; ];
spyOn(configurationService, 'getOpenIDConfiguration').and.returnValue( vi.spyOn(configurationService, 'getOpenIDConfiguration').mockReturnValue(
of(configs[0] as OpenIdConfiguration) of(configs[0] as OpenIdConfiguration)
); );
const refreshSessionWithRefreshTokensSpy = spyOn( const refreshSessionWithRefreshTokensSpy = vi
refreshSessionRefreshTokenService, .spyOn(
'refreshSessionWithRefreshTokens' refreshSessionRefreshTokenService,
).and.returnValue(of({} as CallbackContext)); 'refreshSessionWithRefreshTokens'
)
.mockReturnValue(of({} as CallbackContext));
periodicallyTokenCheckService.startTokenValidationPeriodically( periodicallyTokenCheckService.startTokenValidationPeriodically(
configs, configs,
configs[0] configs[0]!
); );
tick(1000); await vi.advanceTimersByTimeAsync(1000);
intervalService.runTokenValidationRunning?.unsubscribe(); intervalService.runTokenValidationRunning?.unsubscribe();
intervalService.runTokenValidationRunning = null; intervalService.runTokenValidationRunning = null;
expect(refreshSessionWithRefreshTokensSpy).toHaveBeenCalled(); expect(refreshSessionWithRefreshTokensSpy).toHaveBeenCalled();
})); });
}); });
describe('shouldStartPeriodicallyCheckForConfig', () => { describe('shouldStartPeriodicallyCheckForConfig', () => {
it('returns false when there is no IdToken', () => { it('returns false when there is no IdToken', () => {
spyOn(authStateService, 'getIdToken').and.returnValue(''); vi.spyOn(authStateService, 'getIdToken').mockReturnValue('');
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false); vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(false);
spyOn(userService, 'getUserDataFromStore').and.returnValue( vi.spyOn(userService, 'getUserDataFromStore').mockReturnValue(
'some-userdata' 'some-userdata'
); );
@@ -317,13 +359,13 @@ describe('PeriodicallyTokenCheckService', () => {
periodicallyTokenCheckService as any periodicallyTokenCheckService as any
).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' }); ).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' });
expect(result).toBeFalse(); expect(result).toBeFalsy();
}); });
it('returns false when silent renew is running', () => { it('returns false when silent renew is running', () => {
spyOn(authStateService, 'getIdToken').and.returnValue('idToken'); vi.spyOn(authStateService, 'getIdToken').mockReturnValue('idToken');
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true); vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(true);
spyOn(userService, 'getUserDataFromStore').and.returnValue( vi.spyOn(userService, 'getUserDataFromStore').mockReturnValue(
'some-userdata' 'some-userdata'
); );
@@ -331,14 +373,14 @@ describe('PeriodicallyTokenCheckService', () => {
periodicallyTokenCheckService as any periodicallyTokenCheckService as any
).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' }); ).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' });
expect(result).toBeFalse(); expect(result).toBeFalsy();
}); });
it('returns false when code flow is in progress', () => { it('returns false when code flow is in progress', () => {
spyOn(authStateService, 'getIdToken').and.returnValue('idToken'); vi.spyOn(authStateService, 'getIdToken').mockReturnValue('idToken');
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false); vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(false);
spyOn(flowsDataService, 'isCodeFlowInProgress').and.returnValue(true); vi.spyOn(flowsDataService, 'isCodeFlowInProgress').mockReturnValue(true);
spyOn(userService, 'getUserDataFromStore').and.returnValue( vi.spyOn(userService, 'getUserDataFromStore').mockReturnValue(
'some-userdata' 'some-userdata'
); );
@@ -346,87 +388,87 @@ describe('PeriodicallyTokenCheckService', () => {
periodicallyTokenCheckService as any periodicallyTokenCheckService as any
).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' }); ).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' });
expect(result).toBeFalse(); expect(result).toBeFalsy();
}); });
it('returns false when there is no userdata from the store', () => { it('returns false when there is no userdata from the store', () => {
spyOn(authStateService, 'getIdToken').and.returnValue('idToken'); vi.spyOn(authStateService, 'getIdToken').mockReturnValue('idToken');
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true); vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(true);
spyOn(userService, 'getUserDataFromStore').and.returnValue(null); vi.spyOn(userService, 'getUserDataFromStore').mockReturnValue(null);
const result = ( const result = (
periodicallyTokenCheckService as any periodicallyTokenCheckService as any
).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' }); ).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' });
expect(result).toBeFalse(); expect(result).toBeFalsy();
}); });
it('returns true when there is userDataFromStore, silentrenew is not running and there is an idtoken', () => { it('returns true when there is userDataFromStore, silentrenew is not running and there is an idtoken', () => {
spyOn(authStateService, 'getIdToken').and.returnValue('idToken'); vi.spyOn(authStateService, 'getIdToken').mockReturnValue('idToken');
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false); vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(false);
spyOn(userService, 'getUserDataFromStore').and.returnValue( vi.spyOn(userService, 'getUserDataFromStore').mockReturnValue(
'some-userdata' 'some-userdata'
); );
spyOn( vi.spyOn(
authStateService, authStateService,
'hasIdTokenExpiredAndRenewCheckIsEnabled' 'hasIdTokenExpiredAndRenewCheckIsEnabled'
).and.returnValue(true); ).mockReturnValue(true);
spyOn( vi.spyOn(
authStateService, authStateService,
'hasAccessTokenExpiredIfExpiryExists' 'hasAccessTokenExpiredIfExpiryExists'
).and.returnValue(true); ).mockReturnValue(true);
const result = ( const result = (
periodicallyTokenCheckService as any periodicallyTokenCheckService as any
).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' }); ).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' });
expect(result).toBeTrue(); expect(result).toBeTruthy();
}); });
it('returns false if tokens are not expired', () => { it('returns false if tokens are not expired', () => {
spyOn(authStateService, 'getIdToken').and.returnValue('idToken'); vi.spyOn(authStateService, 'getIdToken').mockReturnValue('idToken');
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false); vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(false);
spyOn(userService, 'getUserDataFromStore').and.returnValue( vi.spyOn(userService, 'getUserDataFromStore').mockReturnValue(
'some-userdata' 'some-userdata'
); );
spyOn( vi.spyOn(
authStateService, authStateService,
'hasIdTokenExpiredAndRenewCheckIsEnabled' 'hasIdTokenExpiredAndRenewCheckIsEnabled'
).and.returnValue(false); ).mockReturnValue(false);
spyOn( vi.spyOn(
authStateService, authStateService,
'hasAccessTokenExpiredIfExpiryExists' 'hasAccessTokenExpiredIfExpiryExists'
).and.returnValue(false); ).mockReturnValue(false);
const result = ( const result = (
periodicallyTokenCheckService as any periodicallyTokenCheckService as any
).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' }); ).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' });
expect(result).toBeFalse(); expect(result).toBeFalsy();
}); });
it('returns true if tokens are expired', () => { it('returns true if tokens are expired', () => {
spyOn(authStateService, 'getIdToken').and.returnValue('idToken'); vi.spyOn(authStateService, 'getIdToken').mockReturnValue('idToken');
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false); vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(false);
spyOn(userService, 'getUserDataFromStore').and.returnValue( vi.spyOn(userService, 'getUserDataFromStore').mockReturnValue(
'some-userdata' 'some-userdata'
); );
spyOn( vi.spyOn(
authStateService, authStateService,
'hasIdTokenExpiredAndRenewCheckIsEnabled' 'hasIdTokenExpiredAndRenewCheckIsEnabled'
).and.returnValue(true); ).mockReturnValue(true);
spyOn( vi.spyOn(
authStateService, authStateService,
'hasAccessTokenExpiredIfExpiryExists' 'hasAccessTokenExpiredIfExpiryExists'
).and.returnValue(true); ).mockReturnValue(true);
const result = ( const result = (
periodicallyTokenCheckService as any periodicallyTokenCheckService as any
).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' }); ).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' });
expect(result).toBeTrue(); expect(result).toBeTruthy();
}); });
}); });
}); });

View File

@@ -1,10 +1,10 @@
import { inject, Injectable } from 'injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import { forkJoin, Observable, of, throwError } from 'rxjs'; import { type Observable, ReplaySubject, forkJoin, of, throwError } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators'; import { catchError, map, share, switchMap } from 'rxjs/operators';
import { AuthStateService } from '../auth-state/auth-state.service'; import { AuthStateService } from '../auth-state/auth-state.service';
import { ConfigurationService } from '../config/config.service'; import { ConfigurationService } from '../config/config.service';
import { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
import { CallbackContext } from '../flows/callback-context'; import type { CallbackContext } from '../flows/callback-context';
import { FlowsDataService } from '../flows/flows-data.service'; import { FlowsDataService } from '../flows/flows-data.service';
import { ResetAuthDataService } from '../flows/reset-auth-data.service'; import { ResetAuthDataService } from '../flows/reset-auth-data.service';
import { RefreshSessionIframeService } from '../iframe/refresh-session-iframe.service'; import { RefreshSessionIframeService } from '../iframe/refresh-session-iframe.service';
@@ -52,16 +52,16 @@ export class PeriodicallyTokenCheckService {
startTokenValidationPeriodically( startTokenValidationPeriodically(
allConfigs: OpenIdConfiguration[], allConfigs: OpenIdConfiguration[],
currentConfig: OpenIdConfiguration currentConfig: OpenIdConfiguration
): void { ): Observable<undefined> {
const configsWithSilentRenewEnabled = const configsWithSilentRenewEnabled =
this.getConfigsWithSilentRenewEnabled(allConfigs); this.getConfigsWithSilentRenewEnabled(allConfigs);
if (configsWithSilentRenewEnabled.length <= 0) { if (configsWithSilentRenewEnabled.length <= 0) {
return; return of(undefined);
} }
if (this.intervalService.isTokenValidationRunning()) { if (this.intervalService.isTokenValidationRunning()) {
return; return of(undefined);
} }
const refreshTimeInSeconds = this.getSmallestRefreshTimeFromConfigs( const refreshTimeInSeconds = this.getSmallestRefreshTimeFromConfigs(
@@ -75,46 +75,56 @@ export class PeriodicallyTokenCheckService {
[id: string]: Observable<boolean | CallbackContext | null>; [id: string]: Observable<boolean | CallbackContext | null>;
} = {}; } = {};
configsWithSilentRenewEnabled.forEach((config) => { for (const config of configsWithSilentRenewEnabled) {
const identifier = config.configId as string; const identifier = config.configId as string;
const refreshEvent = this.getRefreshEvent(config, allConfigs); const refreshEvent = this.getRefreshEvent(config, allConfigs);
objectWithConfigIdsAndRefreshEvent[identifier] = refreshEvent; objectWithConfigIdsAndRefreshEvent[identifier] = refreshEvent;
}); }
return forkJoin(objectWithConfigIdsAndRefreshEvent); return forkJoin(objectWithConfigIdsAndRefreshEvent);
}) })
); );
this.intervalService.runTokenValidationRunning = periodicallyCheck$ const o$ = periodicallyCheck$.pipe(
.pipe(catchError((error) => throwError(() => new Error(error)))) catchError((error) => {
.subscribe({ this.loggerService.logError(
next: (objectWithConfigIds) => { currentConfig,
for (const [configId, _] of Object.entries(objectWithConfigIds)) { 'silent renew failed!',
this.configurationService error
.getOpenIDConfiguration(configId) );
.subscribe((config) => { return throwError(() => error);
this.loggerService.logDebug( }),
config, map((objectWithConfigIds) => {
'silent renew, periodic check finished!' for (const [configId, _] of Object.entries(objectWithConfigIds)) {
); this.configurationService
.getOpenIDConfiguration(configId)
.subscribe((config) => {
this.loggerService.logDebug(
config,
'silent renew, periodic check finished!'
);
if ( if (
this.flowHelper.isCurrentFlowCodeFlowWithRefreshTokens(config) this.flowHelper.isCurrentFlowCodeFlowWithRefreshTokens(config)
) { ) {
this.flowsDataService.resetSilentRenewRunning(config); this.flowsDataService.resetSilentRenewRunning(config);
} }
}); });
} return undefined;
}, }
error: (error) => { }),
this.loggerService.logError( share({
currentConfig, connector: () => new ReplaySubject(1),
'silent renew failed!', resetOnError: false,
error resetOnComplete: false,
); resetOnRefCountZero: false,
}, })
}); );
this.intervalService.runTokenValidationRunning = o$.subscribe({});
return o$;
} }
private getRefreshEvent( private getRefreshEvent(

View File

@@ -1,10 +1,11 @@
import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { TestBed } from '@/testing';
import { of, throwError } from 'rxjs'; import { firstValueFrom, of, throwError } from 'rxjs';
import { mockProvider } from '../../test/auto-mock'; import { vi } from 'vitest';
import { CallbackContext } from '../flows/callback-context'; import type { CallbackContext } from '../flows/callback-context';
import { FlowsService } from '../flows/flows.service'; import { FlowsService } from '../flows/flows.service';
import { ResetAuthDataService } from '../flows/reset-auth-data.service'; import { ResetAuthDataService } from '../flows/reset-auth-data.service';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
import { mockProvider } from '../testing/mock';
import { IntervalService } from './interval.service'; import { IntervalService } from './interval.service';
import { RefreshSessionRefreshTokenService } from './refresh-session-refresh-token.service'; import { RefreshSessionRefreshTokenService } from './refresh-session-refresh-token.service';
@@ -15,6 +16,7 @@ describe('RefreshSessionRefreshTokenService', () => {
let flowsService: FlowsService; let flowsService: FlowsService;
beforeEach(() => { beforeEach(() => {
vi.useFakeTimers();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [],
providers: [ providers: [
@@ -25,9 +27,6 @@ describe('RefreshSessionRefreshTokenService', () => {
mockProvider(IntervalService), mockProvider(IntervalService),
], ],
}); });
});
beforeEach(() => {
flowsService = TestBed.inject(FlowsService); flowsService = TestBed.inject(FlowsService);
refreshSessionRefreshTokenService = TestBed.inject( refreshSessionRefreshTokenService = TestBed.inject(
RefreshSessionRefreshTokenService RefreshSessionRefreshTokenService
@@ -36,66 +35,73 @@ describe('RefreshSessionRefreshTokenService', () => {
resetAuthDataService = TestBed.inject(ResetAuthDataService); resetAuthDataService = TestBed.inject(ResetAuthDataService);
}); });
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => {
vi.useRealTimers();
});
it('should create', () => { it('should create', () => {
expect(refreshSessionRefreshTokenService).toBeTruthy(); expect(refreshSessionRefreshTokenService).toBeTruthy();
}); });
describe('refreshSessionWithRefreshTokens', () => { describe('refreshSessionWithRefreshTokens', () => {
it('calls flowsService.processRefreshToken()', waitForAsync(() => { it('calls flowsService.processRefreshToken()', async () => {
const spy = spyOn(flowsService, 'processRefreshToken').and.returnValue( const spy = vi
of({} as CallbackContext) .spyOn(flowsService, 'processRefreshToken')
); .mockReturnValue(of({} as CallbackContext));
refreshSessionRefreshTokenService await firstValueFrom(
.refreshSessionWithRefreshTokens({ configId: 'configId1' }, [ refreshSessionRefreshTokenService.refreshSessionWithRefreshTokens(
{ configId: 'configId1' }, { configId: 'configId1' },
]) [{ configId: 'configId1' }]
.subscribe(() => { )
expect(spy).toHaveBeenCalled(); );
}); expect(spy).toHaveBeenCalled();
})); });
it('resetAuthorizationData in case of error', waitForAsync(() => { it('resetAuthorizationData in case of error', async () => {
spyOn(flowsService, 'processRefreshToken').and.returnValue( vi.spyOn(flowsService, 'processRefreshToken').mockReturnValue(
throwError(() => new Error('error')) throwError(() => new Error('error'))
); );
const resetSilentRenewRunningSpy = spyOn( const resetSilentRenewRunningSpy = vi.spyOn(
resetAuthDataService, resetAuthDataService,
'resetAuthorizationData' 'resetAuthorizationData'
); );
refreshSessionRefreshTokenService try {
.refreshSessionWithRefreshTokens({ configId: 'configId1' }, [ await firstValueFrom(
{ configId: 'configId1' }, refreshSessionRefreshTokenService.refreshSessionWithRefreshTokens(
]) { configId: 'configId1' },
.subscribe({ [{ configId: 'configId1' }]
error: (err) => { )
expect(resetSilentRenewRunningSpy).toHaveBeenCalled(); );
expect(err).toBeTruthy(); } catch (err: any) {
}, expect(resetSilentRenewRunningSpy).toHaveBeenCalled();
}); expect(err).toBeTruthy();
})); }
});
it('finalize with stopPeriodicTokenCheck in case of error', fakeAsync(() => { it('finalize with stopPeriodicTokenCheck in case of error', async () => {
spyOn(flowsService, 'processRefreshToken').and.returnValue( vi.spyOn(flowsService, 'processRefreshToken').mockReturnValue(
throwError(() => new Error('error')) throwError(() => new Error('error'))
); );
const stopPeriodicallyTokenCheckSpy = spyOn( const stopPeriodicallyTokenCheckSpy = vi.spyOn(
intervalService, intervalService,
'stopPeriodicTokenCheck' 'stopPeriodicTokenCheck'
); );
refreshSessionRefreshTokenService try {
.refreshSessionWithRefreshTokens({ configId: 'configId1' }, [ await firstValueFrom(
{ configId: 'configId1' }, refreshSessionRefreshTokenService.refreshSessionWithRefreshTokens(
]) { configId: 'configId1' },
.subscribe({ [{ configId: 'configId1' }]
error: (err) => { )
expect(err).toBeTruthy(); );
}, } catch (err: any) {
}); expect(err).toBeTruthy();
tick(); }
await vi.advanceTimersByTimeAsync(0);
expect(stopPeriodicallyTokenCheckSpy).toHaveBeenCalled(); expect(stopPeriodicallyTokenCheckSpy).toHaveBeenCalled();
})); });
}); });
}); });

View File

@@ -1,8 +1,8 @@
import { inject, Injectable } from 'injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import { Observable, throwError } from 'rxjs'; import { type Observable, throwError } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators'; import { catchError, finalize } from 'rxjs/operators';
import { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
import { CallbackContext } from '../flows/callback-context'; import type { CallbackContext } from '../flows/callback-context';
import { FlowsService } from '../flows/flows.service'; import { FlowsService } from '../flows/flows.service';
import { ResetAuthDataService } from '../flows/reset-auth-data.service'; import { ResetAuthDataService } from '../flows/reset-auth-data.service';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';

View File

@@ -1,17 +1,24 @@
import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { TestBed, spyOnProperty } from '@/testing';
import { of, throwError } from 'rxjs'; import {
import { delay } from 'rxjs/operators'; EmptyError,
import { mockProvider } from '../../test/auto-mock'; 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 { AuthStateService } from '../auth-state/auth-state.service';
import { AuthWellKnownService } from '../config/auth-well-known/auth-well-known.service'; import { AuthWellKnownService } from '../config/auth-well-known/auth-well-known.service';
import { CallbackContext } from '../flows/callback-context'; import type { CallbackContext } from '../flows/callback-context';
import { FlowsDataService } from '../flows/flows-data.service'; import { FlowsDataService } from '../flows/flows-data.service';
import { RefreshSessionIframeService } from '../iframe/refresh-session-iframe.service'; import { RefreshSessionIframeService } from '../iframe/refresh-session-iframe.service';
import { SilentRenewService } from '../iframe/silent-renew.service'; import { SilentRenewService } from '../iframe/silent-renew.service';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
import { LoginResponse } from '../login/login-response'; import type { LoginResponse } from '../login/login-response';
import { PublicEventsService } from '../public-events/public-events.service'; import { PublicEventsService } from '../public-events/public-events.service';
import { StoragePersistenceService } from '../storage/storage-persistence.service'; import { StoragePersistenceService } from '../storage/storage-persistence.service';
import { mockProvider } from '../testing/mock';
import { UserService } from '../user-data/user.service'; import { UserService } from '../user-data/user.service';
import { FlowHelper } from '../utils/flowHelper/flow-helper.service'; import { FlowHelper } from '../utils/flowHelper/flow-helper.service';
import { RefreshSessionRefreshTokenService } from './refresh-session-refresh-token.service'; import { RefreshSessionRefreshTokenService } from './refresh-session-refresh-token.service';
@@ -21,6 +28,7 @@ import {
} from './refresh-session.service'; } from './refresh-session.service';
describe('RefreshSessionService ', () => { describe('RefreshSessionService ', () => {
vi.useFakeTimers();
let refreshSessionService: RefreshSessionService; let refreshSessionService: RefreshSessionService;
let flowHelper: FlowHelper; let flowHelper: FlowHelper;
let authStateService: AuthStateService; let authStateService: AuthStateService;
@@ -49,9 +57,6 @@ describe('RefreshSessionService ', () => {
mockProvider(PublicEventsService), mockProvider(PublicEventsService),
], ],
}); });
});
beforeEach(() => {
refreshSessionService = TestBed.inject(RefreshSessionService); refreshSessionService = TestBed.inject(RefreshSessionService);
flowsDataService = TestBed.inject(FlowsDataService); flowsDataService = TestBed.inject(FlowsDataService);
flowHelper = TestBed.inject(FlowHelper); flowHelper = TestBed.inject(FlowHelper);
@@ -65,24 +70,29 @@ describe('RefreshSessionService ', () => {
storagePersistenceService = TestBed.inject(StoragePersistenceService); storagePersistenceService = TestBed.inject(StoragePersistenceService);
}); });
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => {
vi.useRealTimers();
});
it('should create', () => { it('should create', () => {
expect(refreshSessionService).toBeTruthy(); expect(refreshSessionService).toBeTruthy();
}); });
describe('userForceRefreshSession', () => { describe('userForceRefreshSession', () => {
it('should persist params refresh when extra custom params given and useRefreshToken is true', waitForAsync(() => { it('should persist params refresh when extra custom params given and useRefreshToken is true', async () => {
spyOn( vi.spyOn(
flowHelper, flowHelper,
'isCurrentFlowCodeFlowWithRefreshTokens' 'isCurrentFlowCodeFlowWithRefreshTokens'
).and.returnValue(true); ).mockReturnValue(true);
spyOn( vi.spyOn(
refreshSessionService as any, refreshSessionService as any,
'startRefreshSession' 'startRefreshSession'
).and.returnValue(of(null)); ).mockReturnValue(of(null));
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
true true
); );
const writeSpy = spyOn(storagePersistenceService, 'write'); const writeSpy = vi.spyOn(storagePersistenceService, 'write');
const allConfigs = [ const allConfigs = [
{ {
configId: 'configId1', configId: 'configId1',
@@ -93,27 +103,30 @@ describe('RefreshSessionService ', () => {
const extraCustomParams = { extra: 'custom' }; const extraCustomParams = { extra: 'custom' };
refreshSessionService await firstValueFrom(
.userForceRefreshSession(allConfigs[0], allConfigs, extraCustomParams) refreshSessionService.userForceRefreshSession(
.subscribe(() => { allConfigs[0]!,
expect(writeSpy).toHaveBeenCalledOnceWith( allConfigs,
'storageCustomParamsRefresh', extraCustomParams
extraCustomParams, )
allConfigs[0] );
); expect(writeSpy).toHaveBeenCalledExactlyOnceWith(
}); 'storageCustomParamsRefresh',
})); extraCustomParams,
allConfigs[0]
);
});
it('should persist storageCustomParamsAuthRequest when extra custom params given and useRefreshToken is false', waitForAsync(() => { it('should persist storageCustomParamsAuthRequest when extra custom params given and useRefreshToken is false', async () => {
spyOn( vi.spyOn(
flowHelper, flowHelper,
'isCurrentFlowCodeFlowWithRefreshTokens' 'isCurrentFlowCodeFlowWithRefreshTokens'
).and.returnValue(true); ).mockReturnValue(true);
spyOn( vi.spyOn(
refreshSessionService as any, refreshSessionService as any,
'startRefreshSession' 'startRefreshSession'
).and.returnValue(of(null)); ).mockReturnValue(of(null));
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
true true
); );
const allConfigs = [ const allConfigs = [
@@ -123,31 +136,34 @@ describe('RefreshSessionService ', () => {
silentRenewTimeoutInSeconds: 10, silentRenewTimeoutInSeconds: 10,
}, },
]; ];
const writeSpy = spyOn(storagePersistenceService, 'write'); const writeSpy = vi.spyOn(storagePersistenceService, 'write');
const extraCustomParams = { extra: 'custom' }; const extraCustomParams = { extra: 'custom' };
refreshSessionService await firstValueFrom(
.userForceRefreshSession(allConfigs[0], allConfigs, extraCustomParams) refreshSessionService.userForceRefreshSession(
.subscribe(() => { allConfigs[0]!,
expect(writeSpy).toHaveBeenCalledOnceWith( allConfigs,
'storageCustomParamsAuthRequest', extraCustomParams
extraCustomParams, )
allConfigs[0] );
); expect(writeSpy).toHaveBeenCalledExactlyOnceWith(
}); 'storageCustomParamsAuthRequest',
})); extraCustomParams,
allConfigs[0]
);
});
it('should NOT persist customparams if no customparams are given', waitForAsync(() => { it('should NOT persist customparams if no customparams are given', async () => {
spyOn( vi.spyOn(
flowHelper, flowHelper,
'isCurrentFlowCodeFlowWithRefreshTokens' 'isCurrentFlowCodeFlowWithRefreshTokens'
).and.returnValue(true); ).mockReturnValue(true);
spyOn( vi.spyOn(
refreshSessionService as any, refreshSessionService as any,
'startRefreshSession' 'startRefreshSession'
).and.returnValue(of(null)); ).mockReturnValue(of(null));
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
true true
); );
const allConfigs = [ const allConfigs = [
@@ -157,20 +173,22 @@ describe('RefreshSessionService ', () => {
silentRenewTimeoutInSeconds: 10, silentRenewTimeoutInSeconds: 10,
}, },
]; ];
const writeSpy = spyOn(storagePersistenceService, 'write'); const writeSpy = vi.spyOn(storagePersistenceService, 'write');
refreshSessionService await firstValueFrom(
.userForceRefreshSession(allConfigs[0], allConfigs) refreshSessionService.userForceRefreshSession(
.subscribe(() => { allConfigs[0]!,
expect(writeSpy).not.toHaveBeenCalled(); allConfigs
}); )
})); );
expect(writeSpy).not.toHaveBeenCalled();
});
it('should call resetSilentRenewRunning in case of an error', waitForAsync(() => { it('should call resetSilentRenewRunning in case of an error', async () => {
spyOn(refreshSessionService, 'forceRefreshSession').and.returnValue( vi.spyOn(refreshSessionService, 'forceRefreshSession').mockReturnValue(
throwError(() => new Error('error')) throwError(() => new Error('error'))
); );
spyOn(flowsDataService, 'resetSilentRenewRunning'); vi.spyOn(flowsDataService, 'resetSilentRenewRunning');
const allConfigs = [ const allConfigs = [
{ {
configId: 'configId1', configId: 'configId1',
@@ -179,28 +197,29 @@ describe('RefreshSessionService ', () => {
}, },
]; ];
refreshSessionService try {
.userForceRefreshSession(allConfigs[0], allConfigs) const result = await firstValueFrom(
.subscribe({ refreshSessionService.userForceRefreshSession(
next: () => { allConfigs[0]!,
fail('It should not return any result.'); allConfigs
}, )
error: (error) => { );
expect(error).toBeInstanceOf(Error); if (result) {
}, expect.fail('It should not return any result.');
complete: () => { } else {
expect( expect(
flowsDataService.resetSilentRenewRunning flowsDataService.resetSilentRenewRunning
).toHaveBeenCalledOnceWith(allConfigs[0]); ).toHaveBeenCalledExactlyOnceWith(allConfigs[0]);
}, }
}); } catch (error: any) {
})); expect(error).toBeInstanceOf(Error);
}
it('should call resetSilentRenewRunning in case of no error', waitForAsync(() => { });
spyOn(refreshSessionService, 'forceRefreshSession').and.returnValue( it('should call resetSilentRenewRunning in case of no error', async () => {
vi.spyOn(refreshSessionService, 'forceRefreshSession').mockReturnValue(
of({} as LoginResponse) of({} as LoginResponse)
); );
spyOn(flowsDataService, 'resetSilentRenewRunning'); vi.spyOn(flowsDataService, 'resetSilentRenewRunning');
const allConfigs = [ const allConfigs = [
{ {
configId: 'configId1', configId: 'configId1',
@@ -209,36 +228,42 @@ describe('RefreshSessionService ', () => {
}, },
]; ];
refreshSessionService try {
.userForceRefreshSession(allConfigs[0], allConfigs) await firstValueFrom(
.subscribe({ refreshSessionService.userForceRefreshSession(
error: () => { allConfigs[0]!,
fail('It should not return any error.'); allConfigs
}, )
complete: () => { );
expect( } catch (err: any) {
flowsDataService.resetSilentRenewRunning if (err instanceof EmptyError) {
).toHaveBeenCalledOnceWith(allConfigs[0]); expect(
}, flowsDataService.resetSilentRenewRunning
}); ).toHaveBeenCalledExactlyOnceWith(allConfigs[0]);
})); } else {
expect.fail('It should not return any error.');
}
}
});
}); });
describe('forceRefreshSession', () => { describe('forceRefreshSession', () => {
it('only calls start refresh session and returns idToken and accessToken if auth is true', waitForAsync(() => { it('only calls start refresh session and returns idToken and accessToken if auth is true', async () => {
spyOn( vi.spyOn(
flowHelper, flowHelper,
'isCurrentFlowCodeFlowWithRefreshTokens' 'isCurrentFlowCodeFlowWithRefreshTokens'
).and.returnValue(true); ).mockReturnValue(true);
spyOn( vi.spyOn(
refreshSessionService as any, refreshSessionService as any,
'startRefreshSession' 'startRefreshSession'
).and.returnValue(of(null)); ).mockReturnValue(of(null));
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
true true
); );
spyOn(authStateService, 'getIdToken').and.returnValue('id-token'); vi.spyOn(authStateService, 'getIdToken').mockReturnValue('id-token');
spyOn(authStateService, 'getAccessToken').and.returnValue('access-token'); vi.spyOn(authStateService, 'getAccessToken').mockReturnValue(
'access-token'
);
const allConfigs = [ const allConfigs = [
{ {
configId: 'configId1', configId: 'configId1',
@@ -246,24 +271,23 @@ describe('RefreshSessionService ', () => {
}, },
]; ];
refreshSessionService const result = await firstValueFrom(
.forceRefreshSession(allConfigs[0], allConfigs) refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
.subscribe((result) => { );
expect(result.idToken).toEqual('id-token'); expect(result.idToken).toEqual('id-token');
expect(result.accessToken).toEqual('access-token'); expect(result.accessToken).toEqual('access-token');
}); });
}));
it('only calls start refresh session and returns null if auth is false', waitForAsync(() => { it('only calls start refresh session and returns null if auth is false', async () => {
spyOn( vi.spyOn(
flowHelper, flowHelper,
'isCurrentFlowCodeFlowWithRefreshTokens' 'isCurrentFlowCodeFlowWithRefreshTokens'
).and.returnValue(true); ).mockReturnValue(true);
spyOn( vi.spyOn(
refreshSessionService as any, refreshSessionService as any,
'startRefreshSession' 'startRefreshSession'
).and.returnValue(of(null)); ).mockReturnValue(of(null));
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false false
); );
const allConfigs = [ const allConfigs = [
@@ -273,36 +297,35 @@ describe('RefreshSessionService ', () => {
}, },
]; ];
refreshSessionService const result = await firstValueFrom(
.forceRefreshSession(allConfigs[0], allConfigs) refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
.subscribe((result) => { );
expect(result).toEqual({ expect(result).toEqual({
isAuthenticated: false, isAuthenticated: false,
errorMessage: '', errorMessage: '',
userData: null, userData: null,
idToken: '', idToken: '',
accessToken: '', accessToken: '',
configId: 'configId1', configId: 'configId1',
}); });
}); });
}));
it('calls start refresh session and waits for completed, returns idtoken and accesstoken if auth is true', waitForAsync(() => { it('calls start refresh session and waits for completed, returns idtoken and accesstoken if auth is true', async () => {
spyOn( vi.spyOn(
flowHelper, flowHelper,
'isCurrentFlowCodeFlowWithRefreshTokens' 'isCurrentFlowCodeFlowWithRefreshTokens'
).and.returnValue(false); ).mockReturnValue(false);
spyOn( vi.spyOn(
refreshSessionService as any, refreshSessionService as any,
'startRefreshSession' 'startRefreshSession'
).and.returnValue(of(null)); ).mockReturnValue(of(null));
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
true true
); );
spyOnProperty( spyOnProperty(
silentRenewService, silentRenewService,
'refreshSessionWithIFrameCompleted$' 'refreshSessionWithIFrameCompleted$'
).and.returnValue( ).mockReturnValue(
of({ of({
authResult: { authResult: {
id_token: 'some-id_token', id_token: 'some-id_token',
@@ -317,30 +340,29 @@ describe('RefreshSessionService ', () => {
}, },
]; ];
refreshSessionService const result = await firstValueFrom(
.forceRefreshSession(allConfigs[0], allConfigs) refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
.subscribe((result) => { );
expect(result.idToken).toBeDefined(); expect(result.idToken).toBeDefined();
expect(result.accessToken).toBeDefined(); expect(result.accessToken).toBeDefined();
}); });
}));
it('calls start refresh session and waits for completed, returns LoginResponse if auth is false', waitForAsync(() => { it('calls start refresh session and waits for completed, returns LoginResponse if auth is false', async () => {
spyOn( vi.spyOn(
flowHelper, flowHelper,
'isCurrentFlowCodeFlowWithRefreshTokens' 'isCurrentFlowCodeFlowWithRefreshTokens'
).and.returnValue(false); ).mockReturnValue(false);
spyOn( vi.spyOn(
refreshSessionService as any, refreshSessionService as any,
'startRefreshSession' 'startRefreshSession'
).and.returnValue(of(null)); ).mockReturnValue(of(null));
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false false
); );
spyOnProperty( spyOnProperty(
silentRenewService, silentRenewService,
'refreshSessionWithIFrameCompleted$' 'refreshSessionWithIFrameCompleted$'
).and.returnValue(of(null)); ).mockReturnValue(of(null));
const allConfigs = [ const allConfigs = [
{ {
configId: 'configId1', configId: 'configId1',
@@ -348,35 +370,36 @@ describe('RefreshSessionService ', () => {
}, },
]; ];
refreshSessionService const result = await firstValueFrom(
.forceRefreshSession(allConfigs[0], allConfigs) refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
.subscribe((result) => { );
expect(result).toEqual({ expect(result).toEqual({
isAuthenticated: false, isAuthenticated: false,
errorMessage: '', errorMessage: '',
userData: null, userData: null,
idToken: '', idToken: '',
accessToken: '', accessToken: '',
configId: 'configId1', configId: 'configId1',
}); });
}); });
}));
it('occurs timeout error and retry mechanism exhausted max retry count throws error', fakeAsync(() => { it('occurs timeout error and retry mechanism exhausted max retry count throws error', async () => {
spyOn( vi.useRealTimers();
vi.useFakeTimers();
vi.spyOn(
flowHelper, flowHelper,
'isCurrentFlowCodeFlowWithRefreshTokens' 'isCurrentFlowCodeFlowWithRefreshTokens'
).and.returnValue(false); ).mockReturnValue(false);
spyOn( vi.spyOn(
refreshSessionService as any, refreshSessionService as any,
'startRefreshSession' 'startRefreshSession'
).and.returnValue(of(null)); ).mockReturnValue(of(null));
spyOnProperty( spyOnProperty(
silentRenewService, silentRenewService,
'refreshSessionWithIFrameCompleted$' 'refreshSessionWithIFrameCompleted$'
).and.returnValue(of(null).pipe(delay(11000))); ).mockReturnValue(of(null).pipe(delay(11000)));
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false false
); );
const allConfigs = [ const allConfigs = [
@@ -386,30 +409,44 @@ describe('RefreshSessionService ', () => {
}, },
]; ];
const resetSilentRenewRunningSpy = spyOn( const resetSilentRenewRunningSpy = vi.spyOn(
flowsDataService, flowsDataService,
'resetSilentRenewRunning' 'resetSilentRenewRunning'
); );
const expectedInvokeCount = MAX_RETRY_ATTEMPTS; const expectedInvokeCount = MAX_RETRY_ATTEMPTS;
refreshSessionService try {
.forceRefreshSession(allConfigs[0], allConfigs) const o$ = refreshSessionService
.subscribe({ .forceRefreshSession(allConfigs[0]!, allConfigs)
next: () => { .pipe(
fail('It should not return any result.'); share({
}, connector: () => new ReplaySubject(1),
error: (error) => { resetOnError: false,
expect(error).toBeInstanceOf(Error); resetOnComplete: false,
expect(resetSilentRenewRunningSpy).toHaveBeenCalledTimes( resetOnRefCountZero: true,
expectedInvokeCount })
); );
},
});
tick(allConfigs[0].silentRenewTimeoutInSeconds * 10000); o$.subscribe();
}));
it('occurs unknown error throws it to subscriber', fakeAsync(() => { await vi.advanceTimersByTimeAsync(
allConfigs[0]!.silentRenewTimeoutInSeconds * 10000
);
const result = await firstValueFrom(o$);
if (result) {
expect.fail('It should not return any result.');
}
} catch (error: any) {
expect(error).toBeInstanceOf(Error);
expect(resetSilentRenewRunningSpy).toHaveBeenCalledTimes(
expectedInvokeCount
);
}
});
it('occurs unknown error throws it to subscriber', async () => {
const allConfigs = [ const allConfigs = [
{ {
configId: 'configId1', configId: 'configId1',
@@ -419,43 +456,41 @@ describe('RefreshSessionService ', () => {
const expectedErrorMessage = 'Test error message'; const expectedErrorMessage = 'Test error message';
spyOn( vi.spyOn(
flowHelper, flowHelper,
'isCurrentFlowCodeFlowWithRefreshTokens' 'isCurrentFlowCodeFlowWithRefreshTokens'
).and.returnValue(false); ).mockReturnValue(false);
spyOnProperty( spyOnProperty(
silentRenewService, silentRenewService,
'refreshSessionWithIFrameCompleted$' 'refreshSessionWithIFrameCompleted$'
).and.returnValue(of(null)); ).mockReturnValue(of(null));
spyOn( vi.spyOn(
refreshSessionService as any, refreshSessionService as any,
'startRefreshSession' 'startRefreshSession'
).and.returnValue(throwError(() => new Error(expectedErrorMessage))); ).mockReturnValue(throwError(() => new Error(expectedErrorMessage)));
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false false
); );
const resetSilentRenewRunningSpy = spyOn( const resetSilentRenewRunningSpy = vi.spyOn(
flowsDataService, flowsDataService,
'resetSilentRenewRunning' 'resetSilentRenewRunning'
); );
refreshSessionService try {
.forceRefreshSession(allConfigs[0], allConfigs) await firstValueFrom(
.subscribe({ refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
next: () => { );
fail('It should not return any result.'); expect.fail('It should not return any result.');
}, } catch (error: any) {
error: (error) => { expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(Error); expect(error.message).toEqual(`Error: ${expectedErrorMessage}`);
expect(error.message).toEqual(`Error: ${expectedErrorMessage}`); expect(resetSilentRenewRunningSpy).not.toHaveBeenCalled();
expect(resetSilentRenewRunningSpy).not.toHaveBeenCalled(); }
}, });
});
}));
describe('NOT isCurrentFlowCodeFlowWithRefreshTokens', () => { describe('NOT isCurrentFlowCodeFlowWithRefreshTokens', () => {
it('does return null when not authenticated', waitForAsync(() => { it('does return null when not authenticated', async () => {
const allConfigs = [ const allConfigs = [
{ {
configId: 'configId1', configId: 'configId1',
@@ -463,37 +498,36 @@ describe('RefreshSessionService ', () => {
}, },
]; ];
spyOn( vi.spyOn(
flowHelper, flowHelper,
'isCurrentFlowCodeFlowWithRefreshTokens' 'isCurrentFlowCodeFlowWithRefreshTokens'
).and.returnValue(false); ).mockReturnValue(false);
spyOn( vi.spyOn(
refreshSessionService as any, refreshSessionService as any,
'startRefreshSession' 'startRefreshSession'
).and.returnValue(of(null)); ).mockReturnValue(of(null));
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue( vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false false
); );
spyOnProperty( spyOnProperty(
silentRenewService, silentRenewService,
'refreshSessionWithIFrameCompleted$' 'refreshSessionWithIFrameCompleted$'
).and.returnValue(of(null)); ).mockReturnValue(of(null));
refreshSessionService const result = await firstValueFrom(
.forceRefreshSession(allConfigs[0], allConfigs) refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
.subscribe((result) => { );
expect(result).toEqual({ expect(result).toEqual({
isAuthenticated: false, isAuthenticated: false,
errorMessage: '', errorMessage: '',
userData: null, userData: null,
idToken: '', idToken: '',
accessToken: '', accessToken: '',
configId: 'configId1', configId: 'configId1',
}); });
}); });
}));
it('return value only returns once', waitForAsync(() => { it('return value only returns once', async () => {
const allConfigs = [ const allConfigs = [
{ {
configId: 'configId1', configId: 'configId1',
@@ -501,18 +535,18 @@ describe('RefreshSessionService ', () => {
}, },
]; ];
spyOn( vi.spyOn(
flowHelper, flowHelper,
'isCurrentFlowCodeFlowWithRefreshTokens' 'isCurrentFlowCodeFlowWithRefreshTokens'
).and.returnValue(false); ).mockReturnValue(false);
spyOn( vi.spyOn(
refreshSessionService as any, refreshSessionService as any,
'startRefreshSession' 'startRefreshSession'
).and.returnValue(of(null)); ).mockReturnValue(of(null));
spyOnProperty( spyOnProperty(
silentRenewService, silentRenewService,
'refreshSessionWithIFrameCompleted$' 'refreshSessionWithIFrameCompleted$'
).and.returnValue( ).mockReturnValue(
of({ of({
authResult: { authResult: {
id_token: 'some-id_token', id_token: 'some-id_token',
@@ -520,50 +554,46 @@ describe('RefreshSessionService ', () => {
}, },
} as CallbackContext) } as CallbackContext)
); );
const spyInsideMap = spyOn( const spyInsideMap = vi
authStateService, .spyOn(authStateService, 'areAuthStorageTokensValid')
'areAuthStorageTokensValid' .mockReturnValue(true);
).and.returnValue(true);
refreshSessionService const result = await firstValueFrom(
.forceRefreshSession(allConfigs[0], allConfigs) refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
.subscribe((result) => { );
expect(result).toEqual({ expect(result).toEqual({
idToken: 'some-id_token', idToken: 'some-id_token',
accessToken: 'some-access_token', accessToken: 'some-access_token',
isAuthenticated: true, isAuthenticated: true,
userData: undefined, userData: undefined,
configId: 'configId1', configId: 'configId1',
}); });
expect(spyInsideMap).toHaveBeenCalledTimes(1); expect(spyInsideMap).toHaveBeenCalledTimes(1);
}); });
}));
}); });
}); });
describe('startRefreshSession', () => { describe('startRefreshSession', () => {
it('returns null if no auth well known endpoint defined', waitForAsync(() => { it('returns null if no auth well known endpoint defined', async () => {
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true); vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(true);
(refreshSessionService as any) const result = await firstValueFrom(
.startRefreshSession() (refreshSessionService as any).startRefreshSession()
.subscribe((result: any) => { );
expect(result).toBe(null); expect(result).toBe(null);
}); });
}));
it('returns null if silent renew Is running', waitForAsync(() => { it('returns null if silent renew Is running', async () => {
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true); vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(true);
(refreshSessionService as any) const result = await firstValueFrom(
.startRefreshSession() (refreshSessionService as any).startRefreshSession()
.subscribe((result: any) => { );
expect(result).toBe(null); expect(result).toBe(null);
}); });
}));
it('calls `setSilentRenewRunning` when should be executed', waitForAsync(() => { it('calls `setSilentRenewRunning` when should be executed', async () => {
const setSilentRenewRunningSpy = spyOn( const setSilentRenewRunningSpy = vi.spyOn(
flowsDataService, flowsDataService,
'setSilentRenewRunning' 'setSilentRenewRunning'
); );
@@ -574,30 +604,32 @@ describe('RefreshSessionService ', () => {
}, },
]; ];
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false); vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(false);
spyOn( vi.spyOn(
authWellKnownService, authWellKnownService,
'queryAndStoreAuthWellKnownEndPoints' 'queryAndStoreAuthWellKnownEndPoints'
).and.returnValue(of({})); ).mockReturnValue(of({}));
spyOn( vi.spyOn(
flowHelper, flowHelper,
'isCurrentFlowCodeFlowWithRefreshTokens' 'isCurrentFlowCodeFlowWithRefreshTokens'
).and.returnValue(true); ).mockReturnValue(true);
spyOn( vi.spyOn(
refreshSessionRefreshTokenService, refreshSessionRefreshTokenService,
'refreshSessionWithRefreshTokens' 'refreshSessionWithRefreshTokens'
).and.returnValue(of({} as CallbackContext)); ).mockReturnValue(of({} as CallbackContext));
(refreshSessionService as any) await firstValueFrom(
.startRefreshSession(allConfigs[0], allConfigs) (refreshSessionService as any).startRefreshSession(
.subscribe(() => { allConfigs[0]!,
expect(setSilentRenewRunningSpy).toHaveBeenCalled(); allConfigs
}); )
})); );
expect(setSilentRenewRunningSpy).toHaveBeenCalled();
});
it('calls refreshSessionWithRefreshTokens when current flow is codeflow with refresh tokens', waitForAsync(() => { it('calls refreshSessionWithRefreshTokens when current flow is codeflow with refresh tokens', async () => {
spyOn(flowsDataService, 'setSilentRenewRunning'); vi.spyOn(flowsDataService, 'setSilentRenewRunning');
const allConfigs = [ const allConfigs = [
{ {
configId: 'configId1', configId: 'configId1',
@@ -605,30 +637,34 @@ describe('RefreshSessionService ', () => {
}, },
]; ];
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false); vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(false);
spyOn( vi.spyOn(
authWellKnownService, authWellKnownService,
'queryAndStoreAuthWellKnownEndPoints' 'queryAndStoreAuthWellKnownEndPoints'
).and.returnValue(of({})); ).mockReturnValue(of({}));
spyOn( vi.spyOn(
flowHelper, flowHelper,
'isCurrentFlowCodeFlowWithRefreshTokens' 'isCurrentFlowCodeFlowWithRefreshTokens'
).and.returnValue(true); ).mockReturnValue(true);
const refreshSessionWithRefreshTokensSpy = spyOn( const refreshSessionWithRefreshTokensSpy = vi
refreshSessionRefreshTokenService, .spyOn(
'refreshSessionWithRefreshTokens' refreshSessionRefreshTokenService,
).and.returnValue(of({} as CallbackContext)); 'refreshSessionWithRefreshTokens'
)
.mockReturnValue(of({} as CallbackContext));
(refreshSessionService as any) await firstValueFrom(
.startRefreshSession(allConfigs[0], allConfigs) (refreshSessionService as any).startRefreshSession(
.subscribe(() => { allConfigs[0]!,
expect(refreshSessionWithRefreshTokensSpy).toHaveBeenCalled(); allConfigs
}); )
})); );
expect(refreshSessionWithRefreshTokensSpy).toHaveBeenCalled();
});
it('calls refreshSessionWithIframe when current flow is NOT codeflow with refresh tokens', waitForAsync(() => { it('calls refreshSessionWithIframe when current flow is NOT codeflow with refresh tokens', async () => {
spyOn(flowsDataService, 'setSilentRenewRunning'); vi.spyOn(flowsDataService, 'setSilentRenewRunning');
const allConfigs = [ const allConfigs = [
{ {
configId: 'configId1', configId: 'configId1',
@@ -636,32 +672,35 @@ describe('RefreshSessionService ', () => {
}, },
]; ];
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false); vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(false);
spyOn( vi.spyOn(
authWellKnownService, authWellKnownService,
'queryAndStoreAuthWellKnownEndPoints' 'queryAndStoreAuthWellKnownEndPoints'
).and.returnValue(of({})); ).mockReturnValue(of({}));
spyOn( vi.spyOn(
flowHelper, flowHelper,
'isCurrentFlowCodeFlowWithRefreshTokens' 'isCurrentFlowCodeFlowWithRefreshTokens'
).and.returnValue(false); ).mockReturnValue(false);
const refreshSessionWithRefreshTokensSpy = spyOn( const refreshSessionWithRefreshTokensSpy = vi
refreshSessionRefreshTokenService, .spyOn(
'refreshSessionWithRefreshTokens' refreshSessionRefreshTokenService,
).and.returnValue(of({} as CallbackContext)); 'refreshSessionWithRefreshTokens'
)
.mockReturnValue(of({} as CallbackContext));
const refreshSessionWithIframeSpy = spyOn( const refreshSessionWithIframeSpy = vi
refreshSessionIframeService, .spyOn(refreshSessionIframeService, 'refreshSessionWithIframe')
'refreshSessionWithIframe' .mockReturnValue(of(false));
).and.returnValue(of(false));
(refreshSessionService as any) await firstValueFrom(
.startRefreshSession(allConfigs[0], allConfigs) (refreshSessionService as any).startRefreshSession(
.subscribe(() => { allConfigs[0]!,
expect(refreshSessionWithRefreshTokensSpy).not.toHaveBeenCalled(); allConfigs
expect(refreshSessionWithIframeSpy).toHaveBeenCalled(); )
}); );
})); expect(refreshSessionWithRefreshTokensSpy).not.toHaveBeenCalled();
expect(refreshSessionWithIframeSpy).toHaveBeenCalled();
});
}); });
}); });

View File

@@ -1,10 +1,10 @@
import { inject, Injectable } from 'injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import { import {
type Observable,
TimeoutError,
forkJoin, forkJoin,
Observable,
of, of,
throwError, throwError,
TimeoutError,
timer, timer,
} from 'rxjs'; } from 'rxjs';
import { import {
@@ -18,13 +18,13 @@ import {
} from 'rxjs/operators'; } from 'rxjs/operators';
import { AuthStateService } from '../auth-state/auth-state.service'; import { AuthStateService } from '../auth-state/auth-state.service';
import { AuthWellKnownService } from '../config/auth-well-known/auth-well-known.service'; import { AuthWellKnownService } from '../config/auth-well-known/auth-well-known.service';
import { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
import { CallbackContext } from '../flows/callback-context'; import type { CallbackContext } from '../flows/callback-context';
import { FlowsDataService } from '../flows/flows-data.service'; import { FlowsDataService } from '../flows/flows-data.service';
import { RefreshSessionIframeService } from '../iframe/refresh-session-iframe.service'; import { RefreshSessionIframeService } from '../iframe/refresh-session-iframe.service';
import { SilentRenewService } from '../iframe/silent-renew.service'; import { SilentRenewService } from '../iframe/silent-renew.service';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
import { LoginResponse } from '../login/login-response'; import type { LoginResponse } from '../login/login-response';
import { StoragePersistenceService } from '../storage/storage-persistence.service'; import { StoragePersistenceService } from '../storage/storage-persistence.service';
import { UserService } from '../user-data/user.service'; import { UserService } from '../user-data/user.service';
import { FlowHelper } from '../utils/flowHelper/flow-helper.service'; import { FlowHelper } from '../utils/flowHelper/flow-helper.service';

View File

@@ -1,11 +1,12 @@
import { TestBed, waitForAsync } from '@angular/core/testing'; import { TestBed } from '@/testing';
import { of, throwError } from 'rxjs'; import { firstValueFrom, of, throwError } from 'rxjs';
import { mockProvider } from '../../../test/auto-mock'; import { vi } from 'vitest';
import { createRetriableStream } from '../../../test/create-retriable-stream.helper';
import { DataService } from '../../api/data.service'; import { DataService } from '../../api/data.service';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { createRetriableStream } from '../../testing/create-retriable-stream.helper';
import { mockProvider } from '../../testing/mock';
import { AuthWellKnownDataService } from './auth-well-known-data.service'; import { AuthWellKnownDataService } from './auth-well-known-data.service';
import { AuthWellKnownEndpoints } from './auth-well-known-endpoints'; import type { AuthWellKnownEndpoints } from './auth-well-known-endpoints';
const DUMMY_WELL_KNOWN_DOCUMENT = { const DUMMY_WELL_KNOWN_DOCUMENT = {
issuer: 'https://identity-server.test/realms/main', issuer: 'https://identity-server.test/realms/main',
@@ -38,9 +39,6 @@ describe('AuthWellKnownDataService', () => {
mockProvider(LoggerService), mockProvider(LoggerService),
], ],
}); });
});
beforeEach(() => {
service = TestBed.inject(AuthWellKnownDataService); service = TestBed.inject(AuthWellKnownDataService);
loggerService = TestBed.inject(LoggerService); loggerService = TestBed.inject(LoggerService);
dataService = TestBed.inject(DataService); dataService = TestBed.inject(DataService);
@@ -51,92 +49,94 @@ describe('AuthWellKnownDataService', () => {
}); });
describe('getWellKnownDocument', () => { describe('getWellKnownDocument', () => {
it('should add suffix if it does not exist on current URL', waitForAsync(() => { it('should add suffix if it does not exist on current URL', async () => {
const dataServiceSpy = spyOn(dataService, 'get').and.returnValue( const dataServiceSpy = vi
of(null) .spyOn(dataService, 'get')
); .mockReturnValue(of(null));
const urlWithoutSuffix = 'myUrl'; const urlWithoutSuffix = 'myUrl';
const urlWithSuffix = `${urlWithoutSuffix}/.well-known/openid-configuration`; const urlWithSuffix = `${urlWithoutSuffix}/.well-known/openid-configuration`;
(service as any) await firstValueFrom(
.getWellKnownDocument(urlWithoutSuffix, { configId: 'configId1' }) (service as any).getWellKnownDocument(urlWithoutSuffix, {
.subscribe(() => { configId: 'configId1',
expect(dataServiceSpy).toHaveBeenCalledOnceWith(urlWithSuffix, { })
configId: 'configId1',
});
});
}));
it('should not add suffix if it does exist on current url', waitForAsync(() => {
const dataServiceSpy = spyOn(dataService, 'get').and.returnValue(
of(null)
); );
const urlWithSuffix = `myUrl/.well-known/openid-configuration`; expect(dataServiceSpy).toHaveBeenCalledExactlyOnceWith(urlWithSuffix, {
configId: 'configId1',
});
});
(service as any) it('should not add suffix if it does exist on current url', async () => {
.getWellKnownDocument(urlWithSuffix, { configId: 'configId1' }) const dataServiceSpy = vi
.subscribe(() => { .spyOn(dataService, 'get')
expect(dataServiceSpy).toHaveBeenCalledOnceWith(urlWithSuffix, { .mockReturnValue(of(null));
configId: 'configId1', const urlWithSuffix = 'myUrl/.well-known/openid-configuration';
});
});
}));
it('should not add suffix if it does exist in the middle of current url', waitForAsync(() => { await firstValueFrom(
const dataServiceSpy = spyOn(dataService, 'get').and.returnValue( (service as any).getWellKnownDocument(urlWithSuffix, {
of(null) configId: 'configId1',
})
); );
const urlWithSuffix = `myUrl/.well-known/openid-configuration/and/some/more/stuff`; expect(dataServiceSpy).toHaveBeenCalledExactlyOnceWith(urlWithSuffix, {
configId: 'configId1',
});
});
(service as any) it('should not add suffix if it does exist in the middle of current url', async () => {
.getWellKnownDocument(urlWithSuffix, { configId: 'configId1' }) const dataServiceSpy = vi
.subscribe(() => { .spyOn(dataService, 'get')
expect(dataServiceSpy).toHaveBeenCalledOnceWith(urlWithSuffix, { .mockReturnValue(of(null));
configId: 'configId1', const urlWithSuffix =
}); 'myUrl/.well-known/openid-configuration/and/some/more/stuff';
});
}));
it('should use the custom suffix provided in the config', waitForAsync(() => { await firstValueFrom(
const dataServiceSpy = spyOn(dataService, 'get').and.returnValue( (service as any).getWellKnownDocument(urlWithSuffix, {
of(null) configId: 'configId1',
})
); );
const urlWithoutSuffix = `myUrl`; expect(dataServiceSpy).toHaveBeenCalledExactlyOnceWith(urlWithSuffix, {
configId: 'configId1',
});
});
it('should use the custom suffix provided in the config', async () => {
const dataServiceSpy = vi
.spyOn(dataService, 'get')
.mockReturnValue(of(null));
const urlWithoutSuffix = 'myUrl';
const urlWithSuffix = `${urlWithoutSuffix}/.well-known/test-openid-configuration`; const urlWithSuffix = `${urlWithoutSuffix}/.well-known/test-openid-configuration`;
(service as any) await firstValueFrom(
.getWellKnownDocument(urlWithoutSuffix, { (service as any).getWellKnownDocument(urlWithoutSuffix, {
configId: 'configId1', configId: 'configId1',
authWellknownUrlSuffix: '/.well-known/test-openid-configuration', authWellknownUrlSuffix: '/.well-known/test-openid-configuration',
}) })
.subscribe(() => { );
expect(dataServiceSpy).toHaveBeenCalledOnceWith(urlWithSuffix, { expect(dataServiceSpy).toHaveBeenCalledExactlyOnceWith(urlWithSuffix, {
configId: 'configId1', configId: 'configId1',
authWellknownUrlSuffix: '/.well-known/test-openid-configuration', authWellknownUrlSuffix: '/.well-known/test-openid-configuration',
}); });
}); });
}));
it('should retry once', waitForAsync(() => { it('should retry once', async () => {
spyOn(dataService, 'get').and.returnValue( vi.spyOn(dataService, 'get').mockReturnValue(
createRetriableStream( createRetriableStream(
throwError(() => new Error('one')), throwError(() => new Error('one')),
of(DUMMY_WELL_KNOWN_DOCUMENT) of(DUMMY_WELL_KNOWN_DOCUMENT)
) )
); );
(service as any) const res: unknown = await firstValueFrom(
.getWellKnownDocument('anyurl', { configId: 'configId1' }) (service as any).getWellKnownDocument('anyurl', {
.subscribe({ configId: 'configId1',
next: (res: unknown) => { })
expect(res).toBeTruthy(); );
expect(res).toEqual(DUMMY_WELL_KNOWN_DOCUMENT); expect(res).toBeTruthy();
}, expect(res).toEqual(DUMMY_WELL_KNOWN_DOCUMENT);
}); });
}));
it('should retry twice', waitForAsync(() => { it('should retry twice', async () => {
spyOn(dataService, 'get').and.returnValue( vi.spyOn(dataService, 'get').mockReturnValue(
createRetriableStream( createRetriableStream(
throwError(() => new Error('one')), throwError(() => new Error('one')),
throwError(() => new Error('two')), throwError(() => new Error('two')),
@@ -144,18 +144,17 @@ describe('AuthWellKnownDataService', () => {
) )
); );
(service as any) const res: any = await firstValueFrom(
.getWellKnownDocument('anyurl', { configId: 'configId1' }) (service as any).getWellKnownDocument('anyurl', {
.subscribe({ configId: 'configId1',
next: (res: any) => { })
expect(res).toBeTruthy(); );
expect(res).toEqual(DUMMY_WELL_KNOWN_DOCUMENT); expect(res).toBeTruthy();
}, expect(res).toEqual(DUMMY_WELL_KNOWN_DOCUMENT);
}); });
}));
it('should fail after three tries', waitForAsync(() => { it('should fail after three tries', async () => {
spyOn(dataService, 'get').and.returnValue( vi.spyOn(dataService, 'get').mockReturnValue(
createRetriableStream( createRetriableStream(
throwError(() => new Error('one')), throwError(() => new Error('one')),
throwError(() => new Error('two')), throwError(() => new Error('two')),
@@ -164,55 +163,57 @@ describe('AuthWellKnownDataService', () => {
) )
); );
(service as any).getWellKnownDocument('anyurl', 'configId').subscribe({ try {
error: (err: unknown) => { await firstValueFrom(
expect(err).toBeTruthy(); (service as any).getWellKnownDocument('anyurl', 'configId')
}, );
}); } catch (err: unknown) {
})); expect(err).toBeTruthy();
}
});
}); });
describe('getWellKnownEndPointsForConfig', () => { describe('getWellKnownEndPointsForConfig', () => {
it('calling internal getWellKnownDocument and maps', waitForAsync(() => { it('calling internal getWellKnownDocument and maps', async () => {
spyOn(dataService, 'get').and.returnValue(of({ jwks_uri: 'jwks_uri' })); vi.spyOn(dataService, 'get').mockReturnValue(
of({ jwks_uri: 'jwks_uri' })
);
const spy = spyOn( const spy = vi.spyOn(service as any, 'getWellKnownDocument');
service as any,
'getWellKnownDocument'
).and.callThrough();
service const result = await firstValueFrom(
.getWellKnownEndPointsForConfig({ service.getWellKnownEndPointsForConfig({
configId: 'configId1', configId: 'configId1',
authWellknownEndpointUrl: 'any-url', authWellknownEndpointUrl: 'any-url',
}) })
.subscribe((result) => { );
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
expect((result as any).jwks_uri).toBeUndefined(); expect((result as any).jwks_uri).toBeUndefined();
expect(result.jwksUri).toBe('jwks_uri'); expect(result.jwksUri).toBe('jwks_uri');
}); });
}));
it('throws error and logs if no authwellknownUrl is given', waitForAsync(() => { it('throws error and logs if no authwellknownUrl is given', async () => {
const loggerSpy = spyOn(loggerService, 'logError'); const loggerSpy = vi.spyOn(loggerService, 'logError');
const config = { const config = {
configId: 'configId1', configId: 'configId1',
authWellknownEndpointUrl: undefined, authWellknownEndpointUrl: undefined,
}; };
service.getWellKnownEndPointsForConfig(config).subscribe({ try {
error: (error) => { await firstValueFrom(service.getWellKnownEndPointsForConfig(config));
expect(loggerSpy).toHaveBeenCalledOnceWith( } catch (error: any) {
config, expect(loggerSpy).toHaveBeenCalledExactlyOnceWith(
'no authWellknownEndpoint given!' config,
); 'no authWellknownEndpoint given!'
expect(error.message).toEqual('no authWellknownEndpoint given!'); );
}, expect(error.message).toEqual('no authWellknownEndpoint given!');
}); }
})); });
it('should merge the mapped endpoints with the provided endpoints', waitForAsync(() => { it('should merge the mapped endpoints with the provided endpoints', async () => {
spyOn(dataService, 'get').and.returnValue(of(DUMMY_WELL_KNOWN_DOCUMENT)); vi.spyOn(dataService, 'get').mockReturnValue(
of(DUMMY_WELL_KNOWN_DOCUMENT)
);
const expected: AuthWellKnownEndpoints = { const expected: AuthWellKnownEndpoints = {
endSessionEndpoint: 'config-endSessionEndpoint', endSessionEndpoint: 'config-endSessionEndpoint',
@@ -220,8 +221,8 @@ describe('AuthWellKnownDataService', () => {
jwksUri: DUMMY_WELL_KNOWN_DOCUMENT.jwks_uri, jwksUri: DUMMY_WELL_KNOWN_DOCUMENT.jwks_uri,
}; };
service const result = await firstValueFrom(
.getWellKnownEndPointsForConfig({ service.getWellKnownEndPointsForConfig({
configId: 'configId1', configId: 'configId1',
authWellknownEndpointUrl: 'any-url', authWellknownEndpointUrl: 'any-url',
authWellknownEndpoints: { authWellknownEndpoints: {
@@ -229,9 +230,8 @@ describe('AuthWellKnownDataService', () => {
revocationEndpoint: 'config-revocationEndpoint', revocationEndpoint: 'config-revocationEndpoint',
}, },
}) })
.subscribe((result) => { );
expect(result).toEqual(jasmine.objectContaining(expected)); expect(result).toEqual(expect.objectContaining(expected));
}); });
}));
}); });
}); });

View File

@@ -1,12 +1,12 @@
import { inject, Injectable } from 'injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import { Observable, throwError } from 'rxjs'; import { type Observable, throwError } from 'rxjs';
import { map, retry } from 'rxjs/operators'; import { map, retry } from 'rxjs/operators';
import { DataService } from '../../api/data.service'; import { DataService } from '../../api/data.service';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { OpenIdConfiguration } from '../openid-configuration'; import type { OpenIdConfiguration } from '../openid-configuration';
import { AuthWellKnownEndpoints } from './auth-well-known-endpoints'; import type { AuthWellKnownEndpoints } from './auth-well-known-endpoints';
const WELL_KNOWN_SUFFIX = `/.well-known/openid-configuration`; const WELL_KNOWN_SUFFIX = '/.well-known/openid-configuration';
@Injectable() @Injectable()
export class AuthWellKnownDataService { export class AuthWellKnownDataService {
@@ -42,7 +42,7 @@ export class AuthWellKnownDataService {
introspectionEndpoint: wellKnownEndpoints.introspection_endpoint, introspectionEndpoint: wellKnownEndpoints.introspection_endpoint,
parEndpoint: parEndpoint:
wellKnownEndpoints.pushed_authorization_request_endpoint, wellKnownEndpoints.pushed_authorization_request_endpoint,
} as AuthWellKnownEndpoints) }) as AuthWellKnownEndpoints
), ),
map((mappedWellKnownEndpoints) => ({ map((mappedWellKnownEndpoints) => ({
...mappedWellKnownEndpoints, ...mappedWellKnownEndpoints,

View File

@@ -1,9 +1,10 @@
import { TestBed, waitForAsync } from '@angular/core/testing'; import { TestBed, mockImplementationWhenArgsEqual } from '@/testing';
import { of, throwError } from 'rxjs'; import { firstValueFrom, of, throwError } from 'rxjs';
import { mockProvider } from '../../../test/auto-mock'; import { vi } from 'vitest';
import { EventTypes } from '../../public-events/event-types'; import { EventTypes } from '../../public-events/event-types';
import { PublicEventsService } from '../../public-events/public-events.service'; import { PublicEventsService } from '../../public-events/public-events.service';
import { StoragePersistenceService } from '../../storage/storage-persistence.service'; import { StoragePersistenceService } from '../../storage/storage-persistence.service';
import { mockProvider } from '../../testing/mock';
import { AuthWellKnownDataService } from './auth-well-known-data.service'; import { AuthWellKnownDataService } from './auth-well-known-data.service';
import { AuthWellKnownService } from './auth-well-known.service'; import { AuthWellKnownService } from './auth-well-known.service';
@@ -22,9 +23,6 @@ describe('AuthWellKnownService', () => {
mockProvider(StoragePersistenceService), mockProvider(StoragePersistenceService),
], ],
}); });
});
beforeEach(() => {
service = TestBed.inject(AuthWellKnownService); service = TestBed.inject(AuthWellKnownService);
dataService = TestBed.inject(AuthWellKnownDataService); dataService = TestBed.inject(AuthWellKnownDataService);
storagePersistenceService = TestBed.inject(StoragePersistenceService); storagePersistenceService = TestBed.inject(StoragePersistenceService);
@@ -36,75 +34,75 @@ describe('AuthWellKnownService', () => {
}); });
describe('getAuthWellKnownEndPoints', () => { describe('getAuthWellKnownEndPoints', () => {
it('getAuthWellKnownEndPoints throws an error if not config provided', waitForAsync(() => { it('getAuthWellKnownEndPoints throws an error if not config provided', async () => {
service.queryAndStoreAuthWellKnownEndPoints(null).subscribe({ try {
error: (error) => { await firstValueFrom(service.queryAndStoreAuthWellKnownEndPoints(null));
expect(error).toEqual( } catch (error) {
new Error( expect(error).toEqual(
'Please provide a configuration before setting up the module' new Error(
) 'Please provide a configuration before setting up the module'
); )
}, );
}); }
})); });
it('getAuthWellKnownEndPoints calls always dataservice', waitForAsync(() => { it('getAuthWellKnownEndPoints calls always dataservice', async () => {
const dataServiceSpy = spyOn( const dataServiceSpy = vi
dataService, .spyOn(dataService, 'getWellKnownEndPointsForConfig')
'getWellKnownEndPointsForConfig' .mockReturnValue(of({ issuer: 'anything' }));
).and.returnValue(of({ issuer: 'anything' }));
spyOn(storagePersistenceService, 'read') mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', { configId: 'configId1' }) vi.spyOn(storagePersistenceService, 'read'),
.and.returnValue({ issuer: 'anything' }); ['authWellKnownEndPoints', { configId: 'configId1' }],
() => ({ issuer: 'anything' })
);
service const result = await firstValueFrom(
.queryAndStoreAuthWellKnownEndPoints({ configId: 'configId1' }) service.queryAndStoreAuthWellKnownEndPoints({ configId: 'configId1' })
.subscribe((result) => { );
expect(storagePersistenceService.read).not.toHaveBeenCalled(); expect(storagePersistenceService.read).not.toHaveBeenCalled();
expect(dataServiceSpy).toHaveBeenCalled(); expect(dataServiceSpy).toHaveBeenCalled();
expect(result).toEqual({ issuer: 'anything' }); expect(result).toEqual({ issuer: 'anything' });
}); });
}));
it('getAuthWellKnownEndPoints stored the result if http call is made', waitForAsync(() => { it('getAuthWellKnownEndPoints stored the result if http call is made', async () => {
const dataServiceSpy = spyOn( const dataServiceSpy = vi
dataService, .spyOn(dataService, 'getWellKnownEndPointsForConfig')
'getWellKnownEndPointsForConfig' .mockReturnValue(of({ issuer: 'anything' }));
).and.returnValue(of({ issuer: 'anything' }));
spyOn(storagePersistenceService, 'read') mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', { configId: 'configId1' }) vi.spyOn(storagePersistenceService, 'read'),
.and.returnValue(null); ['authWellKnownEndPoints', { configId: 'configId1' }],
const storeSpy = spyOn(service, 'storeWellKnownEndpoints'); () => null
);
const storeSpy = vi.spyOn(service, 'storeWellKnownEndpoints');
service const result = await firstValueFrom(
.queryAndStoreAuthWellKnownEndPoints({ configId: 'configId1' }) service.queryAndStoreAuthWellKnownEndPoints({ configId: 'configId1' })
.subscribe((result) => { );
expect(dataServiceSpy).toHaveBeenCalled(); expect(dataServiceSpy).toHaveBeenCalled();
expect(storeSpy).toHaveBeenCalled(); expect(storeSpy).toHaveBeenCalled();
expect(result).toEqual({ issuer: 'anything' }); expect(result).toEqual({ issuer: 'anything' });
}); });
}));
it('throws `ConfigLoadingFailed` event when error happens from http', waitForAsync(() => { it('throws `ConfigLoadingFailed` event when error happens from http', async () => {
spyOn(dataService, 'getWellKnownEndPointsForConfig').and.returnValue( vi.spyOn(dataService, 'getWellKnownEndPointsForConfig').mockReturnValue(
throwError(() => new Error('error')) throwError(() => new Error('error'))
); );
const publicEventsServiceSpy = spyOn(publicEventsService, 'fireEvent'); const publicEventsServiceSpy = vi.spyOn(publicEventsService, 'fireEvent');
service try {
.queryAndStoreAuthWellKnownEndPoints({ configId: 'configId1' }) await firstValueFrom(
.subscribe({ service.queryAndStoreAuthWellKnownEndPoints({ configId: 'configId1' })
error: (err) => { );
expect(err).toBeTruthy(); } catch (err: any) {
expect(publicEventsServiceSpy).toHaveBeenCalledTimes(1); expect(err).toBeTruthy();
expect(publicEventsServiceSpy).toHaveBeenCalledOnceWith( expect(publicEventsServiceSpy).toHaveBeenCalledTimes(1);
EventTypes.ConfigLoadingFailed, expect(publicEventsServiceSpy).toHaveBeenCalledExactlyOnceWith(
null EventTypes.ConfigLoadingFailed,
); null
}, );
}); }
})); });
}); });
}); });

View File

@@ -1,12 +1,12 @@
import { inject, Injectable } from 'injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import { Observable, throwError } from 'rxjs'; import { type Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators'; import { catchError, tap } from 'rxjs/operators';
import { EventTypes } from '../../public-events/event-types'; import { EventTypes } from '../../public-events/event-types';
import { PublicEventsService } from '../../public-events/public-events.service'; import { PublicEventsService } from '../../public-events/public-events.service';
import { StoragePersistenceService } from '../../storage/storage-persistence.service'; import { StoragePersistenceService } from '../../storage/storage-persistence.service';
import { OpenIdConfiguration } from '../openid-configuration'; import type { OpenIdConfiguration } from '../openid-configuration';
import { AuthWellKnownDataService } from './auth-well-known-data.service'; import { AuthWellKnownDataService } from './auth-well-known-data.service';
import { AuthWellKnownEndpoints } from './auth-well-known-endpoints'; import type { AuthWellKnownEndpoints } from './auth-well-known-endpoints';
@Injectable() @Injectable()
export class AuthWellKnownService { export class AuthWellKnownService {

View File

@@ -1,15 +1,16 @@
import { TestBed, waitForAsync } from '@angular/core/testing'; import { TestBed } from '@/testing';
import { of } from 'rxjs'; import { firstValueFrom, of } from 'rxjs';
import { mockAbstractProvider, mockProvider } from '../../test/auto-mock'; import { vi } from 'vitest';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
import { EventTypes } from '../public-events/event-types'; import { EventTypes } from '../public-events/event-types';
import { PublicEventsService } from '../public-events/public-events.service'; import { PublicEventsService } from '../public-events/public-events.service';
import { StoragePersistenceService } from '../storage/storage-persistence.service'; import { StoragePersistenceService } from '../storage/storage-persistence.service';
import { mockAbstractProvider, mockProvider } from '../testing/mock';
import { PlatformProvider } from '../utils/platform-provider/platform.provider'; import { PlatformProvider } from '../utils/platform-provider/platform.provider';
import { AuthWellKnownService } from './auth-well-known/auth-well-known.service'; import { AuthWellKnownService } from './auth-well-known/auth-well-known.service';
import { ConfigurationService } from './config.service'; import { ConfigurationService } from './config.service';
import { StsConfigLoader, StsConfigStaticLoader } from './loader/config-loader'; import { StsConfigLoader, StsConfigStaticLoader } from './loader/config-loader';
import { OpenIdConfiguration } from './openid-configuration'; import type { OpenIdConfiguration } from './openid-configuration';
import { ConfigValidationService } from './validation/config-validation.service'; import { ConfigValidationService } from './validation/config-validation.service';
describe('Configuration Service', () => { describe('Configuration Service', () => {
@@ -34,9 +35,6 @@ describe('Configuration Service', () => {
mockAbstractProvider(StsConfigLoader, StsConfigStaticLoader), mockAbstractProvider(StsConfigLoader, StsConfigStaticLoader),
], ],
}); });
});
beforeEach(() => {
configService = TestBed.inject(ConfigurationService); configService = TestBed.inject(ConfigurationService);
publicEventsService = TestBed.inject(PublicEventsService); publicEventsService = TestBed.inject(PublicEventsService);
authWellKnownService = TestBed.inject(AuthWellKnownService); authWellKnownService = TestBed.inject(AuthWellKnownService);
@@ -88,98 +86,110 @@ describe('Configuration Service', () => {
}); });
describe('getOpenIDConfiguration', () => { describe('getOpenIDConfiguration', () => {
it(`if config is already saved 'loadConfigs' is not called`, waitForAsync(() => { it(`if config is already saved 'loadConfigs' is not called`, async () => {
(configService as any).configsInternal = { (configService as any).configsInternal = {
configId1: { configId: 'configId1' }, configId1: { configId: 'configId1' },
configId2: { configId: 'configId2' }, configId2: { configId: 'configId2' },
}; };
const spy = spyOn(configService as any, 'loadConfigs'); const spy = vi.spyOn(configService as any, 'loadConfigs');
configService.getOpenIDConfiguration('configId1').subscribe((config) => { const config = await firstValueFrom(
expect(config).toBeTruthy(); configService.getOpenIDConfiguration('configId1')
expect(spy).not.toHaveBeenCalled(); );
}); expect(config).toBeTruthy();
})); expect(spy).not.toHaveBeenCalled();
});
it(`if config is NOT already saved 'loadConfigs' is called`, waitForAsync(() => { it(`if config is NOT already saved 'loadConfigs' is called`, async () => {
const configs = [{ configId: 'configId1' }, { configId: 'configId2' }]; const configs = [{ configId: 'configId1' }, { configId: 'configId2' }];
const spy = spyOn(configService as any, 'loadConfigs').and.returnValue( const spy = vi
.spyOn(configService as any, 'loadConfigs')
.mockReturnValue(of(configs));
vi.spyOn(configValidationService, 'validateConfig').mockReturnValue(true);
const config = await firstValueFrom(
configService.getOpenIDConfiguration('configId1')
);
expect(config).toBeTruthy();
expect(spy).toHaveBeenCalled();
});
it('returns null if config is not valid', async () => {
const configs = [{ configId: 'configId1' }];
vi.spyOn(configService as any, 'loadConfigs').mockReturnValue(
of(configs) of(configs)
); );
vi.spyOn(configValidationService, 'validateConfig').mockReturnValue(
false
);
const consoleSpy = vi.spyOn(console, 'warn');
spyOn(configValidationService, 'validateConfig').and.returnValue(true); const config = await firstValueFrom(
configService.getOpenIDConfiguration('configId1')
);
expect(config).toBeNull();
expect(consoleSpy).toHaveBeenCalledExactlyOnceWith(
`[oidc-client-rx] No configuration found for config id 'configId1'.`
);
});
configService.getOpenIDConfiguration('configId1').subscribe((config) => { it('returns null if configs are stored but not existing ID is passed', async () => {
expect(config).toBeTruthy();
expect(spy).toHaveBeenCalled();
});
}));
it(`returns null if config is not valid`, waitForAsync(() => {
const configs = [{ configId: 'configId1' }];
spyOn(configService as any, 'loadConfigs').and.returnValue(of(configs));
spyOn(configValidationService, 'validateConfig').and.returnValue(false);
const consoleSpy = spyOn(console, 'warn');
configService.getOpenIDConfiguration('configId1').subscribe((config) => {
expect(config).toBeNull();
expect(consoleSpy).toHaveBeenCalledOnceWith(`[oidc-client-rx] No configuration found for config id 'configId1'.`)
});
}));
it(`returns null if configs are stored but not existing ID is passed`, waitForAsync(() => {
(configService as any).configsInternal = { (configService as any).configsInternal = {
configId1: { configId: 'configId1' }, configId1: { configId: 'configId1' },
configId2: { configId: 'configId2' }, configId2: { configId: 'configId2' },
}; };
configService const config = await firstValueFrom(
.getOpenIDConfiguration('notExisting') configService.getOpenIDConfiguration('notExisting')
.subscribe((config) => { );
expect(config).toBeNull(); expect(config).toBeNull();
}); });
}));
it(`sets authWellKnownEndPoints on config if authWellKnownEndPoints is stored`, waitForAsync(() => { it('sets authWellKnownEndPoints on config if authWellKnownEndPoints is stored', async () => {
const configs = [{ configId: 'configId1' }]; const configs = [{ configId: 'configId1' }];
spyOn(configService as any, 'loadConfigs').and.returnValue(of(configs)); vi.spyOn(configService as any, 'loadConfigs').mockReturnValue(
spyOn(configValidationService, 'validateConfig').and.returnValue(true); of(configs)
const consoleSpy = spyOn(console, 'warn'); );
vi.spyOn(configValidationService, 'validateConfig').mockReturnValue(true);
const consoleSpy = vi.spyOn(console, 'warn');
spyOn(storagePersistenceService, 'read').and.returnValue({ vi.spyOn(storagePersistenceService, 'read').mockReturnValue({
issuer: 'auth-well-known', issuer: 'auth-well-known',
}); });
configService.getOpenIDConfiguration('configId1').subscribe((config) => { const config = await firstValueFrom(
expect(config?.authWellknownEndpoints).toEqual({ configService.getOpenIDConfiguration('configId1')
issuer: 'auth-well-known', );
}); expect(config?.authWellknownEndpoints).toEqual({
expect(consoleSpy).not.toHaveBeenCalled() issuer: 'auth-well-known',
}); });
})); expect(consoleSpy).not.toHaveBeenCalled();
});
it(`fires ConfigLoaded if authWellKnownEndPoints is stored`, waitForAsync(() => { it('fires ConfigLoaded if authWellKnownEndPoints is stored', async () => {
const configs = [{ configId: 'configId1' }]; const configs = [{ configId: 'configId1' }];
spyOn(configService as any, 'loadConfigs').and.returnValue(of(configs)); vi.spyOn(configService as any, 'loadConfigs').mockReturnValue(
spyOn(configValidationService, 'validateConfig').and.returnValue(true); of(configs)
spyOn(storagePersistenceService, 'read').and.returnValue({ );
vi.spyOn(configValidationService, 'validateConfig').mockReturnValue(true);
vi.spyOn(storagePersistenceService, 'read').mockReturnValue({
issuer: 'auth-well-known', issuer: 'auth-well-known',
}); });
const spy = spyOn(publicEventsService, 'fireEvent'); const spy = vi.spyOn(publicEventsService, 'fireEvent');
configService.getOpenIDConfiguration('configId1').subscribe(() => { await firstValueFrom(configService.getOpenIDConfiguration('configId1'));
expect(spy).toHaveBeenCalledOnceWith( expect(spy).toHaveBeenCalledExactlyOnceWith(
EventTypes.ConfigLoaded, EventTypes.ConfigLoaded,
jasmine.anything() expect.anything()
); );
}); });
}));
it(`stores, uses and fires event when authwellknownendpoints are passed`, waitForAsync(() => { it('stores, uses and fires event when authwellknownendpoints are passed', async () => {
const configs = [ const configs = [
{ {
configId: 'configId1', configId: 'configId1',
@@ -187,92 +197,96 @@ describe('Configuration Service', () => {
}, },
]; ];
spyOn(configService as any, 'loadConfigs').and.returnValue(of(configs)); vi.spyOn(configService as any, 'loadConfigs').mockReturnValue(
spyOn(configValidationService, 'validateConfig').and.returnValue(true); of(configs)
spyOn(storagePersistenceService, 'read').and.returnValue(null); );
vi.spyOn(configValidationService, 'validateConfig').mockReturnValue(true);
vi.spyOn(storagePersistenceService, 'read').mockReturnValue(null);
const fireEventSpy = spyOn(publicEventsService, 'fireEvent'); const fireEventSpy = vi.spyOn(publicEventsService, 'fireEvent');
const storeWellKnownEndpointsSpy = spyOn( const storeWellKnownEndpointsSpy = vi.spyOn(
authWellKnownService, authWellKnownService,
'storeWellKnownEndpoints' 'storeWellKnownEndpoints'
); );
configService.getOpenIDConfiguration('configId1').subscribe((config) => { const config = await firstValueFrom(
expect(config).toBeTruthy(); configService.getOpenIDConfiguration('configId1')
expect(fireEventSpy).toHaveBeenCalledOnceWith( );
EventTypes.ConfigLoaded, expect(config).toBeTruthy();
jasmine.anything() expect(fireEventSpy).toHaveBeenCalledExactlyOnceWith(
); EventTypes.ConfigLoaded,
expect(storeWellKnownEndpointsSpy).toHaveBeenCalledOnceWith( expect.anything()
config as OpenIdConfiguration, );
{ expect(storeWellKnownEndpointsSpy).toHaveBeenCalledExactlyOnceWith(
issuer: 'auth-well-known', config as OpenIdConfiguration,
} {
); issuer: 'auth-well-known',
}); }
})); );
});
}); });
describe('getOpenIDConfigurations', () => { describe('getOpenIDConfigurations', () => {
it(`returns correct result`, waitForAsync(() => { it('returns correct result', async () => {
spyOn(stsConfigLoader, 'loadConfigs').and.returnValue( vi.spyOn(stsConfigLoader, 'loadConfigs').mockReturnValue(
of([ of([
{ configId: 'configId1' } as OpenIdConfiguration, { configId: 'configId1' } as OpenIdConfiguration,
{ configId: 'configId2' } as OpenIdConfiguration, { configId: 'configId2' } as OpenIdConfiguration,
]) ])
); );
spyOn(configValidationService, 'validateConfig').and.returnValue(true); vi.spyOn(configValidationService, 'validateConfig').mockReturnValue(true);
configService.getOpenIDConfigurations('configId1').subscribe((result) => { const result = await firstValueFrom(
expect(result.allConfigs.length).toEqual(2); configService.getOpenIDConfigurations('configId1')
expect(result.currentConfig).toBeTruthy(); );
}); expect(result.allConfigs.length).toEqual(2);
})); expect(result.currentConfig).toBeTruthy();
});
it(`created configId when configId is not set`, waitForAsync(() => { it('created configId when configId is not set', async () => {
spyOn(stsConfigLoader, 'loadConfigs').and.returnValue( vi.spyOn(stsConfigLoader, 'loadConfigs').mockReturnValue(
of([ of([
{ clientId: 'clientId1' } as OpenIdConfiguration, { clientId: 'clientId1' } as OpenIdConfiguration,
{ clientId: 'clientId2' } as OpenIdConfiguration, { clientId: 'clientId2' } as OpenIdConfiguration,
]) ])
); );
spyOn(configValidationService, 'validateConfig').and.returnValue(true); vi.spyOn(configValidationService, 'validateConfig').mockReturnValue(true);
configService.getOpenIDConfigurations().subscribe((result) => { const result = await firstValueFrom(
expect(result.allConfigs.length).toEqual(2); configService.getOpenIDConfigurations()
const allConfigIds = result.allConfigs.map((x) => x.configId); );
expect(result.allConfigs.length).toEqual(2);
const allConfigIds = result.allConfigs.map((x) => x.configId);
expect(allConfigIds).toEqual(['0-clientId1', '1-clientId2']);
expect(result.currentConfig).toBeTruthy();
expect(result.currentConfig?.configId).toBeTruthy();
});
expect(allConfigIds).toEqual(['0-clientId1', '1-clientId2']); it('returns empty array if config is not valid', async () => {
vi.spyOn(stsConfigLoader, 'loadConfigs').mockReturnValue(
expect(result.currentConfig).toBeTruthy();
expect(result.currentConfig?.configId).toBeTruthy();
});
}));
it(`returns empty array if config is not valid`, waitForAsync(() => {
spyOn(stsConfigLoader, 'loadConfigs').and.returnValue(
of([ of([
{ configId: 'configId1' } as OpenIdConfiguration, { configId: 'configId1' } as OpenIdConfiguration,
{ configId: 'configId2' } as OpenIdConfiguration, { configId: 'configId2' } as OpenIdConfiguration,
]) ])
); );
spyOn(configValidationService, 'validateConfigs').and.returnValue(false); vi.spyOn(configValidationService, 'validateConfigs').mockReturnValue(
false
);
configService const { allConfigs, currentConfig } = await firstValueFrom(
.getOpenIDConfigurations() configService.getOpenIDConfigurations()
.subscribe(({ allConfigs, currentConfig }) => { );
expect(allConfigs).toEqual([]); expect(allConfigs).toEqual([]);
expect(currentConfig).toBeNull(); expect(currentConfig).toBeNull();
}); });
}));
}); });
describe('setSpecialCases', () => { describe('setSpecialCases', () => {
it(`should set special cases when current platform is browser`, () => { it('should set special cases when current platform is browser', () => {
spyOn(platformProvider, 'isBrowser').and.returnValue(false); vi.spyOn(platformProvider, 'isBrowser').mockReturnValue(false);
const config = { configId: 'configId1' } as OpenIdConfiguration; const config = { configId: 'configId1' } as OpenIdConfiguration;

View File

@@ -1,6 +1,7 @@
import {inject, Injectable, isDevMode} from 'injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import { forkJoin, Observable, of } from 'rxjs'; import { type Observable, forkJoin, of } from 'rxjs';
import { concatMap, map } from 'rxjs/operators'; import { concatMap, map } from 'rxjs/operators';
import { injectAbstractType } from '../injection/inject';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
import { EventTypes } from '../public-events/event-types'; import { EventTypes } from '../public-events/event-types';
import { PublicEventsService } from '../public-events/public-events.service'; import { PublicEventsService } from '../public-events/public-events.service';
@@ -9,7 +10,7 @@ import { PlatformProvider } from '../utils/platform-provider/platform.provider';
import { AuthWellKnownService } from './auth-well-known/auth-well-known.service'; import { AuthWellKnownService } from './auth-well-known/auth-well-known.service';
import { DEFAULT_CONFIG } from './default-config'; import { DEFAULT_CONFIG } from './default-config';
import { StsConfigLoader } from './loader/config-loader'; import { StsConfigLoader } from './loader/config-loader';
import { OpenIdConfiguration } from './openid-configuration'; import type { OpenIdConfiguration } from './openid-configuration';
import { ConfigValidationService } from './validation/config-validation.service'; import { ConfigValidationService } from './validation/config-validation.service';
@Injectable() @Injectable()
@@ -26,7 +27,7 @@ export class ConfigurationService {
private readonly authWellKnownService = inject(AuthWellKnownService); private readonly authWellKnownService = inject(AuthWellKnownService);
private readonly loader = inject(StsConfigLoader); private readonly loader = injectAbstractType(StsConfigLoader);
private readonly configValidationService = inject(ConfigValidationService); private readonly configValidationService = inject(ConfigValidationService);
@@ -84,11 +85,14 @@ export class ConfigurationService {
} }
private getConfig(configId?: string): OpenIdConfiguration | null { private getConfig(configId?: string): OpenIdConfiguration | null {
if (Boolean(configId)) { if (configId) {
const config = this.configsInternal[configId!]; const config = this.configsInternal[configId!];
if(!config && isDevMode()) { if (!config) {
console.warn(`[oidc-client-rx] No configuration found for config id '${configId}'.`); // biome-ignore lint/suspicious/noConsole: <explanation>
console.warn(
`[oidc-client-rx] No configuration found for config id '${configId}'.`
);
} }
return config || null; return config || null;
@@ -165,7 +169,7 @@ export class ConfigurationService {
configuration configuration
); );
if (!!alreadyExistingAuthWellKnownEndpoints) { if (alreadyExistingAuthWellKnownEndpoints) {
configuration.authWellknownEndpoints = configuration.authWellknownEndpoints =
alreadyExistingAuthWellKnownEndpoints; alreadyExistingAuthWellKnownEndpoints;
@@ -174,7 +178,7 @@ export class ConfigurationService {
const passedAuthWellKnownEndpoints = configuration.authWellknownEndpoints; const passedAuthWellKnownEndpoints = configuration.authWellknownEndpoints;
if (!!passedAuthWellKnownEndpoints) { if (passedAuthWellKnownEndpoints) {
this.authWellKnownService.storeWellKnownEndpoints( this.authWellKnownService.storeWellKnownEndpoints(
configuration, configuration,
passedAuthWellKnownEndpoints passedAuthWellKnownEndpoints

View File

@@ -1,5 +1,5 @@
import { LogLevel } from '../logging/log-level'; import { LogLevel } from '../logging/log-level';
import { OpenIdConfiguration } from './openid-configuration'; import type { OpenIdConfiguration } from './openid-configuration';
export const DEFAULT_CONFIG: OpenIdConfiguration = { export const DEFAULT_CONFIG: OpenIdConfiguration = {
authority: 'https://please_set', authority: 'https://please_set',

View File

@@ -1,12 +1,11 @@
import { waitForAsync } from '@angular/core/testing'; import { firstValueFrom, of } from 'rxjs';
import { of } from 'rxjs'; import type { OpenIdConfiguration } from '../openid-configuration';
import { OpenIdConfiguration } from '../openid-configuration';
import { StsConfigHttpLoader, StsConfigStaticLoader } from './config-loader'; import { StsConfigHttpLoader, StsConfigStaticLoader } from './config-loader';
describe('ConfigLoader', () => { describe('ConfigLoader', () => {
describe('StsConfigStaticLoader', () => { describe('StsConfigStaticLoader', () => {
describe('loadConfigs', () => { describe('loadConfigs', () => {
it('returns an array if an array is passed', waitForAsync(() => { it('returns an array if an array is passed', async () => {
const toPass = [ const toPass = [
{ configId: 'configId1' } as OpenIdConfiguration, { configId: 'configId1' } as OpenIdConfiguration,
{ configId: 'configId2' } as OpenIdConfiguration, { configId: 'configId2' } as OpenIdConfiguration,
@@ -16,28 +15,26 @@ describe('ConfigLoader', () => {
const result$ = loader.loadConfigs(); const result$ = loader.loadConfigs();
result$.subscribe((result) => { const result = await firstValueFrom(result$);
expect(Array.isArray(result)).toBeTrue(); expect(Array.isArray(result)).toBeTruthy();
}); });
}));
it('returns an array if only one config is passed', waitForAsync(() => { it('returns an array if only one config is passed', async () => {
const loader = new StsConfigStaticLoader({ const loader = new StsConfigStaticLoader({
configId: 'configId1', configId: 'configId1',
} as OpenIdConfiguration); } as OpenIdConfiguration);
const result$ = loader.loadConfigs(); const result$ = loader.loadConfigs();
result$.subscribe((result) => { const result = await firstValueFrom(result$);
expect(Array.isArray(result)).toBeTrue(); expect(Array.isArray(result)).toBeTruthy();
}); });
}));
}); });
}); });
describe('StsConfigHttpLoader', () => { describe('StsConfigHttpLoader', () => {
describe('loadConfigs', () => { describe('loadConfigs', () => {
it('returns an array if an array of observables is passed', waitForAsync(() => { it('returns an array if an array of observables is passed', async () => {
const toPass = [ const toPass = [
of({ configId: 'configId1' } as OpenIdConfiguration), of({ configId: 'configId1' } as OpenIdConfiguration),
of({ configId: 'configId2' } as OpenIdConfiguration), of({ configId: 'configId2' } as OpenIdConfiguration),
@@ -46,14 +43,13 @@ describe('ConfigLoader', () => {
const result$ = loader.loadConfigs(); const result$ = loader.loadConfigs();
result$.subscribe((result) => { const result = await firstValueFrom(result$);
expect(Array.isArray(result)).toBeTrue(); expect(Array.isArray(result)).toBeTruthy();
expect(result[0].configId).toBe('configId1'); expect(result[0]!.configId).toBe('configId1');
expect(result[1].configId).toBe('configId2'); expect(result[1]!.configId).toBe('configId2');
}); });
}));
it('returns an array if an observable with a config array is passed', waitForAsync(() => { it('returns an array if an observable with a config array is passed', async () => {
const toPass = of([ const toPass = of([
{ configId: 'configId1' } as OpenIdConfiguration, { configId: 'configId1' } as OpenIdConfiguration,
{ configId: 'configId2' } as OpenIdConfiguration, { configId: 'configId2' } as OpenIdConfiguration,
@@ -62,25 +58,23 @@ describe('ConfigLoader', () => {
const result$ = loader.loadConfigs(); const result$ = loader.loadConfigs();
result$.subscribe((result) => { const result = await firstValueFrom(result$);
expect(Array.isArray(result)).toBeTrue(); expect(Array.isArray(result)).toBeTruthy();
expect(result[0].configId).toBe('configId1'); expect(result[0]!.configId).toBe('configId1');
expect(result[1].configId).toBe('configId2'); expect(result[1]!.configId).toBe('configId2');
}); });
}));
it('returns an array if only one config is passed', waitForAsync(() => { it('returns an array if only one config is passed', async () => {
const loader = new StsConfigHttpLoader( const loader = new StsConfigHttpLoader(
of({ configId: 'configId1' } as OpenIdConfiguration) of({ configId: 'configId1' } as OpenIdConfiguration)
); );
const result$ = loader.loadConfigs(); const result$ = loader.loadConfigs();
result$.subscribe((result) => { const result = await firstValueFrom(result$);
expect(Array.isArray(result)).toBeTrue(); expect(Array.isArray(result)).toBeTruthy();
expect(result[0].configId).toBe('configId1'); expect(result[0]!.configId).toBe('configId1');
}); });
}));
}); });
}); });
}); });

View File

@@ -1,7 +1,7 @@
import { Provider } from 'injection-js'; import type { Provider } from '@outposts/injection-js';
import { forkJoin, Observable, of } from 'rxjs'; import { type Observable, forkJoin, of } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { OpenIdConfiguration } from '../openid-configuration'; import type { OpenIdConfiguration } from '../openid-configuration';
export class OpenIdConfigLoader { export class OpenIdConfigLoader {
loader?: Provider; loader?: Provider;

View File

@@ -1,5 +1,5 @@
import { LogLevel } from '../logging/log-level'; import type { LogLevel } from '../logging/log-level';
import { AuthWellKnownEndpoints } from './auth-well-known/auth-well-known-endpoints'; import type { AuthWellKnownEndpoints } from './auth-well-known/auth-well-known-endpoints';
export interface OpenIdConfiguration { export interface OpenIdConfiguration {
/** /**
@@ -40,6 +40,12 @@ export interface OpenIdConfiguration {
* or if it contains additional audiences not trusted by the Client. * or if it contains additional audiences not trusted by the Client.
*/ */
clientId?: string; clientId?: string;
/**
* @dangerous
* @see [client secret is missing](https://github.com/damienbod/angular-auth-oidc-client/issues/399)
* The client secret. For some oidc service the must provide this.
*/
clientSecret?: string;
/** /**
* `code`, `id_token token` or `id_token`. * `code`, `id_token token` or `id_token`.
* Name of the flow which can be configured. * Name of the flow which can be configured.
@@ -207,5 +213,5 @@ export interface OpenIdConfiguration {
/** /**
* Disable cleaning up the popup when receiving invalid messages * Disable cleaning up the popup when receiving invalid messages
*/ */
disableCleaningPopupOnInvalidMessage?: boolean disableCleaningPopupOnInvalidMessage?: boolean;
} }

View File

@@ -1,8 +1,10 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@/testing';
import { mockProvider } from '../../../test/auto-mock'; import { mockImplementationWhenArgs, spyOnWithOrigin } from '@/testing/spy';
import { vi } from 'vitest';
import { LogLevel } from '../../logging/log-level'; import { LogLevel } from '../../logging/log-level';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { OpenIdConfiguration } from '../openid-configuration'; import { mockProvider } from '../../testing/mock';
import type { OpenIdConfiguration } from '../openid-configuration';
import { ConfigValidationService } from './config-validation.service'; import { ConfigValidationService } from './config-validation.service';
import { allMultipleConfigRules } from './rules'; import { allMultipleConfigRules } from './rules';
@@ -14,6 +16,8 @@ describe('Config Validation Service', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ConfigValidationService, mockProvider(LoggerService)], providers: [ConfigValidationService, mockProvider(LoggerService)],
}); });
configValidationService = TestBed.inject(ConfigValidationService);
loggerService = TestBed.inject(LoggerService);
}); });
const VALID_CONFIG = { const VALID_CONFIG = {
@@ -29,11 +33,6 @@ describe('Config Validation Service', () => {
logLevel: LogLevel.Debug, logLevel: LogLevel.Debug,
}; };
beforeEach(() => {
configValidationService = TestBed.inject(ConfigValidationService);
loggerService = TestBed.inject(LoggerService);
});
it('should create', () => { it('should create', () => {
expect(configValidationService).toBeTruthy(); expect(configValidationService).toBeTruthy();
}); });
@@ -42,26 +41,27 @@ describe('Config Validation Service', () => {
const config = {}; const config = {};
const result = configValidationService.validateConfig(config); const result = configValidationService.validateConfig(config);
expect(result).toBeFalse(); expect(result).toBeFalsy();
}); });
it('should return true for valid config', () => { it('should return true for valid config', () => {
const result = configValidationService.validateConfig(VALID_CONFIG); const result = configValidationService.validateConfig(VALID_CONFIG);
expect(result).toBeTrue(); expect(result).toBeTruthy();
}); });
it('calls `logWarning` if one rule has warning level', () => { it('calls `logWarning` if one rule has warning level', () => {
const loggerWarningSpy = spyOn(loggerService, 'logWarning'); const loggerWarningSpy = vi.spyOn(loggerService, 'logWarning');
const messageTypeSpy = spyOn( const messageTypeSpy = spyOnWithOrigin(
configValidationService as any, configValidationService,
'getAllMessagesOfType' 'getAllMessagesOfType' as any
); );
messageTypeSpy mockImplementationWhenArgs(
.withArgs('warning', jasmine.any(Array)) messageTypeSpy,
.and.returnValue(['A warning message']); (arg1: any, arg2: any) => arg1 === 'warning' && Array.isArray(arg2),
messageTypeSpy.withArgs('error', jasmine.any(Array)).and.callThrough(); () => ['A warning message']
);
configValidationService.validateConfig(VALID_CONFIG); configValidationService.validateConfig(VALID_CONFIG);
expect(loggerWarningSpy).toHaveBeenCalled(); expect(loggerWarningSpy).toHaveBeenCalled();
@@ -72,7 +72,7 @@ describe('Config Validation Service', () => {
const config = { ...VALID_CONFIG, clientId: '' } as OpenIdConfiguration; const config = { ...VALID_CONFIG, clientId: '' } as OpenIdConfiguration;
const result = configValidationService.validateConfig(config); const result = configValidationService.validateConfig(config);
expect(result).toBeFalse(); expect(result).toBeFalsy();
}); });
}); });
@@ -84,7 +84,7 @@ describe('Config Validation Service', () => {
} as OpenIdConfiguration; } as OpenIdConfiguration;
const result = configValidationService.validateConfig(config); const result = configValidationService.validateConfig(config);
expect(result).toBeFalse(); expect(result).toBeFalsy();
}); });
}); });
@@ -93,7 +93,7 @@ describe('Config Validation Service', () => {
const config = { ...VALID_CONFIG, redirectUrl: '' }; const config = { ...VALID_CONFIG, redirectUrl: '' };
const result = configValidationService.validateConfig(config); const result = configValidationService.validateConfig(config);
expect(result).toBeFalse(); expect(result).toBeFalsy();
}); });
}); });
@@ -107,7 +107,7 @@ describe('Config Validation Service', () => {
} as OpenIdConfiguration; } as OpenIdConfiguration;
const result = configValidationService.validateConfig(config); const result = configValidationService.validateConfig(config);
expect(result).toBeFalse(); expect(result).toBeFalsy();
}); });
}); });
@@ -120,12 +120,12 @@ describe('Config Validation Service', () => {
scopes: 'scope1 scope2 but_no_offline_access', scopes: 'scope1 scope2 but_no_offline_access',
}; };
const loggerSpy = spyOn(loggerService, 'logError'); const loggerSpy = vi.spyOn(loggerService, 'logError');
const loggerWarningSpy = spyOn(loggerService, 'logWarning'); const loggerWarningSpy = vi.spyOn(loggerService, 'logWarning');
const result = configValidationService.validateConfig(config); const result = configValidationService.validateConfig(config);
expect(result).toBeTrue(); expect(result).toBeTruthy();
expect(loggerSpy).not.toHaveBeenCalled(); expect(loggerSpy).not.toHaveBeenCalled();
expect(loggerWarningSpy).toHaveBeenCalled(); expect(loggerWarningSpy).toHaveBeenCalled();
}); });
@@ -146,47 +146,47 @@ describe('Config Validation Service', () => {
scopes: 'scope1 scope2 but_no_offline_access', scopes: 'scope1 scope2 but_no_offline_access',
}; };
const loggerErrorSpy = spyOn(loggerService, 'logError'); const loggerErrorSpy = vi.spyOn(loggerService, 'logError');
const loggerWarningSpy = spyOn(loggerService, 'logWarning'); const loggerWarningSpy = vi.spyOn(loggerService, 'logWarning');
const result = configValidationService.validateConfigs([ const result = configValidationService.validateConfigs([
config1, config1,
config2, config2,
]); ]);
expect(result).toBeTrue(); expect(result).toBeTruthy();
expect(loggerErrorSpy).not.toHaveBeenCalled(); expect(loggerErrorSpy).not.toHaveBeenCalled();
expect(loggerWarningSpy.calls.argsFor(0)).toEqual([ expect(vi.mocked(loggerWarningSpy).mock.calls[0]).toEqual([
config1, config1,
'You added multiple configs with the same authority, clientId and scope', 'You added multiple configs with the same authority, clientId and scope',
]); ]);
expect(loggerWarningSpy.calls.argsFor(1)).toEqual([ expect(vi.mocked(loggerWarningSpy).mock.calls[1]).toEqual([
config2, config2,
'You added multiple configs with the same authority, clientId and scope', 'You added multiple configs with the same authority, clientId and scope',
]); ]);
}); });
it('should return false and a better error message when config is not passed as object with config property', () => { it('should return false and a better error message when config is not passed as object with config property', () => {
const loggerWarningSpy = spyOn(loggerService, 'logWarning'); const loggerWarningSpy = vi.spyOn(loggerService, 'logWarning');
const result = configValidationService.validateConfigs([]); const result = configValidationService.validateConfigs([]);
expect(result).toBeFalse(); expect(result).toBeFalsy();
expect(loggerWarningSpy).not.toHaveBeenCalled(); expect(loggerWarningSpy).not.toHaveBeenCalled();
}); });
}); });
describe('validateConfigs', () => { describe('validateConfigs', () => {
it('calls internal method with empty array if something falsy is passed', () => { it('calls internal method with empty array if something falsy is passed', () => {
const spy = spyOn( const spy = vi.spyOn(
configValidationService as any, configValidationService as any,
'validateConfigsInternal' 'validateConfigsInternal'
).and.callThrough(); );
const result = configValidationService.validateConfigs([]); const result = configValidationService.validateConfigs([]);
expect(result).toBeFalse(); expect(result).toBeFalsy();
expect(spy).toHaveBeenCalledOnceWith([], allMultipleConfigRules); expect(spy).toHaveBeenCalledExactlyOnceWith([], allMultipleConfigRules);
}); });
}); });
}); });

View File

@@ -1,7 +1,7 @@
import { inject, Injectable } from 'injection-js'; import { Injectable, inject } from '@outposts/injection-js';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { OpenIdConfiguration } from '../openid-configuration'; import type { OpenIdConfiguration } from '../openid-configuration';
import { Level, RuleValidationResult } from './rule'; import type { Level, RuleValidationResult } from './rule';
import { allMultipleConfigRules, allRules } from './rules'; import { allMultipleConfigRules, allRules } from './rules';
@Injectable() @Injectable()
@@ -35,14 +35,14 @@ export class ConfigValidationService {
let overallErrorCount = 0; let overallErrorCount = 0;
passedConfigs.forEach((passedConfig) => { for (const passedConfig of passedConfigs) {
const errorCount = this.processValidationResultsAndGetErrorCount( const errorCount = this.processValidationResultsAndGetErrorCount(
allValidationResults, allValidationResults,
passedConfig passedConfig
); );
overallErrorCount += errorCount; overallErrorCount += errorCount;
}); }
return overallErrorCount === 0; return overallErrorCount === 0;
} }
@@ -75,17 +75,17 @@ export class ConfigValidationService {
const allErrorMessages = this.getAllMessagesOfType('error', allMessages); const allErrorMessages = this.getAllMessagesOfType('error', allMessages);
const allWarnings = this.getAllMessagesOfType('warning', allMessages); const allWarnings = this.getAllMessagesOfType('warning', allMessages);
allErrorMessages.forEach((message) => for (const message of allErrorMessages) {
this.loggerService.logError(config, message) this.loggerService.logError(config, message);
); }
allWarnings.forEach((message) => for (const message of allWarnings) {
this.loggerService.logWarning(config, message) this.loggerService.logWarning(config, message);
); }
return allErrorMessages.length; return allErrorMessages.length;
} }
private getAllMessagesOfType( protected getAllMessagesOfType(
type: Level, type: Level,
results: RuleValidationResult[] results: RuleValidationResult[]
): string[] { ): string[] {

View File

@@ -1,4 +1,4 @@
import { OpenIdConfiguration } from '../openid-configuration'; import type { OpenIdConfiguration } from '../openid-configuration';
export interface Rule { export interface Rule {
validate(passedConfig: OpenIdConfiguration): RuleValidationResult; validate(passedConfig: OpenIdConfiguration): RuleValidationResult;

View File

@@ -1,5 +1,5 @@
import { OpenIdConfiguration } from '../../openid-configuration'; import type { OpenIdConfiguration } from '../../openid-configuration';
import { POSITIVE_VALIDATION_RESULT, RuleValidationResult } from '../rule'; import { POSITIVE_VALIDATION_RESULT, type RuleValidationResult } from '../rule';
export const ensureAuthority = ( export const ensureAuthority = (
passedConfig: OpenIdConfiguration passedConfig: OpenIdConfiguration

View File

@@ -1,5 +1,5 @@
import { OpenIdConfiguration } from '../../openid-configuration'; import type { OpenIdConfiguration } from '../../openid-configuration';
import { POSITIVE_VALIDATION_RESULT, RuleValidationResult } from '../rule'; import { POSITIVE_VALIDATION_RESULT, type RuleValidationResult } from '../rule';
export const ensureClientId = ( export const ensureClientId = (
passedConfig: OpenIdConfiguration passedConfig: OpenIdConfiguration

View File

@@ -1,5 +1,5 @@
import { OpenIdConfiguration } from '../../openid-configuration'; import type { OpenIdConfiguration } from '../../openid-configuration';
import { POSITIVE_VALIDATION_RESULT, RuleValidationResult } from '../rule'; import { POSITIVE_VALIDATION_RESULT, type RuleValidationResult } from '../rule';
const createIdentifierToCheck = (passedConfig: OpenIdConfiguration): string => { const createIdentifierToCheck = (passedConfig: OpenIdConfiguration): string => {
if (!passedConfig) { if (!passedConfig) {

View File

@@ -1,5 +1,5 @@
import { OpenIdConfiguration } from '../../openid-configuration'; import type { OpenIdConfiguration } from '../../openid-configuration';
import { POSITIVE_VALIDATION_RESULT, RuleValidationResult } from '../rule'; import { POSITIVE_VALIDATION_RESULT, type RuleValidationResult } from '../rule';
export const ensureRedirectRule = ( export const ensureRedirectRule = (
passedConfig: OpenIdConfiguration passedConfig: OpenIdConfiguration

View File

@@ -1,5 +1,5 @@
import { OpenIdConfiguration } from '../../openid-configuration'; import type { OpenIdConfiguration } from '../../openid-configuration';
import { POSITIVE_VALIDATION_RESULT, RuleValidationResult } from '../rule'; import { POSITIVE_VALIDATION_RESULT, type RuleValidationResult } from '../rule';
export const ensureSilentRenewUrlWhenNoRefreshTokenUsed = ( export const ensureSilentRenewUrlWhenNoRefreshTokenUsed = (
passedConfig: OpenIdConfiguration passedConfig: OpenIdConfiguration

View File

@@ -1,5 +1,5 @@
import { OpenIdConfiguration } from '../../openid-configuration'; import type { OpenIdConfiguration } from '../../openid-configuration';
import { POSITIVE_VALIDATION_RESULT, RuleValidationResult } from '../rule'; import { POSITIVE_VALIDATION_RESULT, type RuleValidationResult } from '../rule';
export const useOfflineScopeWithSilentRenew = ( export const useOfflineScopeWithSilentRenew = (
passedConfig: OpenIdConfiguration passedConfig: OpenIdConfiguration

View File

@@ -1,3 +1,3 @@
import { InjectionToken } from "injection-js"; import { InjectionToken } from '@outposts/injection-js';
export const DOCUMENT = new InjectionToken<Document>('document'); export const DOCUMENT = new InjectionToken<Document>('document');

View File

@@ -1,4 +1,4 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@/testing';
import { CryptoService } from '../utils/crypto/crypto.service'; import { CryptoService } from '../utils/crypto/crypto.service';
import { import {
JwkExtractor, JwkExtractor,
@@ -93,9 +93,6 @@ describe('JwkExtractor', () => {
imports: [], imports: [],
providers: [JwkExtractor, CryptoService], providers: [JwkExtractor, CryptoService],
}); });
});
beforeEach(() => {
service = TestBed.inject(JwkExtractor); service = TestBed.inject(JwkExtractor);
}); });
@@ -105,21 +102,30 @@ describe('JwkExtractor', () => {
describe('extractJwk', () => { describe('extractJwk', () => {
it('throws error if no keys are present in array', () => { it('throws error if no keys are present in array', () => {
expect(() => { try {
service.extractJwk([]); 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', () => { it('throws error if spec.kid is present, but no key was matching', () => {
expect(() => { try {
service.extractJwk(keys, { kid: 'doot' }); 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', () => { it('throws error if spec.use is present, but no key was matching', () => {
expect(() => { try {
service.extractJwk(keys, { use: 'blorp' }); 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', () => { it('does not throw error if no key is matching when throwOnEmpty is false', () => {
@@ -129,9 +135,12 @@ describe('JwkExtractor', () => {
}); });
it('throws error if multiple keys are present, and spec is not present', () => { it('throws error if multiple keys are present, and spec is not present', () => {
expect(() => { try {
service.extractJwk(keys); service.extractJwk(keys);
}).toThrow(JwkExtractorSeveralMatchingKeysError); expect.fail('should error');
} catch (error: any) {
expect(error).toBe(JwkExtractorSeveralMatchingKeysError);
}
}); });
it('returns array of keys matching spec.kid', () => { it('returns array of keys matching spec.kid', () => {

View File

@@ -1,4 +1,4 @@
import { Injectable } from 'injection-js'; import { Injectable } from '@outposts/injection-js';
@Injectable() @Injectable()
export class JwkExtractor { export class JwkExtractor {
@@ -7,20 +7,20 @@ export class JwkExtractor {
spec?: { kid?: string; use?: string; kty?: string }, spec?: { kid?: string; use?: string; kty?: string },
throwOnEmpty = true throwOnEmpty = true
): JsonWebKey[] { ): JsonWebKey[] {
if (0 === keys.length) { if (keys.length === 0) {
throw JwkExtractorInvalidArgumentError; throw JwkExtractorInvalidArgumentError;
} }
const foundKeys = keys const foundKeys = keys
.filter((k) => (spec?.kid ? (k as any)['kid'] === spec.kid : true)) .filter((k) => (spec?.kid ? (k as any).kid === spec.kid : true))
.filter((k) => (spec?.use ? k['use'] === spec.use : true)) .filter((k) => (spec?.use ? k.use === spec.use : true))
.filter((k) => (spec?.kty ? k['kty'] === spec.kty : true)); .filter((k) => (spec?.kty ? k.kty === spec.kty : true));
if (foundKeys.length === 0 && throwOnEmpty) { if (foundKeys.length === 0 && throwOnEmpty) {
throw JwkExtractorNoMatchingKeysError; throw JwkExtractorNoMatchingKeysError;
} }
if (foundKeys.length > 1 && (null === spec || undefined === spec)) { if (foundKeys.length > 1 && (spec === null || undefined === spec)) {
throw JwkExtractorSeveralMatchingKeysError; throw JwkExtractorSeveralMatchingKeysError;
} }
@@ -29,7 +29,7 @@ export class JwkExtractor {
} }
function buildErrorName(name: string): string { function buildErrorName(name: string): string {
return JwkExtractor.name + ': ' + name; return `${JwkExtractor.name}: ${name}`;
} }
export const JwkExtractorInvalidArgumentError = { export const JwkExtractorInvalidArgumentError = {

131
src/features/core.ts Normal file
View File

@@ -0,0 +1,131 @@
import type { HttpFeature } from '@ngify/http';
import type { Provider } from '@outposts/injection-js';
import { DOCUMENT } from '../dom';
import { provideHttpClient } from '../http';
import {
AbstractRouter,
VanillaHistoryRouter,
VanillaLocationRouter,
} from '../router';
import { AbstractSecurityStorage } from '../storage/abstract-security-storage';
import { DefaultLocalStorageService } from '../storage/default-localstorage.service';
import { DefaultSessionStorageService } from '../storage/default-sessionstorage.service';
import { PLATFORM_ID } from '../utils/platform-provider/platform.provider';
/**
* A feature to be used with `provideAuth`.
*/
export interface AuthFeature {
ɵproviders: Provider[];
}
export interface BrowserPlatformFeatureOptions {
enabled?: boolean;
}
export function withBrowserPlatform({
enabled = true,
}: BrowserPlatformFeatureOptions = {}): AuthFeature {
return {
ɵproviders: enabled
? [
{
provide: DOCUMENT,
useFactory: () => document,
},
{
provide: PLATFORM_ID,
useValue: 'browser',
},
]
: [],
};
}
export interface HttpClientFeatureOptions {
enabled?: boolean;
features?: HttpFeature[];
}
export function withHttpClient({
features,
enabled = true,
}: HttpClientFeatureOptions = {}): AuthFeature {
return {
ɵproviders: enabled ? provideHttpClient(features) : [],
};
}
export type SecurityStorageType = 'session-storage' | 'local-storage';
export interface SecurityStorageFeatureOptions {
enabled?: boolean;
type?: SecurityStorageType;
}
export function withSecurityStorage({
enabled = true,
type = 'session-storage',
}: SecurityStorageFeatureOptions = {}): AuthFeature {
return {
ɵproviders: enabled
? [
type === 'session-storage'
? {
provide: AbstractSecurityStorage,
useClass: DefaultLocalStorageService,
}
: {
provide: AbstractSecurityStorage,
useClass: DefaultSessionStorageService,
},
]
: [],
};
}
export type VanillaRouterType = 'location' | 'history';
export interface VanillaRouterFeatureOptions {
enabled?: boolean;
type?: VanillaRouterType;
}
export function withVanillaRouter({
enabled = true,
type = 'history',
}: VanillaRouterFeatureOptions = {}): AuthFeature {
return {
ɵproviders: enabled
? [
type === 'location'
? {
provide: AbstractRouter,
useClass: VanillaLocationRouter,
}
: {
provide: AbstractRouter,
useClass: VanillaHistoryRouter,
},
]
: [],
};
}
export interface DefaultFeaturesOptions {
browserPlatform?: BrowserPlatformFeatureOptions;
securityStorage?: SecurityStorageFeatureOptions;
router?: VanillaRouterFeatureOptions;
httpClient?: HttpClientFeatureOptions;
}
export function withDefaultFeatures(
options: DefaultFeaturesOptions = {}
): AuthFeature[] {
return [
withBrowserPlatform(options.browserPlatform),
withSecurityStorage(options.securityStorage),
withHttpClient(options.httpClient),
withVanillaRouter(options.router),
].filter(Boolean) as AuthFeature[];
}

8
src/features/index.ts Normal file
View File

@@ -0,0 +1,8 @@
export type * from './core';
export * from './core';
export {
CHECK_AUTH_RESULT_EVENT,
withCheckAuthResultEvent,
type CheckAuthResultEventType,
type WithCheckAuthResultEventProps,
} from './with-check-auth-result-event';

View File

@@ -0,0 +1,45 @@
import { InjectionToken, inject } from '@outposts/injection-js';
import { type Observable, filter, shareReplay } from 'rxjs';
import { EventTypes } from '../public-events/event-types';
import { PublicEventsService } from '../public-events/public-events.service';
import type { AuthFeature } from './core';
export type CheckAuthResultEventType =
| { type: EventTypes.CheckingAuthFinished }
| {
type: EventTypes.CheckingAuthFinishedWithError;
value: string;
};
export const CHECK_AUTH_RESULT_EVENT = new InjectionToken<
Observable<CheckAuthResultEventType>
>('CHECK_AUTH_RESULT_EVENT');
export interface WithCheckAuthResultEventProps {
shareReplayCount?: number;
}
export function withCheckAuthResultEvent({
shareReplayCount = 1,
}: WithCheckAuthResultEventProps = {}): AuthFeature {
return {
ɵproviders: [
{
provide: CHECK_AUTH_RESULT_EVENT,
useFactory: () => {
const publishEventService = inject(PublicEventsService);
return publishEventService.registerForEvents().pipe(
filter(
(e) =>
e.type === EventTypes.CheckingAuthFinishedWithError ||
e.type === EventTypes.CheckingAuthFinished
),
shareReplay(shareReplayCount)
);
},
deps: [PublicEventsService],
},
],
};
}

View File

@@ -1,5 +1,5 @@
import { JwtKeys } from '../validation/jwtkeys'; import type { JwtKeys } from '../validation/jwtkeys';
import { StateValidationResult } from '../validation/state-validation-result'; import type { StateValidationResult } from '../validation/state-validation-result';
export interface CallbackContext { export interface CallbackContext {
code: string; code: string;

View File

@@ -1,14 +1,15 @@
import { HttpErrorResponse, HttpHeaders } from '@angular/common/http'; import { TestBed, mockImplementationWhenArgsEqual } from '@/testing';
import { TestBed, waitForAsync } from '@angular/core/testing'; import { HttpErrorResponse, HttpHeaders } from '@ngify/http';
import { of, throwError } from 'rxjs'; import { firstValueFrom, of, throwError } from 'rxjs';
import { mockProvider } from '../../../test/auto-mock'; import { vi } from 'vitest';
import { createRetriableStream } from '../../../test/create-retriable-stream.helper';
import { DataService } from '../../api/data.service'; import { DataService } from '../../api/data.service';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { StoragePersistenceService } from '../../storage/storage-persistence.service'; import { StoragePersistenceService } from '../../storage/storage-persistence.service';
import { createRetriableStream } from '../../testing/create-retriable-stream.helper';
import { mockProvider } from '../../testing/mock';
import { UrlService } from '../../utils/url/url.service'; import { UrlService } from '../../utils/url/url.service';
import { TokenValidationService } from '../../validation/token-validation.service'; import { TokenValidationService } from '../../validation/token-validation.service';
import { CallbackContext } from '../callback-context'; import type { CallbackContext } from '../callback-context';
import { FlowsDataService } from '../flows-data.service'; import { FlowsDataService } from '../flows-data.service';
import { CodeFlowCallbackHandlerService } from './code-flow-callback-handler.service'; import { CodeFlowCallbackHandlerService } from './code-flow-callback-handler.service';
@@ -31,9 +32,6 @@ describe('CodeFlowCallbackHandlerService', () => {
mockProvider(DataService), mockProvider(DataService),
], ],
}); });
});
beforeEach(() => {
service = TestBed.inject(CodeFlowCallbackHandlerService); service = TestBed.inject(CodeFlowCallbackHandlerService);
dataService = TestBed.inject(DataService); dataService = TestBed.inject(DataService);
urlService = TestBed.inject(UrlService); urlService = TestBed.inject(UrlService);
@@ -46,42 +44,48 @@ describe('CodeFlowCallbackHandlerService', () => {
}); });
describe('codeFlowCallback', () => { describe('codeFlowCallback', () => {
it('throws error if no state is given', waitForAsync(() => { it('throws error if no state is given', async () => {
const getUrlParameterSpy = spyOn( const getUrlParameterSpy = vi
urlService, .spyOn(urlService, 'getUrlParameter')
'getUrlParameter' .mockReturnValue('params');
).and.returnValue('params');
getUrlParameterSpy.withArgs('test-url', 'state').and.returnValue(''); mockImplementationWhenArgsEqual(
getUrlParameterSpy,
['test-url', 'state'],
() => ''
);
service try {
.codeFlowCallback('test-url', { configId: 'configId1' }) await firstValueFrom(
.subscribe({ service.codeFlowCallback('test-url', { configId: 'configId1' })
error: (err) => { );
expect(err).toBeTruthy(); } catch (err: any) {
}, expect(err).toBeTruthy();
}); }
})); });
it('throws error if no code is given', waitForAsync(() => { it('throws error if no code is given', async () => {
const getUrlParameterSpy = spyOn( const getUrlParameterSpy = vi
urlService, .spyOn(urlService, 'getUrlParameter')
'getUrlParameter' .mockReturnValue('params');
).and.returnValue('params');
getUrlParameterSpy.withArgs('test-url', 'code').and.returnValue(''); mockImplementationWhenArgsEqual(
getUrlParameterSpy,
['test-url', 'code'],
() => ''
);
service try {
.codeFlowCallback('test-url', { configId: 'configId1' }) await firstValueFrom(
.subscribe({ service.codeFlowCallback('test-url', { configId: 'configId1' })
error: (err) => { );
expect(err).toBeTruthy(); } catch (err: any) {
}, expect(err).toBeTruthy();
}); }
})); });
it('returns callbackContext if all params are good', waitForAsync(() => { it('returns callbackContext if all params are good', async () => {
spyOn(urlService, 'getUrlParameter').and.returnValue('params'); vi.spyOn(urlService, 'getUrlParameter').mockReturnValue('params');
const expectedCallbackContext = { const expectedCallbackContext = {
code: 'params', code: 'params',
@@ -95,12 +99,11 @@ describe('CodeFlowCallbackHandlerService', () => {
existingIdToken: null, existingIdToken: null,
} as CallbackContext; } as CallbackContext;
service const callbackContext = await firstValueFrom(
.codeFlowCallback('test-url', { configId: 'configId1' }) service.codeFlowCallback('test-url', { configId: 'configId1' })
.subscribe((callbackContext) => { );
expect(callbackContext).toEqual(expectedCallbackContext); expect(callbackContext).toEqual(expectedCallbackContext);
}); });
}));
}); });
describe('codeFlowCodeRequest ', () => { describe('codeFlowCodeRequest ', () => {
@@ -112,83 +115,96 @@ describe('CodeFlowCallbackHandlerService', () => {
url: 'https://identity-server.test/openid-connect/token', url: 'https://identity-server.test/openid-connect/token',
}); });
it('throws error if state is not correct', waitForAsync(() => { it('throws error if state is not correct', async () => {
spyOn( vi.spyOn(
tokenValidationService, tokenValidationService,
'validateStateFromHashCallback' 'validateStateFromHashCallback'
).and.returnValue(false); ).mockReturnValue(false);
service try {
.codeFlowCodeRequest({} as CallbackContext, { configId: 'configId1' }) await firstValueFrom(
.subscribe({ service.codeFlowCodeRequest({} as CallbackContext, {
error: (err) => { configId: 'configId1',
expect(err).toBeTruthy(); })
}, );
}); } catch (err: any) {
})); expect(err).toBeTruthy();
}
});
it('throws error if authWellknownEndpoints is null is given', waitForAsync(() => { it('throws error if authWellknownEndpoints is null is given', async () => {
spyOn( vi.spyOn(
tokenValidationService, tokenValidationService,
'validateStateFromHashCallback' 'validateStateFromHashCallback'
).and.returnValue(true); ).mockReturnValue(true);
spyOn(storagePersistenceService, 'read') mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', { configId: 'configId1' }) vi.spyOn(storagePersistenceService, 'read'),
.and.returnValue(null); ['authWellKnownEndPoints', { configId: 'configId1' }],
() => null
);
service try {
.codeFlowCodeRequest({} as CallbackContext, { configId: 'configId1' }) await firstValueFrom(
.subscribe({ service.codeFlowCodeRequest({} as CallbackContext, {
error: (err) => { configId: 'configId1',
expect(err).toBeTruthy(); })
}, );
}); } catch (err: any) {
})); expect(err).toBeTruthy();
}
});
it('throws error if tokenendpoint is null is given', waitForAsync(() => { it('throws error if tokenendpoint is null is given', async () => {
spyOn( vi.spyOn(
tokenValidationService, tokenValidationService,
'validateStateFromHashCallback' 'validateStateFromHashCallback'
).and.returnValue(true); ).mockReturnValue(true);
spyOn(storagePersistenceService, 'read') mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', { configId: 'configId1' }) vi.spyOn(storagePersistenceService, 'read'),
.and.returnValue({ tokenEndpoint: null }); ['authWellKnownEndPoints', { configId: 'configId1' }],
() => ({ tokenEndpoint: null })
);
service try {
.codeFlowCodeRequest({} as CallbackContext, { configId: 'configId1' }) await firstValueFrom(
.subscribe({ service.codeFlowCodeRequest({} as CallbackContext, {
error: (err) => { configId: 'configId1',
expect(err).toBeTruthy(); })
}, );
}); } catch (err: any) {
})); expect(err).toBeTruthy();
}
});
it('calls dataService if all params are good', waitForAsync(() => { it('calls dataService if all params are good', async () => {
const postSpy = spyOn(dataService, 'post').and.returnValue(of({})); const postSpy = vi.spyOn(dataService, 'post').mockReturnValue(of({}));
spyOn(storagePersistenceService, 'read') mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', { configId: 'configId1' }) vi.spyOn(storagePersistenceService, 'read'),
.and.returnValue({ tokenEndpoint: 'tokenEndpoint' }); ['authWellKnownEndPoints', { configId: 'configId1' }],
() => ({ tokenEndpoint: 'tokenEndpoint' })
);
spyOn( vi.spyOn(
tokenValidationService, tokenValidationService,
'validateStateFromHashCallback' 'validateStateFromHashCallback'
).and.returnValue(true); ).mockReturnValue(true);
service await firstValueFrom(
.codeFlowCodeRequest({} as CallbackContext, { configId: 'configId1' }) service.codeFlowCodeRequest({} as CallbackContext, {
.subscribe(() => { configId: 'configId1',
expect(postSpy).toHaveBeenCalledOnceWith( })
'tokenEndpoint', );
undefined, expect(postSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, 'tokenEndpoint',
jasmine.any(HttpHeaders) undefined,
); { configId: 'configId1' },
}); expect.any(HttpHeaders)
})); );
});
it('calls url service with custom token params', waitForAsync(() => { it('calls url service with custom token params', async () => {
const urlServiceSpy = spyOn( const urlServiceSpy = vi.spyOn(
urlService, urlService,
'createBodyForCodeFlowCodeRequest' 'createBodyForCodeFlowCodeRequest'
); );
@@ -197,76 +213,83 @@ describe('CodeFlowCallbackHandlerService', () => {
customParamsCodeRequest: { foo: 'bar' }, customParamsCodeRequest: { foo: 'bar' },
}; };
spyOn(storagePersistenceService, 'read') mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) vi.spyOn(storagePersistenceService, 'read'),
.and.returnValue({ tokenEndpoint: 'tokenEndpoint' }); ['authWellKnownEndPoints', config],
() => ({ tokenEndpoint: 'tokenEndpoint' })
);
spyOn( vi.spyOn(
tokenValidationService, tokenValidationService,
'validateStateFromHashCallback' 'validateStateFromHashCallback'
).and.returnValue(true); ).mockReturnValue(true);
const postSpy = spyOn(dataService, 'post').and.returnValue(of({})); const postSpy = vi.spyOn(dataService, 'post').mockReturnValue(of({}));
service await firstValueFrom(
.codeFlowCodeRequest({ code: 'foo' } as CallbackContext, config) service.codeFlowCodeRequest({ code: 'foo' } as CallbackContext, config)
.subscribe(() => { );
expect(urlServiceSpy).toHaveBeenCalledOnceWith('foo', config, { expect(urlServiceSpy).toHaveBeenCalledExactlyOnceWith('foo', config, {
foo: 'bar', foo: 'bar',
}); });
expect(postSpy).toHaveBeenCalledTimes(1); expect(postSpy).toHaveBeenCalledTimes(1);
}); });
}));
it('calls dataService with correct headers if all params are good', waitForAsync(() => { it('calls dataService with correct headers if all params are good', async () => {
const postSpy = spyOn(dataService, 'post').and.returnValue(of({})); const postSpy = vi.spyOn(dataService, 'post').mockReturnValue(of({}));
const config = { const config = {
configId: 'configId1', configId: 'configId1',
customParamsCodeRequest: { foo: 'bar' }, customParamsCodeRequest: { foo: 'bar' },
}; };
spyOn(storagePersistenceService, 'read') mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) vi.spyOn(storagePersistenceService, 'read'),
.and.returnValue({ tokenEndpoint: 'tokenEndpoint' }); ['authWellKnownEndPoints', config],
() => ({ tokenEndpoint: 'tokenEndpoint' })
);
spyOn( vi.spyOn(
tokenValidationService, tokenValidationService,
'validateStateFromHashCallback' 'validateStateFromHashCallback'
).and.returnValue(true); ).mockReturnValue(true);
service await firstValueFrom(
.codeFlowCodeRequest({} as CallbackContext, config) service.codeFlowCodeRequest({} as CallbackContext, config)
.subscribe(() => { );
const httpHeaders = postSpy.calls.mostRecent().args[3] as HttpHeaders; const httpHeaders = postSpy.mock.calls.at(-1)?.[3] as HttpHeaders;
expect(httpHeaders.has('Content-Type')).toBeTruthy();
expect(httpHeaders.get('Content-Type')).toBe(
'application/x-www-form-urlencoded'
);
});
expect(httpHeaders.has('Content-Type')).toBeTrue(); it('returns error in case of http error', async () => {
expect(httpHeaders.get('Content-Type')).toBe( vi.spyOn(dataService, 'post').mockReturnValue(
'application/x-www-form-urlencoded' throwError(() => HTTP_ERROR)
); );
});
}));
it('returns error in case of http error', waitForAsync(() => {
spyOn(dataService, 'post').and.returnValue(throwError(() => HTTP_ERROR));
const config = { const config = {
configId: 'configId1', configId: 'configId1',
customParamsCodeRequest: { foo: 'bar' }, customParamsCodeRequest: { foo: 'bar' },
authority: 'authority', authority: 'authority',
}; };
spyOn(storagePersistenceService, 'read') mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) vi.spyOn(storagePersistenceService, 'read'),
.and.returnValue({ tokenEndpoint: 'tokenEndpoint' }); ['authWellKnownEndPoints', config],
() => ({ tokenEndpoint: 'tokenEndpoint' })
);
service.codeFlowCodeRequest({} as CallbackContext, config).subscribe({ try {
error: (err) => { await firstValueFrom(
expect(err).toBeTruthy(); service.codeFlowCodeRequest({} as CallbackContext, config)
}, );
}); } catch (err: any) {
})); expect(err).toBeTruthy();
}
});
it('retries request in case of no connection http error and succeeds', waitForAsync(() => { it('retries request in case of no connection http error and succeeds', async () => {
const postSpy = spyOn(dataService, 'post').and.returnValue( const postSpy = vi.spyOn(dataService, 'post').mockReturnValue(
createRetriableStream( createRetriableStream(
throwError(() => CONNECTION_ERROR), throwError(() => CONNECTION_ERROR),
of({}) of({})
@@ -278,29 +301,30 @@ describe('CodeFlowCallbackHandlerService', () => {
authority: 'authority', authority: 'authority',
}; };
spyOn(storagePersistenceService, 'read') mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) vi.spyOn(storagePersistenceService, 'read'),
.and.returnValue({ tokenEndpoint: 'tokenEndpoint' }); ['authWellKnownEndPoints', config],
() => ({ tokenEndpoint: 'tokenEndpoint' })
);
spyOn( vi.spyOn(
tokenValidationService, tokenValidationService,
'validateStateFromHashCallback' 'validateStateFromHashCallback'
).and.returnValue(true); ).mockReturnValue(true);
service.codeFlowCodeRequest({} as CallbackContext, config).subscribe({ try {
next: (res) => { const res = await firstValueFrom(
expect(res).toBeTruthy(); service.codeFlowCodeRequest({} as CallbackContext, config)
expect(postSpy).toHaveBeenCalledTimes(1); );
}, expect(res).toBeTruthy();
error: (err) => { expect(postSpy).toHaveBeenCalledTimes(1);
// fails if there should be a result } catch (err: any) {
expect(err).toBeFalsy(); expect(err).toBeFalsy();
}, }
}); });
}));
it('retries request in case of no connection http error and fails because of http error afterwards', waitForAsync(() => { it('retries request in case of no connection http error and fails because of http error afterwards', async () => {
const postSpy = spyOn(dataService, 'post').and.returnValue( const postSpy = vi.spyOn(dataService, 'post').mockReturnValue(
createRetriableStream( createRetriableStream(
throwError(() => CONNECTION_ERROR), throwError(() => CONNECTION_ERROR),
throwError(() => HTTP_ERROR) throwError(() => HTTP_ERROR)
@@ -312,25 +336,26 @@ describe('CodeFlowCallbackHandlerService', () => {
authority: 'authority', authority: 'authority',
}; };
spyOn(storagePersistenceService, 'read') mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) vi.spyOn(storagePersistenceService, 'read'),
.and.returnValue({ tokenEndpoint: 'tokenEndpoint' }); ['authWellKnownEndPoints', config],
() => ({ tokenEndpoint: 'tokenEndpoint' })
);
spyOn( vi.spyOn(
tokenValidationService, tokenValidationService,
'validateStateFromHashCallback' 'validateStateFromHashCallback'
).and.returnValue(true); ).mockReturnValue(true);
service.codeFlowCodeRequest({} as CallbackContext, config).subscribe({ try {
next: (res) => { const res = await firstValueFrom(
// fails if there should be a result service.codeFlowCodeRequest({} as CallbackContext, config)
expect(res).toBeFalsy(); );
}, expect(res).toBeFalsy();
error: (err) => { } catch (err: any) {
expect(err).toBeTruthy(); expect(err).toBeTruthy();
expect(postSpy).toHaveBeenCalledTimes(1); expect(postSpy).toHaveBeenCalledTimes(1);
}, }
}); });
}));
}); });
}); });

Some files were not shown because too many files have changed in this diff Show More