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]
branches:
- main
workflow_dispatch:
jobs:
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
name: Built, Lint and Test Library
name: Build, Lint and Test Library
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v2
- name: Setup Node and Install Dependencies
uses: pnpm/action-setup@v4
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
run: npm run lint-lib
run: npm run lint
- name: Testing Frontend
run: npm run test-lib-ci
run: npm run test-ci
- name: Coveralls
uses: coverallsapp/github-action@master
with:
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: 'Report Coverage'
if: (github.event_name == 'pull_request' && github.event.action != 'closed')
uses: davelosert/vitest-coverage-report-action@v2
- name: Building Frontend
run: npm run build-lib-prod
- name: Copying essential additional files
run: npm run copy-files
run: npm run build
- name: Show files
run: ls
- name: Upload Artefact
uses: actions/upload-artifact@v3
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: angular_auth_oidc_client_artefact
path: dist/oidc-client-rx
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
name: oidc_client_rx_artifact
path: dist

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

223
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">
<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>
## Quick Start
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)
<p align="center">
<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>
</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
```sh
pnpm add oidc-client-rx @outposts/injection-js @abraham/reflection
# npm install oidc-client-rx @outposts/injection-js @abraham/reflection
# yarn add oidc-client-rx @outposts/injection-js @abraham/reflection
```
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
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({
const injector = ReflectiveInjector.resolveAndCreate(
provideAuth(
{
config: {
authority: '<your authority address here>',
redirectUrl: window.location.origin,
authority: '<your-authority>',
redirectUrl: `${window.location.origin}/auth/callback`,
postLogoutRedirectUri: window.location.origin,
clientId: '<your clientId>',
clientId: '<your-client-id>',
scope: 'openid profile email offline_access',
responseType: 'code',
silentRenew: true,
useRefreshToken: true,
logLevel: LogLevel.Debug,
...
},
}),
],
// ...
})
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.
```ts
import { Component, OnInit, inject } from '@angular/core';
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,
}),
};
});
```
You can use the built in interceptor to add the accesstokens to your request
```ts
AuthModule.forRoot({
config: {
// ...
secureRoutes: ['https://my-secure-url.com/', 'https://my-second-secure-url.com/'],
},
}),
withDefaultFeatures()
)
) as Injector;
const oidcSecurityService = injector.get(OidcSecurityService);
oidcSecurityService.checkAuth().subscribe((result) => {
console.debug('checkAuth result: ', result);
});
const isAuthenticated$ = oidcSecurityService.isAuthenticated$;
```
```ts
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
],
```
### More Examples
## 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)
- [React + TanStack Router](https://github.com/lonelyhentxi/oidc-client-rx/tree/main/examples/react-tanstack-router)
## License
[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",
"version": "0.1.0-alpha.8",
"homepage": "https://github.com/lonelyhentxi/oidc-client-rx",
"author": "lonelyhentxi",
"description": "ReactiveX enhanced OIDC and OAuth2 protocol support for browser-based JavaScript applications",
@@ -10,48 +11,103 @@
"bugs": {
"url": "https://github.com/lonelyhentxi/oidc-client-rx/issues"
},
"version": "0.1.0",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"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",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"files": ["dist", "licenses", "LICENSE", "README.md"],
"scripts": {
"build": "rslib build",
"dev": "rslib build --watch",
"test": "vitest --code-coverage",
"test-ci": "vitest --watch=false --browsers=ChromeHeadlessNoSandbox --code-coverage",
"pack": "npm run build && npm pack ./dist",
"publish": "npm run build && npm publish ./dist",
"coverage": "vitest run --coverage",
"test": "vitest --coverage",
"test-ci": "vitest --watch=false --coverage",
"prepublishOnly": "npm run build",
"lint": "ultracite lint",
"format": "ultracite format"
"format": "ultracite format",
"cli": "tsx scripts/cli.ts"
},
"dependencies": {
"@ngify/http": "^2.0.4",
"injection-js": "git+https://github.com/mgechev/injection-js.git#81a10e0",
"rxjs": ">=7.4.0"
"@outposts/injection-js": "^2.5.1",
"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": {
"@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",
"@rslib/core": "^0.3.1",
"@types/jasmine": "^4.0.0",
"@types/node": "^22.10.1",
"@vitest/coverage-v8": "^3.0.1",
"rfc4648": "^1.5.0",
"@rslib/core": "^0.5.3",
"@swc/core": "^1.10.12",
"@tanstack/react-router": "^1.112.11",
"@tanstack/solid-router": "^1.112.11",
"@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",
"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": [
"rxjs",
@@ -68,6 +124,19 @@
"certified",
"oauth",
"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';
// TODO
/**
* Read environment variables from file.
* 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({
source: {
tsconfigPath: './tsconfig.lib.json'
tsconfigPath: './tsconfig.lib.json',
},
lib: [
{
format: 'esm',
syntax: 'es2021',
dts: true,
bundle: false,
dts: {
bundle: false,
build: false,
distPath: './dist',
},
source: {
entry: {
index: ['src/**/*.ts', '!**/*.spec.ts', '!src/testing/**/*'],
},
},
},
{
format: 'cjs',
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: {

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 {
HttpHeaders,
provideHttpClient,
withInterceptorsFromDi,
} from '@angular/common/http';
import {
HttpTestingController,
type DefaultHttpTestingController,
HTTP_CLIENT_TEST_CONTROLLER,
provideHttpClientTesting,
} from '@angular/common/http/testing';
import { TestBed, waitForAsync } from '@angular/core/testing';
} from '@/testing/http';
import { HttpHeaders } from '@ngify/http';
import { ReplaySubject, firstValueFrom, share } from 'rxjs';
import { DataService } from './data.service';
import { HttpBaseService } from './http-base.service';
describe('Data Service', () => {
let dataService: DataService;
let httpMock: HttpTestingController;
let httpMock: DefaultHttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
DataService,
HttpBaseService,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
providers: [DataService, HttpBaseService, provideHttpClientTesting()],
});
});
beforeEach(() => {
dataService = TestBed.inject(DataService);
httpMock = TestBed.inject(HttpTestingController);
httpMock = TestBed.inject(HTTP_CLIENT_TEST_CONTROLLER);
});
it('should create', () => {
@@ -37,14 +27,20 @@ describe('Data Service', () => {
});
describe('get', () => {
it('get call sets the accept header', waitForAsync(() => {
it('get call sets the accept header', async () => {
const url = 'testurl';
dataService
.get(url, { configId: 'configId1' })
.subscribe((data: unknown) => {
expect(data).toBe('bodyData');
});
const test$ = dataService.get(url, { configId: 'configId1' }).pipe(
share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: false,
})
);
test$.subscribe();
const req = httpMock.expectOne(url);
expect(req.request.method).toBe('GET');
@@ -52,37 +48,55 @@ describe('Data Service', () => {
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 token = 'token';
dataService
.get(url, { configId: 'configId1' }, token)
.subscribe((data: unknown) => {
expect(data).toBe('bodyData');
});
const test$ = dataService.get(url, { configId: 'configId1' }, token).pipe(
share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: false,
})
);
test$.subscribe();
const req = httpMock.expectOne(url);
expect(req.request.method).toBe('GET');
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');
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';
dataService
.get(url, { configId: 'configId1' })
.subscribe((data: unknown) => {
expect(data).toBe('bodyData');
});
const test$ = dataService.get(url, { configId: 'configId1' }).pipe(
share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: false,
})
);
test$.subscribe();
const req = httpMock.expectOne(url);
expect(req.request.method).toBe('GET');
@@ -91,36 +105,67 @@ describe('Data Service', () => {
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';
dataService
.get(url, { configId: 'configId1', ngswBypass: true })
.subscribe((data: unknown) => {
expect(data).toBe('bodyData');
});
const req = httpMock.expectOne(url + '?ngsw-bypass=');
const test$ = dataService
.get(url, {
configId: 'configId1',
ngswBypass: true,
})
.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.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');
const data = await firstValueFrom(test$);
expect(data).toBe('bodyData');
httpMock.verify();
}));
});
});
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';
dataService
const test$ = dataService
.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);
expect(req.request.method).toBe('POST');
@@ -128,18 +173,30 @@ describe('Data Service', () => {
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';
let headers = new HttpHeaders();
headers = headers.set('X-MyHeader', 'Genesis');
dataService
const test$ = dataService
.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);
expect(req.request.method).toBe('POST');
@@ -148,15 +205,27 @@ describe('Data Service', () => {
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';
dataService
const test$ = dataService
.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);
expect(req.request.method).toBe('POST');
@@ -165,28 +234,46 @@ describe('Data Service', () => {
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';
dataService
const test$ = dataService
.post(
url,
{ some: 'thing' },
{ configId: 'configId1', ngswBypass: true }
)
.subscribe();
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('POST');
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');
await firstValueFrom(test$);
httpMock.verify();
}));
});
});
});

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,15 @@
import { Injectable, inject } from 'injection-js';
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { Injectable, inject } from '@outposts/injection-js';
import { BehaviorSubject, type Observable, throwError } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
import { OpenIdConfiguration } from '../config/openid-configuration';
import { AuthResult } from '../flows/callback-context';
import type { OpenIdConfiguration } from '../config/openid-configuration';
import type { AuthResult } from '../flows/callback-context';
import { LoggerService } from '../logging/logger.service';
import { EventTypes } from '../public-events/event-types';
import { PublicEventsService } from '../public-events/public-events.service';
import { StoragePersistenceService } from '../storage/storage-persistence.service';
import { TokenValidationService } from '../validation/token-validation.service';
import { AuthenticatedResult } from './auth-result';
import { AuthStateResult } from './auth-state';
import type { AuthenticatedResult } from './auth-result';
import type { AuthStateResult } from './auth-state';
const DEFAULT_AUTHRESULT = {
isAuthenticated: false,
@@ -257,9 +257,8 @@ export class AuthStateService {
private decodeURIComponentSafely(token: string): string {
if (token) {
return decodeURIComponent(token);
} else {
return '';
}
return '';
}
private persistAccessTokenExpirationTime(
@@ -293,7 +292,7 @@ export class AuthStateService {
};
}
return this.checkAllConfigsIfTheyAreAuthenticated(allConfigs);
return this.checkallConfigsIfTheyAreAuthenticated(allConfigs);
}
private composeUnAuthenticatedResult(
@@ -310,10 +309,10 @@ export class AuthStateService {
};
}
return this.checkAllConfigsIfTheyAreAuthenticated(allConfigs);
return this.checkallConfigsIfTheyAreAuthenticated(allConfigs);
}
private checkAllConfigsIfTheyAreAuthenticated(
private checkallConfigsIfTheyAreAuthenticated(
allConfigs: OpenIdConfiguration[]
): AuthenticatedResult {
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 {
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 { forkJoin, Observable, of, throwError } from 'rxjs';
import { Injectable, inject } from '@outposts/injection-js';
import { type Observable, forkJoin, of, throwError } from 'rxjs';
import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { AutoLoginService } from '../auto-login/auto-login.service';
import { CallbackService } from '../callback/callback.service';
import { PeriodicallyTokenCheckService } from '../callback/periodically-token-check.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 { SilentRenewService } from '../iframe/silent-renew.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 { EventTypes } from '../public-events/event-types';
import { PublicEventsService } from '../public-events/public-events.service';
@@ -57,7 +57,7 @@ export class CheckAuthService {
const stateParamFromUrl =
this.currentUrlService.getStateParamFromCurrentUrl(url);
return Boolean(stateParamFromUrl)
return stateParamFromUrl
? this.getConfigurationWithUrlState([configuration], stateParamFromUrl)
: 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 {
ActivatedRouteSnapshot,
Router,
RouterStateSnapshot,
} from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
import { mockProvider } from '../../test/auto-mock';
AbstractRouter,
type ActivatedRouteSnapshot,
type RouterStateSnapshot,
} from 'oidc-client-rx';
import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest';
import { AuthStateService } from '../auth-state/auth-state.service';
import { CheckAuthService } from '../auth-state/check-auth.service';
import { ConfigurationService } from '../config/config.service';
import { LoginService } from '../login/login.service';
import { StoragePersistenceService } from '../storage/storage-persistence.service';
import { mockProvider } from '../testing/mock';
import {
AutoLoginPartialRoutesGuard,
autoLoginPartialRoutesGuard,
@@ -19,11 +19,13 @@ import {
} from './auto-login-partial-routes.guard';
import { AutoLoginService } from './auto-login.service';
describe(`AutoLoginPartialRoutesGuard`, () => {
describe('AutoLoginPartialRoutesGuard', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule],
imports: [],
providers: [
AutoLoginPartialRoutesGuard,
mockRouterProvider(),
AutoLoginService,
mockProvider(AuthStateService),
mockProvider(LoginService),
@@ -41,7 +43,7 @@ describe(`AutoLoginPartialRoutesGuard`, () => {
let storagePersistenceService: StoragePersistenceService;
let configurationService: ConfigurationService;
let autoLoginService: AutoLoginService;
let router: Router;
let router: MockRouter;
beforeEach(() => {
authStateService = TestBed.inject(AuthStateService);
@@ -49,15 +51,16 @@ describe(`AutoLoginPartialRoutesGuard`, () => {
storagePersistenceService = TestBed.inject(StoragePersistenceService);
configurationService = TestBed.inject(ConfigurationService);
spyOn(configurationService, 'getOpenIDConfiguration').and.returnValue(
vi.spyOn(configurationService, 'getOpenIDConfiguration').mockReturnValue(
of({ configId: 'configId1' })
);
guard = TestBed.inject(AutoLoginPartialRoutesGuard);
autoLoginService = TestBed.inject(AutoLoginService);
router = TestBed.inject(Router);
router = TestBed.inject(AbstractRouter);
});
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => {
storagePersistenceService.clear({});
});
@@ -67,281 +70,263 @@ describe(`AutoLoginPartialRoutesGuard`, () => {
});
describe('canActivate', () => {
it('should save current route and call `login` if not authenticated already', waitForAsync(() => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
it('should save current route and call `login` if not authenticated already', async () => {
vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false
);
const checkSavedRedirectRouteAndNavigateSpy = spyOn(
const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService,
'checkSavedRedirectRouteAndNavigate'
);
const saveRedirectRouteSpy = spyOn(
const saveRedirectRouteSpy = vi.spyOn(
autoLoginService,
'saveRedirectRoute'
);
const loginSpy = spyOn(loginService, 'login');
const loginSpy = vi.spyOn(loginService, 'login');
guard
.canActivate(
await firstValueFrom(
guard.canActivate(
{} as ActivatedRouteSnapshot,
{ url: 'some-url1' } as RouterStateSnapshot
)
.subscribe(() => {
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith(
);
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' },
'some-url1'
);
expect(loginSpy).toHaveBeenCalledOnceWith({
expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
configId: 'configId1',
});
expect(
checkSavedRedirectRouteAndNavigateSpy
).not.toHaveBeenCalled();
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
});
}));
it('should save current route and call `login` if not authenticated already and add custom params', waitForAsync(() => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
it('should save current route and call `login` if not authenticated already and add custom params', async () => {
vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false
);
const checkSavedRedirectRouteAndNavigateSpy = spyOn(
const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService,
'checkSavedRedirectRouteAndNavigate'
);
const saveRedirectRouteSpy = spyOn(
const saveRedirectRouteSpy = vi.spyOn(
autoLoginService,
'saveRedirectRoute'
);
const loginSpy = spyOn(loginService, 'login');
const loginSpy = vi.spyOn(loginService, 'login');
guard
.canActivate(
await firstValueFrom(
guard.canActivate(
{ data: { custom: 'param' } } as unknown as ActivatedRouteSnapshot,
{ url: 'some-url1' } as RouterStateSnapshot
)
.subscribe(() => {
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith(
);
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' },
'some-url1'
);
expect(loginSpy).toHaveBeenCalledOnceWith(
expect(loginSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' },
{ customParams: { custom: 'param' } }
);
expect(
checkSavedRedirectRouteAndNavigateSpy
).not.toHaveBeenCalled();
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
});
}));
it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', waitForAsync(() => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => {
vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
true
);
const checkSavedRedirectRouteAndNavigateSpy = spyOn(
const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService,
'checkSavedRedirectRouteAndNavigate'
);
const saveRedirectRouteSpy = spyOn(
const saveRedirectRouteSpy = vi.spyOn(
autoLoginService,
'saveRedirectRoute'
);
const loginSpy = spyOn(loginService, 'login');
const loginSpy = vi.spyOn(loginService, 'login');
guard
.canActivate(
await firstValueFrom(
guard.canActivate(
{} as ActivatedRouteSnapshot,
{ url: 'some-url1' } as RouterStateSnapshot
)
.subscribe(() => {
);
expect(saveRedirectRouteSpy).not.toHaveBeenCalled();
expect(loginSpy).not.toHaveBeenCalled();
expect(
checkSavedRedirectRouteAndNavigateSpy
).toHaveBeenCalledOnceWith({ configId: 'configId1' });
).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' });
});
}));
});
describe('canActivateChild', () => {
it('should save current route and call `login` if not authenticated already', waitForAsync(() => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
it('should save current route and call `login` if not authenticated already', async () => {
vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false
);
const checkSavedRedirectRouteAndNavigateSpy = spyOn(
const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService,
'checkSavedRedirectRouteAndNavigate'
);
const saveRedirectRouteSpy = spyOn(
const saveRedirectRouteSpy = vi.spyOn(
autoLoginService,
'saveRedirectRoute'
);
const loginSpy = spyOn(loginService, 'login');
const loginSpy = vi.spyOn(loginService, 'login');
guard
.canActivateChild(
await firstValueFrom(
guard.canActivateChild(
{} as ActivatedRouteSnapshot,
{ url: 'some-url1' } as RouterStateSnapshot
)
.subscribe(() => {
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith(
);
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' },
'some-url1'
);
expect(loginSpy).toHaveBeenCalledOnceWith({
expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
configId: 'configId1',
});
expect(
checkSavedRedirectRouteAndNavigateSpy
).not.toHaveBeenCalled();
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
});
}));
it('should save current route and call `login` if not authenticated already with custom params', waitForAsync(() => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
it('should save current route and call `login` if not authenticated already with custom params', async () => {
vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false
);
const checkSavedRedirectRouteAndNavigateSpy = spyOn(
const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService,
'checkSavedRedirectRouteAndNavigate'
);
const saveRedirectRouteSpy = spyOn(
const saveRedirectRouteSpy = vi.spyOn(
autoLoginService,
'saveRedirectRoute'
);
const loginSpy = spyOn(loginService, 'login');
const loginSpy = vi.spyOn(loginService, 'login');
guard
.canActivateChild(
await firstValueFrom(
guard.canActivateChild(
{ data: { custom: 'param' } } as unknown as ActivatedRouteSnapshot,
{ url: 'some-url1' } as RouterStateSnapshot
)
.subscribe(() => {
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith(
);
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' },
'some-url1'
);
expect(loginSpy).toHaveBeenCalledOnceWith(
expect(loginSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' },
{ customParams: { custom: 'param' } }
);
expect(
checkSavedRedirectRouteAndNavigateSpy
).not.toHaveBeenCalled();
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
});
}));
it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', waitForAsync(() => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => {
vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
true
);
const checkSavedRedirectRouteAndNavigateSpy = spyOn(
const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService,
'checkSavedRedirectRouteAndNavigate'
);
const saveRedirectRouteSpy = spyOn(
const saveRedirectRouteSpy = vi.spyOn(
autoLoginService,
'saveRedirectRoute'
);
const loginSpy = spyOn(loginService, 'login');
const loginSpy = vi.spyOn(loginService, 'login');
guard
.canActivateChild(
await firstValueFrom(
guard.canActivateChild(
{} as ActivatedRouteSnapshot,
{ url: 'some-url1' } as RouterStateSnapshot
)
.subscribe(() => {
);
expect(saveRedirectRouteSpy).not.toHaveBeenCalled();
expect(loginSpy).not.toHaveBeenCalled();
expect(
checkSavedRedirectRouteAndNavigateSpy
).toHaveBeenCalledOnceWith({ configId: 'configId1' });
).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' });
});
}));
});
describe('canLoad', () => {
it('should save current route (empty) and call `login` if not authenticated already', waitForAsync(() => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
it('should save current route (empty) and call `login` if not authenticated already', async () => {
vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false
);
const checkSavedRedirectRouteAndNavigateSpy = spyOn(
const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService,
'checkSavedRedirectRouteAndNavigate'
);
const saveRedirectRouteSpy = spyOn(
const saveRedirectRouteSpy = vi.spyOn(
autoLoginService,
'saveRedirectRoute'
);
const loginSpy = spyOn(loginService, 'login');
const loginSpy = vi.spyOn(loginService, 'login');
guard.canLoad().subscribe(() => {
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith(
await firstValueFrom(guard.canLoad());
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' },
''
);
expect(loginSpy).toHaveBeenCalledOnceWith({ configId: 'configId1' });
expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
configId: 'configId1',
});
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
});
}));
it('should save current route (with router extractedUrl) and call `login` if not authenticated already', waitForAsync(() => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
it('should save current route (with router extractedUrl) and call `login` if not authenticated already', async () => {
vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false
);
const checkSavedRedirectRouteAndNavigateSpy = spyOn(
const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService,
'checkSavedRedirectRouteAndNavigate'
);
const saveRedirectRouteSpy = spyOn(
const saveRedirectRouteSpy = vi.spyOn(
autoLoginService,
'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(
'some-url12/with/some-param?queryParam=true'
),
extras: {},
id: 1,
initialUrl: router.parseUrl(''),
previousNavigation: null,
trigger: 'imperative',
});
guard.canLoad().subscribe(() => {
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith(
await firstValueFrom(guard.canLoad());
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' },
'some-url12/with/some-param?queryParam=true'
);
expect(loginSpy).toHaveBeenCalledOnceWith({ configId: 'configId1' });
expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
configId: 'configId1',
});
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
});
}));
it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', waitForAsync(() => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => {
vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
true
);
const checkSavedRedirectRouteAndNavigateSpy = spyOn(
const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService,
'checkSavedRedirectRouteAndNavigate'
);
const saveRedirectRouteSpy = spyOn(
const saveRedirectRouteSpy = vi.spyOn(
autoLoginService,
'saveRedirectRoute'
);
const loginSpy = spyOn(loginService, 'login');
const loginSpy = vi.spyOn(loginService, 'login');
guard.canLoad().subscribe(() => {
await firstValueFrom(guard.canLoad());
expect(saveRedirectRouteSpy).not.toHaveBeenCalled();
expect(loginSpy).not.toHaveBeenCalled();
expect(
checkSavedRedirectRouteAndNavigateSpy
).toHaveBeenCalledOnceWith({ configId: 'configId1' });
).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' });
});
}));
});
});
@@ -352,7 +337,7 @@ describe(`AutoLoginPartialRoutesGuard`, () => {
let storagePersistenceService: StoragePersistenceService;
let configurationService: ConfigurationService;
let autoLoginService: AutoLoginService;
let router: Router;
let router: MockRouter;
beforeEach(() => {
authStateService = TestBed.inject(AuthStateService);
@@ -360,98 +345,97 @@ describe(`AutoLoginPartialRoutesGuard`, () => {
storagePersistenceService = TestBed.inject(StoragePersistenceService);
configurationService = TestBed.inject(ConfigurationService);
spyOn(configurationService, 'getOpenIDConfiguration').and.returnValue(
of({ configId: 'configId1' })
);
vi.spyOn(
configurationService,
'getOpenIDConfiguration'
).mockReturnValue(of({ configId: 'configId1' }));
autoLoginService = TestBed.inject(AutoLoginService);
router = TestBed.inject(Router);
router = TestBed.inject(AbstractRouter);
});
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => {
storagePersistenceService.clear({});
});
it('should save current route (empty) and call `login` if not authenticated already', waitForAsync(() => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
it('should save current route (empty) and call `login` if not authenticated already', async () => {
vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false
);
const checkSavedRedirectRouteAndNavigateSpy = spyOn(
const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService,
'checkSavedRedirectRouteAndNavigate'
);
const saveRedirectRouteSpy = spyOn(
const saveRedirectRouteSpy = vi.spyOn(
autoLoginService,
'saveRedirectRoute'
);
const loginSpy = spyOn(loginService, 'login');
const loginSpy = vi.spyOn(loginService, 'login');
const guard$ = TestBed.runInInjectionContext(
autoLoginPartialRoutesGuard
);
guard$.subscribe(() => {
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith(
await firstValueFrom(guard$);
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' },
''
);
expect(loginSpy).toHaveBeenCalledOnceWith({ configId: 'configId1' });
expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
configId: 'configId1',
});
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
});
}));
it('should save current route (with router extractedUrl) and call `login` if not authenticated already', waitForAsync(() => {
spyOn(router, 'getCurrentNavigation').and.returnValue({
it('should save current route (with router extractedUrl) and call `login` if not authenticated already', async () => {
vi.spyOn(router, 'getCurrentNavigation').mockReturnValue({
extractedUrl: router.parseUrl(
'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
);
const checkSavedRedirectRouteAndNavigateSpy = spyOn(
const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService,
'checkSavedRedirectRouteAndNavigate'
);
const saveRedirectRouteSpy = spyOn(
const saveRedirectRouteSpy = vi.spyOn(
autoLoginService,
'saveRedirectRoute'
);
const loginSpy = spyOn(loginService, 'login');
const loginSpy = vi.spyOn(loginService, 'login');
const guard$ = TestBed.runInInjectionContext(
autoLoginPartialRoutesGuard
);
guard$.subscribe(() => {
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith(
await firstValueFrom(guard$);
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' },
'some-url12/with/some-param?queryParam=true'
);
expect(loginSpy).toHaveBeenCalledOnceWith({ configId: 'configId1' });
expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
configId: 'configId1',
});
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
});
}));
it('should save current route and call `login` if not authenticated already and add custom params', waitForAsync(() => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
it('should save current route and call `login` if not authenticated already and add custom params', async () => {
vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false
);
const checkSavedRedirectRouteAndNavigateSpy = spyOn(
const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService,
'checkSavedRedirectRouteAndNavigate'
);
const saveRedirectRouteSpy = spyOn(
const saveRedirectRouteSpy = vi.spyOn(
autoLoginService,
'saveRedirectRoute'
);
const loginSpy = spyOn(loginService, 'login');
const loginSpy = vi.spyOn(loginService, 'login');
const guard$ = TestBed.runInInjectionContext(() =>
autoLoginPartialRoutesGuard({
@@ -459,45 +443,43 @@ describe(`AutoLoginPartialRoutesGuard`, () => {
} as unknown as ActivatedRouteSnapshot)
);
guard$.subscribe(() => {
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith(
await firstValueFrom(guard$);
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' },
''
);
expect(loginSpy).toHaveBeenCalledOnceWith(
expect(loginSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' },
{ customParams: { custom: 'param' } }
);
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
});
}));
it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', waitForAsync(() => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => {
vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
true
);
const checkSavedRedirectRouteAndNavigateSpy = spyOn(
const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService,
'checkSavedRedirectRouteAndNavigate'
);
const saveRedirectRouteSpy = spyOn(
const saveRedirectRouteSpy = vi.spyOn(
autoLoginService,
'saveRedirectRoute'
);
const loginSpy = spyOn(loginService, 'login');
const loginSpy = vi.spyOn(loginService, 'login');
const guard$ = TestBed.runInInjectionContext(
autoLoginPartialRoutesGuard
);
guard$.subscribe(() => {
await firstValueFrom(guard$);
expect(saveRedirectRouteSpy).not.toHaveBeenCalled();
expect(loginSpy).not.toHaveBeenCalled();
expect(
checkSavedRedirectRouteAndNavigateSpy
).toHaveBeenCalledOnceWith({ configId: 'configId1' });
).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' });
});
}));
});
describe('autoLoginPartialRoutesGuardWithConfig', () => {
@@ -513,44 +495,47 @@ describe(`AutoLoginPartialRoutesGuard`, () => {
storagePersistenceService = TestBed.inject(StoragePersistenceService);
configurationService = TestBed.inject(ConfigurationService);
spyOn(configurationService, 'getOpenIDConfiguration').and.callFake(
(configId) => of({ configId })
);
vi.spyOn(
configurationService,
'getOpenIDConfiguration'
).mockImplementation((configId) => of({ configId }));
autoLoginService = TestBed.inject(AutoLoginService);
});
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => {
storagePersistenceService.clear({});
});
it('should save current route (empty) and call `login` if not authenticated already', waitForAsync(() => {
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
it('should save current route (empty) and call `login` if not authenticated already', async () => {
vi.spyOn(authStateService, 'areAuthStorageTokensValid').mockReturnValue(
false
);
const checkSavedRedirectRouteAndNavigateSpy = spyOn(
const checkSavedRedirectRouteAndNavigateSpy = vi.spyOn(
autoLoginService,
'checkSavedRedirectRouteAndNavigate'
);
const saveRedirectRouteSpy = spyOn(
const saveRedirectRouteSpy = vi.spyOn(
autoLoginService,
'saveRedirectRoute'
);
const loginSpy = spyOn(loginService, 'login');
const loginSpy = vi.spyOn(loginService, 'login');
const guard$ = TestBed.runInInjectionContext(
autoLoginPartialRoutesGuardWithConfig('configId1')
);
guard$.subscribe(() => {
expect(saveRedirectRouteSpy).toHaveBeenCalledOnceWith(
await firstValueFrom(guard$);
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' },
''
);
expect(loginSpy).toHaveBeenCalledOnceWith({ configId: 'configId1' });
expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
configId: 'configId1',
});
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
});
}));
});
});
});

View File

@@ -1,15 +1,16 @@
import { inject, Injectable } from 'injection-js';
import {
ActivatedRouteSnapshot,
Router,
RouterStateSnapshot,
} from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { AuthOptions } from '../auth-options';
import { Injectable, inject } from '@outposts/injection-js';
import { type Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import type { AuthOptions } from '../auth-options';
import { AuthStateService } from '../auth-state/auth-state.service';
import { ConfigurationService } from '../config/config.service';
import { injectAbstractType } from '../injection';
import { LoginService } from '../login/login.service';
import {
AbstractRouter,
type ActivatedRouteSnapshot,
type RouterStateSnapshot,
} from '../router';
import { AutoLoginService } from './auto-login.service';
@Injectable()
@@ -22,7 +23,7 @@ export class AutoLoginPartialRoutesGuard {
private readonly configurationService = inject(ConfigurationService);
private readonly router = inject(Router);
private readonly router = injectAbstractType(AbstractRouter);
canLoad(): Observable<boolean> {
const url =
@@ -79,14 +80,14 @@ export class AutoLoginPartialRoutesGuard {
export function autoLoginPartialRoutesGuard(
route?: ActivatedRouteSnapshot,
state?: RouterStateSnapshot,
_state?: RouterStateSnapshot,
configId?: string
): Observable<boolean> {
const configurationService = inject(ConfigurationService);
const authStateService = inject(AuthStateService);
const loginService = inject(LoginService);
const autoLoginService = inject(AutoLoginService);
const router = inject(Router);
const router = injectAbstractType(AbstractRouter);
const authOptions: AuthOptions | undefined = route?.data
? { customParams: route.data }
: undefined;
@@ -125,7 +126,7 @@ function checkAuth(
configId?: string
): Observable<boolean> {
return configurationService.getOpenIDConfiguration(configId).pipe(
map((configuration) => {
switchMap((configuration) => {
const isAuthenticated =
authStateService.areAuthStorageTokensValid(configuration);
@@ -136,13 +137,16 @@ function checkAuth(
if (!isAuthenticated) {
autoLoginService.saveRedirectRoute(configuration, url);
if (authOptions) {
loginService.login(configuration, authOptions);
} else {
loginService.login(configuration);
return loginService
.login(configuration, authOptions)
.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 { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { mockProvider } from '../../test/auto-mock';
import { TestBed, mockRouterProvider } from '@/testing';
import { AbstractRouter } from 'oidc-client-rx/router';
import { vi } from 'vitest';
import { StoragePersistenceService } from '../storage/storage-persistence.service';
import { mockProvider } from '../testing/mock';
import { AutoLoginService } from './auto-login.service';
describe('AutoLoginService ', () => {
let autoLoginService: AutoLoginService;
let storagePersistenceService: StoragePersistenceService;
let router: Router;
let router: AbstractRouter;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule],
providers: [AutoLoginService, mockProvider(StoragePersistenceService)],
imports: [],
providers: [
mockRouterProvider(),
AutoLoginService,
mockProvider(StoragePersistenceService),
],
});
});
beforeEach(() => {
router = TestBed.inject(Router);
router = TestBed.inject(AbstractRouter);
autoLoginService = TestBed.inject(AutoLoginService);
storagePersistenceService = TestBed.inject(StoragePersistenceService);
});
@@ -29,11 +30,11 @@ describe('AutoLoginService ', () => {
describe('checkSavedRedirectRouteAndNavigate', () => {
it('if not route is saved, router and delete are not called', () => {
const deleteSpy = spyOn(storagePersistenceService, 'remove');
const routerSpy = spyOn(router, 'navigateByUrl');
const readSpy = spyOn(storagePersistenceService, 'read').and.returnValue(
null
);
const deleteSpy = vi.spyOn(storagePersistenceService, 'remove');
const routerSpy = vi.spyOn(router, 'navigateByUrl');
const readSpy = vi
.spyOn(storagePersistenceService, 'read')
.mockReturnValue(null);
autoLoginService.checkSavedRedirectRouteAndNavigate({
configId: 'configId1',
@@ -41,27 +42,27 @@ describe('AutoLoginService ', () => {
expect(deleteSpy).not.toHaveBeenCalled();
expect(routerSpy).not.toHaveBeenCalled();
expect(readSpy).toHaveBeenCalledOnceWith('redirect', {
expect(readSpy).toHaveBeenCalledExactlyOnceWith('redirect', {
configId: 'configId1',
});
});
it('if route is saved, router and delete are called', () => {
const deleteSpy = spyOn(storagePersistenceService, 'remove');
const routerSpy = spyOn(router, 'navigateByUrl');
const readSpy = spyOn(storagePersistenceService, 'read').and.returnValue(
'saved-route'
);
const deleteSpy = vi.spyOn(storagePersistenceService, 'remove');
const routerSpy = vi.spyOn(router, 'navigateByUrl');
const readSpy = vi
.spyOn(storagePersistenceService, 'read')
.mockReturnValue('saved-route');
autoLoginService.checkSavedRedirectRouteAndNavigate({
configId: 'configId1',
});
expect(deleteSpy).toHaveBeenCalledOnceWith('redirect', {
expect(deleteSpy).toHaveBeenCalledExactlyOnceWith('redirect', {
configId: 'configId1',
});
expect(routerSpy).toHaveBeenCalledOnceWith('saved-route');
expect(readSpy).toHaveBeenCalledOnceWith('redirect', {
expect(routerSpy).toHaveBeenCalledExactlyOnceWith('saved-route');
expect(readSpy).toHaveBeenCalledExactlyOnceWith('redirect', {
configId: 'configId1',
});
});
@@ -69,16 +70,20 @@ describe('AutoLoginService ', () => {
describe('saveRedirectRoute', () => {
it('calls storageService with correct params', () => {
const writeSpy = spyOn(storagePersistenceService, 'write');
const writeSpy = vi.spyOn(storagePersistenceService, 'write');
autoLoginService.saveRedirectRoute(
{ configId: 'configId1' },
'some-route'
);
expect(writeSpy).toHaveBeenCalledOnceWith('redirect', 'some-route', {
expect(writeSpy).toHaveBeenCalledExactlyOnceWith(
'redirect',
'some-route',
{
configId: 'configId1',
});
}
);
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,19 @@
import { inject, Injectable } from 'injection-js';
import { Router } from '@angular/router';
import { Observable, throwError } from 'rxjs';
import { Injectable, inject } from '@outposts/injection-js';
import { type Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { OpenIdConfiguration } from '../config/openid-configuration';
import { CallbackContext } from '../flows/callback-context';
import type { OpenIdConfiguration } from '../config/openid-configuration';
import type { CallbackContext } from '../flows/callback-context';
import { FlowsDataService } from '../flows/flows-data.service';
import { FlowsService } from '../flows/flows.service';
import { injectAbstractType } from '../injection';
import { AbstractRouter } from '../router';
import { IntervalService } from './interval.service';
@Injectable()
export class ImplicitFlowCallbackService {
private readonly flowsService = inject(FlowsService);
private readonly router = inject(Router);
private readonly router = injectAbstractType(AbstractRouter);
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 { vi } from 'vitest';
import { IntervalService } from './interval.service';
describe('IntervalService', () => {
let intervalService: IntervalService;
beforeEach(() => {
vi.useFakeTimers();
TestBed.configureTestingModule({
providers: [
IntervalService,
{
provide: Document,
useValue: {
@@ -18,10 +21,12 @@ describe('IntervalService', () => {
},
],
});
intervalService = TestBed.inject(IntervalService);
});
beforeEach(() => {
intervalService = TestBed.inject(IntervalService);
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => {
vi.useRealTimers();
});
it('should create', () => {
@@ -31,7 +36,7 @@ describe('IntervalService', () => {
describe('stopPeriodicTokenCheck', () => {
it('calls unsubscribe and sets to null', () => {
intervalService.runTokenValidationRunning = new Subscription();
const spy = spyOn(
const spy = vi.spyOn(
intervalService.runTokenValidationRunning,
'unsubscribe'
);
@@ -44,7 +49,7 @@ describe('IntervalService', () => {
it('does nothing if `runTokenValidationRunning` is null', () => {
intervalService.runTokenValidationRunning = new Subscription();
const spy = spyOn(
const spy = vi.spyOn(
intervalService.runTokenValidationRunning,
'unsubscribe'
);
@@ -57,20 +62,20 @@ describe('IntervalService', () => {
});
describe('startPeriodicTokenCheck', () => {
it('starts check after correct milliseconds', fakeAsync(() => {
it('starts check after correct milliseconds', async () => {
const periodicCheck = intervalService.startPeriodicTokenCheck(0.5);
const spy = jasmine.createSpy();
const spy = vi.fn();
const sub = periodicCheck.subscribe(() => {
spy();
});
tick(500);
await vi.advanceTimersByTimeAsync(500);
expect(spy).toHaveBeenCalledTimes(1);
tick(500);
await vi.advanceTimersByTimeAsync(500);
expect(spy).toHaveBeenCalledTimes(2);
sub.unsubscribe();
}));
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
import { inject, Injectable } from 'injection-js';
import { Observable, throwError } from 'rxjs';
import { Injectable, inject } from '@outposts/injection-js';
import { type Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { EventTypes } from '../../public-events/event-types';
import { PublicEventsService } from '../../public-events/public-events.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 { AuthWellKnownEndpoints } from './auth-well-known-endpoints';
import type { AuthWellKnownEndpoints } from './auth-well-known-endpoints';
@Injectable()
export class AuthWellKnownService {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { LogLevel } from '../logging/log-level';
import { AuthWellKnownEndpoints } from './auth-well-known/auth-well-known-endpoints';
import type { LogLevel } from '../logging/log-level';
import type { AuthWellKnownEndpoints } from './auth-well-known/auth-well-known-endpoints';
export interface OpenIdConfiguration {
/**
@@ -40,6 +40,12 @@ export interface OpenIdConfiguration {
* or if it contains additional audiences not trusted by the Client.
*/
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`.
* Name of the flow which can be configured.
@@ -207,5 +213,5 @@ export interface OpenIdConfiguration {
/**
* 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 { mockProvider } from '../../../test/auto-mock';
import { TestBed } from '@/testing';
import { mockImplementationWhenArgs, spyOnWithOrigin } from '@/testing/spy';
import { vi } from 'vitest';
import { LogLevel } from '../../logging/log-level';
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 { allMultipleConfigRules } from './rules';
@@ -14,6 +16,8 @@ describe('Config Validation Service', () => {
TestBed.configureTestingModule({
providers: [ConfigValidationService, mockProvider(LoggerService)],
});
configValidationService = TestBed.inject(ConfigValidationService);
loggerService = TestBed.inject(LoggerService);
});
const VALID_CONFIG = {
@@ -29,11 +33,6 @@ describe('Config Validation Service', () => {
logLevel: LogLevel.Debug,
};
beforeEach(() => {
configValidationService = TestBed.inject(ConfigValidationService);
loggerService = TestBed.inject(LoggerService);
});
it('should create', () => {
expect(configValidationService).toBeTruthy();
});
@@ -42,26 +41,27 @@ describe('Config Validation Service', () => {
const config = {};
const result = configValidationService.validateConfig(config);
expect(result).toBeFalse();
expect(result).toBeFalsy();
});
it('should return true for valid config', () => {
const result = configValidationService.validateConfig(VALID_CONFIG);
expect(result).toBeTrue();
expect(result).toBeTruthy();
});
it('calls `logWarning` if one rule has warning level', () => {
const loggerWarningSpy = spyOn(loggerService, 'logWarning');
const messageTypeSpy = spyOn(
configValidationService as any,
'getAllMessagesOfType'
const loggerWarningSpy = vi.spyOn(loggerService, 'logWarning');
const messageTypeSpy = spyOnWithOrigin(
configValidationService,
'getAllMessagesOfType' as any
);
messageTypeSpy
.withArgs('warning', jasmine.any(Array))
.and.returnValue(['A warning message']);
messageTypeSpy.withArgs('error', jasmine.any(Array)).and.callThrough();
mockImplementationWhenArgs(
messageTypeSpy,
(arg1: any, arg2: any) => arg1 === 'warning' && Array.isArray(arg2),
() => ['A warning message']
);
configValidationService.validateConfig(VALID_CONFIG);
expect(loggerWarningSpy).toHaveBeenCalled();
@@ -72,7 +72,7 @@ describe('Config Validation Service', () => {
const config = { ...VALID_CONFIG, clientId: '' } as OpenIdConfiguration;
const result = configValidationService.validateConfig(config);
expect(result).toBeFalse();
expect(result).toBeFalsy();
});
});
@@ -84,7 +84,7 @@ describe('Config Validation Service', () => {
} as OpenIdConfiguration;
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 result = configValidationService.validateConfig(config);
expect(result).toBeFalse();
expect(result).toBeFalsy();
});
});
@@ -107,7 +107,7 @@ describe('Config Validation Service', () => {
} as OpenIdConfiguration;
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',
};
const loggerSpy = spyOn(loggerService, 'logError');
const loggerWarningSpy = spyOn(loggerService, 'logWarning');
const loggerSpy = vi.spyOn(loggerService, 'logError');
const loggerWarningSpy = vi.spyOn(loggerService, 'logWarning');
const result = configValidationService.validateConfig(config);
expect(result).toBeTrue();
expect(result).toBeTruthy();
expect(loggerSpy).not.toHaveBeenCalled();
expect(loggerWarningSpy).toHaveBeenCalled();
});
@@ -146,47 +146,47 @@ describe('Config Validation Service', () => {
scopes: 'scope1 scope2 but_no_offline_access',
};
const loggerErrorSpy = spyOn(loggerService, 'logError');
const loggerWarningSpy = spyOn(loggerService, 'logWarning');
const loggerErrorSpy = vi.spyOn(loggerService, 'logError');
const loggerWarningSpy = vi.spyOn(loggerService, 'logWarning');
const result = configValidationService.validateConfigs([
config1,
config2,
]);
expect(result).toBeTrue();
expect(result).toBeTruthy();
expect(loggerErrorSpy).not.toHaveBeenCalled();
expect(loggerWarningSpy.calls.argsFor(0)).toEqual([
expect(vi.mocked(loggerWarningSpy).mock.calls[0]).toEqual([
config1,
'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,
'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', () => {
const loggerWarningSpy = spyOn(loggerService, 'logWarning');
const loggerWarningSpy = vi.spyOn(loggerService, 'logWarning');
const result = configValidationService.validateConfigs([]);
expect(result).toBeFalse();
expect(result).toBeFalsy();
expect(loggerWarningSpy).not.toHaveBeenCalled();
});
});
describe('validateConfigs', () => {
it('calls internal method with empty array if something falsy is passed', () => {
const spy = spyOn(
const spy = vi.spyOn(
configValidationService as any,
'validateConfigsInternal'
).and.callThrough();
);
const result = configValidationService.validateConfigs([]);
expect(result).toBeFalse();
expect(spy).toHaveBeenCalledOnceWith([], allMultipleConfigRules);
expect(result).toBeFalsy();
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 { OpenIdConfiguration } from '../openid-configuration';
import { Level, RuleValidationResult } from './rule';
import type { OpenIdConfiguration } from '../openid-configuration';
import type { Level, RuleValidationResult } from './rule';
import { allMultipleConfigRules, allRules } from './rules';
@Injectable()
@@ -35,14 +35,14 @@ export class ConfigValidationService {
let overallErrorCount = 0;
passedConfigs.forEach((passedConfig) => {
for (const passedConfig of passedConfigs) {
const errorCount = this.processValidationResultsAndGetErrorCount(
allValidationResults,
passedConfig
);
overallErrorCount += errorCount;
});
}
return overallErrorCount === 0;
}
@@ -75,17 +75,17 @@ export class ConfigValidationService {
const allErrorMessages = this.getAllMessagesOfType('error', allMessages);
const allWarnings = this.getAllMessagesOfType('warning', allMessages);
allErrorMessages.forEach((message) =>
this.loggerService.logError(config, message)
);
allWarnings.forEach((message) =>
this.loggerService.logWarning(config, message)
);
for (const message of allErrorMessages) {
this.loggerService.logError(config, message);
}
for (const message of allWarnings) {
this.loggerService.logWarning(config, message);
}
return allErrorMessages.length;
}
private getAllMessagesOfType(
protected getAllMessagesOfType(
type: Level,
results: RuleValidationResult[]
): string[] {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { OpenIdConfiguration } from '../../openid-configuration';
import { POSITIVE_VALIDATION_RESULT, RuleValidationResult } from '../rule';
import type { OpenIdConfiguration } from '../../openid-configuration';
import { POSITIVE_VALIDATION_RESULT, type RuleValidationResult } from '../rule';
export const useOfflineScopeWithSilentRenew = (
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');

View File

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

View File

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

View File

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

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