Compare commits

..

No commits in common. "f00c1d1aef1c1d271069fbb06a6375290e0253ad" and "da0d9855da4490c673d2336e6eed2ca004c9b8ae" have entirely different histories.

198 changed files with 8970 additions and 16798 deletions

View File

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 134 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

2
.gitignore vendored
View File

@ -32,6 +32,7 @@ speed-measure-plugin*.json
.history/* .history/*
# misc # misc
/.angular/cache
/.sass-cache /.sass-cache
/connect.lock /connect.lock
/coverage /coverage
@ -54,4 +55,3 @@ debug.log
/playwright-report/ /playwright-report/
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
/.vitest

210
README.md
View File

@ -1,17 +1,207 @@
<h1 align="center"> # Angular Lib for OpenID Connect & OAuth2
<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/badge/status-work--in--progress-blue" alt="status-badge" />
</div>
</h1>
<p align="center">ReactiveX enhanced OIDC and OAuth2 protocol support for browser-based JavaScript applications.</p> ![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)
## Quick Start <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>
@TODO Coming Soon Secure your Angular app using the latest standards for OpenID Connect & OAuth2. Provides support for token refresh, all modern OIDC Identity Providers and more.
## Acknowledgements
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
```
And answer the questions. A module will be created which encapsulates your configuration.
![oidc-client-rx schematics](https://raw.githubusercontent.com/damienbod/oidc-client-rx/main/.github/oidc-client-rx-schematics-720.gif)
### 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({
config: {
authority: '<your authority address here>',
redirectUrl: window.location.origin,
postLogoutRedirectUri: window.location.origin,
clientId: '<your clientId>',
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/'],
},
}),
```
```ts
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
],
```
## Versions
Current Version is Version 19.x
- [Info about Version 18](https://github.com/damienbod/oidc-client-rx/tree/version-18)
- [Info about Version 17](https://github.com/damienbod/oidc-client-rx/tree/version-17)
- [Info about Version 16](https://github.com/damienbod/oidc-client-rx/tree/version-16)
- [Info about Version 15](https://github.com/damienbod/oidc-client-rx/tree/version-15)
- [Info about Version 14](https://github.com/damienbod/oidc-client-rx/tree/version-14)
- [Info about Version 13](https://github.com/damienbod/oidc-client-rx/tree/version-13)
- [Info about Version 12](https://github.com/damienbod/oidc-client-rx/tree/version-12)
- [Info about Version 11](https://github.com/damienbod/oidc-client-rx/tree/version-11)
- [Info about Version 10](https://github.com/damienbod/oidc-client-rx/tree/version-10)
## License ## License
[MIT](https://choosealicense.com/licenses/mit/) [MIT](https://choosealicense.com/licenses/mit/)
## Authors
- [@DamienBod](https://www.github.com/damienbod)
- [@FabianGosebrink](https://www.github.com/FabianGosebrink)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

25
biome.json Normal file
View File

@ -0,0 +1,25 @@
{
"$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"
]
}
}

View File

@ -1,41 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"extends": ["ultracite"],
"linter": {
"rules": {
"style": {
"noNonNullAssertion": "off",
"noParameterAssign": "off",
"useFilenamingConvention": "warn",
"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"]
}
}
]
}

50
karma.conf.js Normal file
View File

@ -0,0 +1,50 @@
// 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,21 +0,0 @@
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.

View File

@ -1,21 +0,0 @@
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.

3006
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,54 +22,36 @@
"main": "./dist/index.cjs", "main": "./dist/index.cjs",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"files": ["dist"], "files": [
"dist"
],
"scripts": { "scripts": {
"build": "rslib build", "build": "rslib build",
"dev": "rslib build --watch", "dev": "rslib build --watch",
"test": "vitest --coverage", "test": "vitest --code-coverage",
"test-ci": "vitest --watch=false --coverage", "test-ci": "vitest --watch=false --browsers=ChromeHeadlessNoSandbox --code-coverage",
"pack": "npm run build && npm pack ./dist", "pack": "npm run build && npm pack ./dist",
"publish": "npm run build && npm publish ./dist", "publish": "npm run build && npm publish ./dist",
"coverage": "vitest run --coverage", "coverage": "vitest run --coverage",
"lint": "ultracite lint", "lint": "ultracite lint",
"format": "ultracite format", "format": "ultracite format"
"cli": "tsx scripts/cli.ts"
}, },
"dependencies": { "dependencies": {
"@ngify/http": "^2.0.4", "@ngify/http": "^2.0.4",
"injection-js": "git+https://github.com/mgechev/injection-js.git#81a10e0", "injection-js": "git+https://github.com/mgechev/injection-js.git#81a10e0",
"reflect-metadata": "^0.2.2" "rxjs": ">=7.4.0"
},
"peerDependencies": {
"rxjs": "^7.4.0||>=8.0.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4",
"@biomejs/js-api": "0.7.1",
"@biomejs/wasm-nodejs": "^1.9.4",
"@evilmartians/lefthook": "^1.0.3", "@evilmartians/lefthook": "^1.0.3",
"@playwright/test": "^1.49.1", "@playwright/test": "^1.49.1",
"@rslib/core": "^0.3.1", "@rslib/core": "^0.3.1",
"@swc/core": "^1.10.12", "@types/jasmine": "^4.0.0",
"@types/jsdom": "^21.1.7", "@types/node": "^22.10.1",
"@types/lodash-es": "^4.17.12", "@vitest/coverage-v8": "^3.0.1",
"@types/node": "^22.12.0",
"@vitest/browser": "^3.0.4",
"@vitest/coverage-v8": "^3.0.4",
"commander": "^13.1.0",
"jsdom": "^26.0.0",
"lodash-es": "^4.17.21",
"oxc-parser": "^0.48.1",
"oxc-walker": "^0.2.2",
"playwright": "^1.50.0",
"rfc4648": "^1.5.0", "rfc4648": "^1.5.0",
"rxjs": "^7.4.0",
"tsx": "^4.19.2",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"ultracite": "^4.1.15", "ultracite": "^4.1.15",
"unplugin-swc": "^1.5.1", "vitest": "^3.0.1"
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.4"
}, },
"keywords": [ "keywords": [
"rxjs", "rxjs",

View File

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

3366
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +0,0 @@
#!/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

@ -1,82 +0,0 @@
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
);
});
});

View File

@ -1,173 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,5 +1,6 @@
import { TestBed } from '@/testing'; import { TestBed, waitForAsync } from '@angular/core/testing';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { mockProvider } from '../test/auto-mock';
import { PASSED_CONFIG } from './auth-config'; import { PASSED_CONFIG } from './auth-config';
import { AuthModule } from './auth.module'; import { AuthModule } from './auth.module';
import { ConfigurationService } from './config/config.service'; import { ConfigurationService } from './config/config.service';
@ -8,42 +9,37 @@ import {
StsConfigLoader, StsConfigLoader,
StsConfigStaticLoader, StsConfigStaticLoader,
} from './config/loader/config-loader'; } from './config/loader/config-loader';
import { mockProvider } from './testing/mock';
describe('AuthModule', () => { describe('AuthModule', () => {
describe('APP_CONFIG', () => { describe('APP_CONFIG', () => {
let authModule: AuthModule; beforeEach(waitForAsync(() => {
beforeEach(async () => { TestBed.configureTestingModule({
await TestBed.configureTestingModule({
imports: [AuthModule.forRoot({ config: { authority: 'something' } })], imports: [AuthModule.forRoot({ config: { authority: 'something' } })],
providers: [mockProvider(ConfigurationService)], providers: [mockProvider(ConfigurationService)],
}).compileComponents(); }).compileComponents();
authModule = TestBed.getImportByType(AuthModule); }));
});
it('should create', () => { it('should create', () => {
expect(AuthModule).toBeDefined(); expect(AuthModule).toBeDefined();
expect(authModule).toBeDefined(); expect(AuthModule.forRoot({})).toBeDefined();
}); });
it('should provide config', () => { it('should provide config', () => {
const config = authModule.get(PASSED_CONFIG); const config = TestBed.inject(PASSED_CONFIG);
expect(config).toEqual({ config: { authority: 'something' } }); expect(config).toEqual({ config: { authority: 'something' } });
}); });
it('should create StsConfigStaticLoader if config is passed', () => { it('should create StsConfigStaticLoader if config is passed', () => {
const configLoader = authModule.get(StsConfigLoader); const configLoader = TestBed.inject(StsConfigLoader);
expect(configLoader instanceof StsConfigStaticLoader).toBe(true); expect(configLoader instanceof StsConfigStaticLoader).toBe(true);
}); });
}); });
describe('StsConfigHttpLoader', () => { describe('StsConfigHttpLoader', () => {
let authModule: AuthModule; beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ imports: [
AuthModule.forRoot({ AuthModule.forRoot({
loader: { loader: {
@ -54,11 +50,10 @@ describe('AuthModule', () => {
], ],
providers: [mockProvider(ConfigurationService)], providers: [mockProvider(ConfigurationService)],
}).compileComponents(); }).compileComponents();
authModule = TestBed.getImportByType(AuthModule); }));
});
it('should create StsConfigStaticLoader if config is passed', () => { it('should create StsConfigStaticLoader if config is passed', () => {
const configLoader = authModule.get(StsConfigLoader); const configLoader = TestBed.inject(StsConfigLoader);
expect(configLoader instanceof StsConfigHttpLoader).toBe(true); expect(configLoader instanceof StsConfigHttpLoader).toBe(true);
}); });

View File

@ -1,41 +1,25 @@
import { CommonModule } from '@angular/common';
import { import {
type InjectionToken, provideHttpClient,
Injector, withInterceptorsFromDi,
ReflectiveInjector, } from '@ngify/http';
type Type, import { ModuleWithProviders, NgModule } from 'injection-js';
} from 'injection-js'; import { PassedInitialConfig } from './auth-config';
import type { PassedInitialConfig } from './auth-config';
import type { Module } from './injection';
import { _provideAuth } from './provide-auth'; import { _provideAuth } from './provide-auth';
export interface AuthModuleOptions { @NgModule({
passedConfig: PassedInitialConfig; declarations: [],
parentInjector?: ReflectiveInjector; exports: [],
} imports: [CommonModule],
providers: [provideHttpClient(withInterceptorsFromDi())],
export class AuthModule extends Injector { })
passedConfig: PassedInitialConfig; export class AuthModule {
injector: ReflectiveInjector; static forRoot(
parentInjector?: Injector; passedConfig: PassedInitialConfig
): ModuleWithProviders<AuthModule> {
constructor(passedConfig?: PassedInitialConfig, parentInjector?: Injector) { return {
super(); ngModule: AuthModule,
this.passedConfig = passedConfig ?? {}; providers: [..._provideAuth(passedConfig)],
this.parentInjector = parentInjector; };
this.injector = ReflectiveInjector.resolveAndCreate(
[..._provideAuth(this.passedConfig)],
this.parentInjector
);
}
static forRoot(passedConfig?: PassedInitialConfig): Module {
return (parentInjector?: Injector) =>
new AuthModule(passedConfig, parentInjector);
}
get<T>(token: Type<T> | InjectionToken<T>, notFoundValue?: T): T;
get(token: any, notFoundValue?: any);
get(token: unknown, notFoundValue?: unknown): any {
return this.injector.get(token, notFoundValue);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,11 @@
import { Injectable, inject } from 'injection-js'; import { Injectable, NgZone, inject } from 'injection-js';
import { type Observable, type Subscription, interval } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { DOCUMENT } from '../dom'; import { DOCUMENT } from '../../dom';
@Injectable() @Injectable()
export class IntervalService { export class IntervalService {
private readonly zone = inject(NgZone);
private readonly document = inject(DOCUMENT); private readonly document = inject(DOCUMENT);
runTokenValidationRunning: Subscription | null = null; runTokenValidationRunning: Subscription | null = null;
@ -22,6 +24,19 @@ export class IntervalService {
startPeriodicTokenCheck(repeatAfterSeconds: number): Observable<unknown> { startPeriodicTokenCheck(repeatAfterSeconds: number): Observable<unknown> {
const millisecondsDelayBetweenTokenCheck = repeatAfterSeconds * 1000; const millisecondsDelayBetweenTokenCheck = repeatAfterSeconds * 1000;
return interval(millisecondsDelayBetweenTokenCheck); 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);
};
});
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import type { LogLevel } from '../logging/log-level'; import { LogLevel } from '../logging/log-level';
import type { AuthWellKnownEndpoints } from './auth-well-known/auth-well-known-endpoints'; import { AuthWellKnownEndpoints } from './auth-well-known/auth-well-known-endpoints';
export interface OpenIdConfiguration { export interface OpenIdConfiguration {
/** /**
@ -207,5 +207,5 @@ export interface OpenIdConfiguration {
/** /**
* Disable cleaning up the popup when receiving invalid messages * Disable cleaning up the popup when receiving invalid messages
*/ */
disableCleaningPopupOnInvalidMessage?: boolean; disableCleaningPopupOnInvalidMessage?: boolean
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,14 @@
import { HttpHeaders } from '@ngify/http'; import { HttpHeaders } from '@ngify/http';
import { Injectable, inject } from 'injection-js'; import { inject, Injectable } from 'injection-js';
import { type Observable, of, throwError, timer } from 'rxjs'; import { Observable, of, throwError, timer } from 'rxjs';
import { catchError, mergeMap, retryWhen, switchMap } from 'rxjs/operators'; import { catchError, mergeMap, retryWhen, switchMap } from 'rxjs/operators';
import { DataService } from '../../api/data.service'; import { DataService } from '../../api/data.service';
import type { OpenIdConfiguration } from '../../config/openid-configuration'; import { OpenIdConfiguration } from '../../config/openid-configuration';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { StoragePersistenceService } from '../../storage/storage-persistence.service'; import { StoragePersistenceService } from '../../storage/storage-persistence.service';
import { UrlService } from '../../utils/url/url.service'; import { UrlService } from '../../utils/url/url.service';
import { TokenValidationService } from '../../validation/token-validation.service'; import { TokenValidationService } from '../../validation/token-validation.service';
import type { AuthResult, CallbackContext } from '../callback-context'; import { AuthResult, CallbackContext } from '../callback-context';
import { FlowsDataService } from '../flows-data.service'; import { FlowsDataService } from '../flows-data.service';
import { isNetworkError } from './error-helper'; import { isNetworkError } from './error-helper';
@ -116,7 +116,7 @@ export class CodeFlowCallbackHandlerService {
switchMap((response) => { switchMap((response) => {
if (response) { if (response) {
const authResult: AuthResult = { const authResult: AuthResult = {
...(response as any), ...response,
state: callbackContext.state, state: callbackContext.state,
session_state: callbackContext.sessionState, session_state: callbackContext.sessionState,
}; };

View File

@ -1,4 +1,4 @@
import { HttpErrorResponse } from '@ngify/http'; import { HttpErrorResponse } from '@angular/common/http';
import { isNetworkError } from './error-helper'; import { isNetworkError } from './error-helper';
describe('error helper', () => { describe('error helper', () => {
@ -27,31 +27,31 @@ describe('error helper', () => {
}); });
it('returns true on http error with status = 0', () => { it('returns true on http error with status = 0', () => {
expect(isNetworkError(CONNECTION_ERROR)).toBeTruthy(); expect(isNetworkError(CONNECTION_ERROR)).toBeTrue();
}); });
it('returns true on http error with status = 0 and unknown error', () => { it('returns true on http error with status = 0 and unknown error', () => {
expect(isNetworkError(UNKNOWN_CONNECTION_ERROR)).toBeTruthy(); expect(isNetworkError(UNKNOWN_CONNECTION_ERROR)).toBeTrue();
}); });
it('returns true on http error with status <> 0 and error ProgressEvent', () => { it('returns true on http error with status <> 0 and error ProgressEvent', () => {
expect(isNetworkError(PARTIAL_CONNECTION_ERROR)).toBeTruthy(); expect(isNetworkError(PARTIAL_CONNECTION_ERROR)).toBeTrue();
}); });
it('returns false on non http error', () => { it('returns false on non http error', () => {
expect(isNetworkError(new Error('not a HttpErrorResponse'))).toBeFalsy(); expect(isNetworkError(new Error('not a HttpErrorResponse'))).toBeFalse();
}); });
it('returns false on string error', () => { it('returns false on string error', () => {
expect(isNetworkError('not a HttpErrorResponse')).toBeFalsy(); expect(isNetworkError('not a HttpErrorResponse')).toBeFalse();
}); });
it('returns false on undefined', () => { it('returns false on undefined', () => {
expect(isNetworkError(undefined)).toBeFalsy(); expect(isNetworkError(undefined)).toBeFalse();
}); });
it('returns false on empty http error', () => { it('returns false on empty http error', () => {
expect(isNetworkError(HTTP_ERROR)).toBeFalsy(); expect(isNetworkError(HTTP_ERROR)).toBeFalse();
}); });
}); });
}); });

View File

@ -1,13 +1,12 @@
import { TestBed } from '@/testing'; import { TestBed, waitForAsync } from '@angular/core/testing';
import { firstValueFrom, of, throwError } from 'rxjs'; import { of, throwError } from 'rxjs';
import { vi } from 'vitest'; import { mockProvider } from '../../../test/auto-mock';
import { AuthStateService } from '../../auth-state/auth-state.service'; import { AuthStateService } from '../../auth-state/auth-state.service';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { StoragePersistenceService } from '../../storage/storage-persistence.service'; import { StoragePersistenceService } from '../../storage/storage-persistence.service';
import { mockProvider } from '../../testing/mock'; import { JwtKey, JwtKeys } from '../../validation/jwtkeys';
import type { JwtKey, JwtKeys } from '../../validation/jwtkeys';
import { ValidationResult } from '../../validation/validation-result'; import { ValidationResult } from '../../validation/validation-result';
import type { AuthResult, CallbackContext } from '../callback-context'; import { AuthResult, CallbackContext } from '../callback-context';
import { FlowsDataService } from '../flows-data.service'; import { FlowsDataService } from '../flows-data.service';
import { ResetAuthDataService } from '../reset-auth-data.service'; import { ResetAuthDataService } from '../reset-auth-data.service';
import { SigninKeyDataService } from '../signin-key-data.service'; import { SigninKeyDataService } from '../signin-key-data.service';
@ -47,6 +46,9 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
mockProvider(ResetAuthDataService), mockProvider(ResetAuthDataService),
], ],
}); });
});
beforeEach(() => {
service = TestBed.inject(HistoryJwtKeysCallbackHandlerService); service = TestBed.inject(HistoryJwtKeysCallbackHandlerService);
storagePersistenceService = TestBed.inject(StoragePersistenceService); storagePersistenceService = TestBed.inject(StoragePersistenceService);
resetAuthDataService = TestBed.inject(ResetAuthDataService); resetAuthDataService = TestBed.inject(ResetAuthDataService);
@ -60,8 +62,8 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
}); });
describe('callbackHistoryAndResetJwtKeys', () => { describe('callbackHistoryAndResetJwtKeys', () => {
it('writes authResult into the storage', async () => { it('writes authResult into the storage', waitForAsync(() => {
const storagePersistenceServiceSpy = vi.spyOn( const storagePersistenceServiceSpy = spyOn(
storagePersistenceService, storagePersistenceService,
'write' 'write'
); );
@ -73,82 +75,86 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
const callbackContext = { const callbackContext = {
authResult: DUMMY_AUTH_RESULT, authResult: DUMMY_AUTH_RESULT,
} as CallbackContext; } as CallbackContext;
const allConfigs = [ const allconfigs = [
{ {
configId: 'configId1', configId: 'configId1',
historyCleanupOff: true, historyCleanupOff: true,
}, },
]; ];
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
of({ keys: [] } as JwtKeys) of({ keys: [] } as JwtKeys)
); );
await firstValueFrom( service
service.callbackHistoryAndResetJwtKeys( .callbackHistoryAndResetJwtKeys(
callbackContext, callbackContext,
allConfigs[0]!, allconfigs[0],
allConfigs allconfigs
) )
); .subscribe(() => {
expect(storagePersistenceServiceSpy.mock.calls).toEqual([ expect(storagePersistenceServiceSpy.calls.allArgs()).toEqual([
['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]], ['authnResult', DUMMY_AUTH_RESULT, allconfigs[0]],
['jwtKeys', { keys: [] }, allConfigs[0]], ['jwtKeys', { keys: [] }, allconfigs[0]],
]); ]);
expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(2); // write authnResult & jwtKeys
}); expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(2);
});
}));
it('writes refresh_token into the storage without reuse (refresh token rotation)', async () => { it('writes refresh_token into the storage without reuse (refresh token rotation)', waitForAsync(() => {
const DUMMY_AUTH_RESULT = { const DUMMY_AUTH_RESULT = {
refresh_token: 'dummy_refresh_token', refresh_token: 'dummy_refresh_token',
id_token: 'some-id-token', id_token: 'some-id-token',
}; };
const storagePersistenceServiceSpy = vi.spyOn( const storagePersistenceServiceSpy = spyOn(
storagePersistenceService, storagePersistenceService,
'write' 'write'
); );
const callbackContext = { const callbackContext = {
authResult: DUMMY_AUTH_RESULT, authResult: DUMMY_AUTH_RESULT,
} as CallbackContext; } as CallbackContext;
const allConfigs = [ const allconfigs = [
{ {
configId: 'configId1', configId: 'configId1',
historyCleanupOff: true, historyCleanupOff: true,
}, },
]; ];
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
of({ keys: [] } as JwtKeys) of({ keys: [] } as JwtKeys)
); );
await firstValueFrom( service
service.callbackHistoryAndResetJwtKeys( .callbackHistoryAndResetJwtKeys(
callbackContext, callbackContext,
allConfigs[0]!, allconfigs[0],
allConfigs allconfigs
) )
); .subscribe(() => {
expect(storagePersistenceServiceSpy.mock.calls).toEqual([ expect(storagePersistenceServiceSpy.calls.allArgs()).toEqual([
['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]], ['authnResult', DUMMY_AUTH_RESULT, allconfigs[0]],
['jwtKeys', { keys: [] }, allConfigs[0]], ['jwtKeys', { keys: [] }, allconfigs[0]],
]); ]);
expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(2); // write authnResult & refresh_token & jwtKeys
}); expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(2);
});
}));
it('writes refresh_token into the storage with reuse (without refresh token rotation)', async () => { it('writes refresh_token into the storage with reuse (without refresh token rotation)', waitForAsync(() => {
const DUMMY_AUTH_RESULT = { const DUMMY_AUTH_RESULT = {
refresh_token: 'dummy_refresh_token', refresh_token: 'dummy_refresh_token',
id_token: 'some-id-token', id_token: 'some-id-token',
}; };
const storagePersistenceServiceSpy = vi.spyOn( const storagePersistenceServiceSpy = spyOn(
storagePersistenceService, storagePersistenceService,
'write' 'write'
); );
const callbackContext = { const callbackContext = {
authResult: DUMMY_AUTH_RESULT, authResult: DUMMY_AUTH_RESULT,
} as CallbackContext; } as CallbackContext;
const allConfigs = [ const allconfigs = [
{ {
configId: 'configId1', configId: 'configId1',
historyCleanupOff: true, historyCleanupOff: true,
@ -156,25 +162,27 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
}, },
]; ];
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
of({ keys: [] } as JwtKeys) of({ keys: [] } as JwtKeys)
); );
await firstValueFrom( service
service.callbackHistoryAndResetJwtKeys( .callbackHistoryAndResetJwtKeys(
callbackContext, callbackContext,
allConfigs[0]!, allconfigs[0],
allConfigs allconfigs
) )
); .subscribe(() => {
expect(storagePersistenceServiceSpy.mock.calls).toEqual([ expect(storagePersistenceServiceSpy.calls.allArgs()).toEqual([
['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]], ['authnResult', DUMMY_AUTH_RESULT, allconfigs[0]],
['reusable_refresh_token', 'dummy_refresh_token', allConfigs[0]], ['reusable_refresh_token', 'dummy_refresh_token', allconfigs[0]],
['jwtKeys', { keys: [] }, allConfigs[0]], ['jwtKeys', { keys: [] }, allconfigs[0]],
]); ]);
expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(3); // write authnResult & refresh_token & jwtKeys
}); expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(3);
});
}));
it('resetBrowserHistory if historyCleanup is turned on and is not in a renewProcess', async () => { it('resetBrowserHistory if historyCleanup is turned on and is not in a renewProcess', waitForAsync(() => {
const DUMMY_AUTH_RESULT = { const DUMMY_AUTH_RESULT = {
id_token: 'some-id-token', id_token: 'some-id-token',
}; };
@ -182,29 +190,30 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
isRenewProcess: false, isRenewProcess: false,
authResult: DUMMY_AUTH_RESULT, authResult: DUMMY_AUTH_RESULT,
} as CallbackContext; } as CallbackContext;
const allConfigs = [ const allconfigs = [
{ {
configId: 'configId1', configId: 'configId1',
historyCleanupOff: false, historyCleanupOff: false,
}, },
]; ];
const windowSpy = vi.spyOn(window.history, 'replaceState'); const windowSpy = spyOn(window.history, 'replaceState');
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
of({ keys: [] } as JwtKeys) of({ keys: [] } as JwtKeys)
); );
await firstValueFrom( service
service.callbackHistoryAndResetJwtKeys( .callbackHistoryAndResetJwtKeys(
callbackContext, callbackContext,
allConfigs[0]!, allconfigs[0],
allConfigs allconfigs
) )
); .subscribe(() => {
expect(windowSpy).toHaveBeenCalledTimes(1); expect(windowSpy).toHaveBeenCalledTimes(1);
}); });
}));
it('returns callbackContext with jwtkeys filled if everything works fine', async () => { it('returns callbackContext with jwtkeys filled if everything works fine', waitForAsync(() => {
const DUMMY_AUTH_RESULT = { const DUMMY_AUTH_RESULT = {
id_token: 'some-id-token', id_token: 'some-id-token',
}; };
@ -213,31 +222,32 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
isRenewProcess: false, isRenewProcess: false,
authResult: DUMMY_AUTH_RESULT, authResult: DUMMY_AUTH_RESULT,
} as CallbackContext; } as CallbackContext;
const allConfigs = [ const allconfigs = [
{ {
configId: 'configId1', configId: 'configId1',
historyCleanupOff: false, historyCleanupOff: false,
}, },
]; ];
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
of({ keys: [{ kty: 'henlo' } as JwtKey] } as JwtKeys) of({ keys: [{ kty: 'henlo' } as JwtKey] } as JwtKeys)
); );
const result = await firstValueFrom( service
service.callbackHistoryAndResetJwtKeys( .callbackHistoryAndResetJwtKeys(
callbackContext, callbackContext,
allConfigs[0]!, allconfigs[0],
allConfigs allconfigs
) )
); .subscribe((result) => {
expect(result).toEqual({ expect(result).toEqual({
isRenewProcess: false, isRenewProcess: false,
authResult: DUMMY_AUTH_RESULT, authResult: DUMMY_AUTH_RESULT,
jwtKeys: { keys: [{ kty: 'henlo' }] }, jwtKeys: { keys: [{ kty: 'henlo' }] },
} as CallbackContext); } as CallbackContext);
}); });
}));
it('returns error if no jwtKeys have been in the call --> keys are null', async () => { it('returns error if no jwtKeys have been in the call --> keys are null', waitForAsync(() => {
const DUMMY_AUTH_RESULT = { const DUMMY_AUTH_RESULT = {
id_token: 'some-id-token', id_token: 'some-id-token',
}; };
@ -246,32 +256,32 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
isRenewProcess: false, isRenewProcess: false,
authResult: DUMMY_AUTH_RESULT, authResult: DUMMY_AUTH_RESULT,
} as CallbackContext; } as CallbackContext;
const allConfigs = [ const allconfigs = [
{ {
configId: 'configId1', configId: 'configId1',
historyCleanupOff: false, historyCleanupOff: false,
}, },
]; ];
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
of({} as JwtKeys) of({} as JwtKeys)
); );
try { service
await firstValueFrom( .callbackHistoryAndResetJwtKeys(
service.callbackHistoryAndResetJwtKeys( callbackContext,
callbackContext, allconfigs[0],
allConfigs[0]!, allconfigs
allConfigs )
) .subscribe({
); error: (err) => {
} catch (err: any) { expect(err.message).toEqual(
expect(err.message).toEqual( `Failed to retrieve signing key with error: Error: Failed to retrieve signing key`
'Failed to retrieve signing key with error: Error: Failed to retrieve signing key' );
); },
} });
}); }));
it('returns error if no jwtKeys have been in the call --> keys throw an error', async () => { it('returns error if no jwtKeys have been in the call --> keys throw an error', waitForAsync(() => {
const DUMMY_AUTH_RESULT = { const DUMMY_AUTH_RESULT = {
id_token: 'some-id-token', id_token: 'some-id-token',
}; };
@ -279,140 +289,140 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
isRenewProcess: false, isRenewProcess: false,
authResult: DUMMY_AUTH_RESULT, authResult: DUMMY_AUTH_RESULT,
} as CallbackContext; } as CallbackContext;
const allConfigs = [ const allconfigs = [
{ {
configId: 'configId1', configId: 'configId1',
historyCleanupOff: false, historyCleanupOff: false,
}, },
]; ];
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
throwError(() => new Error('error')) throwError(() => new Error('error'))
); );
try { service
await firstValueFrom( .callbackHistoryAndResetJwtKeys(
service.callbackHistoryAndResetJwtKeys( callbackContext,
callbackContext, allconfigs[0],
allConfigs[0]!, allconfigs
allConfigs )
) .subscribe({
); error: (err) => {
} catch (err: any) { expect(err.message).toEqual(
expect(err.message).toEqual( `Failed to retrieve signing key with error: Error: Error: error`
'Failed to retrieve signing key with error: Error: Error: error' );
); },
} });
}); }));
it('returns error if callbackContext.authresult has an error property filled', async () => { it('returns error if callbackContext.authresult has an error property filled', waitForAsync(() => {
const callbackContext = { const callbackContext = {
authResult: { error: 'someError' }, authResult: { error: 'someError' },
} as CallbackContext; } as CallbackContext;
const allConfigs = [ const allconfigs = [
{ {
configId: 'configId1', configId: 'configId1',
historyCleanupOff: true, historyCleanupOff: true,
}, },
]; ];
try { service
await firstValueFrom( .callbackHistoryAndResetJwtKeys(
service.callbackHistoryAndResetJwtKeys( callbackContext,
callbackContext, allconfigs[0],
allConfigs[0]!, allconfigs
allConfigs )
) .subscribe({
); error: (err) => {
} catch (err: any) { expect(err.message).toEqual(
expect(err.message).toEqual( `AuthCallback AuthResult came with error: someError`
'AuthCallback AuthResult came with error: someError' );
); },
} });
}); }));
it('calls resetAuthorizationData, resets nonce and authStateService in case of an error', async () => { it('calls resetAuthorizationData, resets nonce and authStateService in case of an error', waitForAsync(() => {
const callbackContext = { const callbackContext = {
authResult: { error: 'someError' }, authResult: { error: 'someError' },
isRenewProcess: false, isRenewProcess: false,
} as CallbackContext; } as CallbackContext;
const allConfigs = [ const allconfigs = [
{ {
configId: 'configId1', configId: 'configId1',
historyCleanupOff: true, historyCleanupOff: true,
}, },
]; ];
const resetAuthorizationDataSpy = vi.spyOn( const resetAuthorizationDataSpy = spyOn(
resetAuthDataService, resetAuthDataService,
'resetAuthorizationData' 'resetAuthorizationData'
); );
const setNonceSpy = vi.spyOn(flowsDataService, 'setNonce'); const setNonceSpy = spyOn(flowsDataService, 'setNonce');
const updateAndPublishAuthStateSpy = vi.spyOn( const updateAndPublishAuthStateSpy = spyOn(
authStateService, authStateService,
'updateAndPublishAuthState' 'updateAndPublishAuthState'
); );
try { service
await firstValueFrom( .callbackHistoryAndResetJwtKeys(
service.callbackHistoryAndResetJwtKeys( callbackContext,
callbackContext, allconfigs[0],
allConfigs[0]!, allconfigs
allConfigs )
) .subscribe({
); error: () => {
} catch { expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1);
expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1); expect(setNonceSpy).toHaveBeenCalledTimes(1);
expect(setNonceSpy).toHaveBeenCalledTimes(1); expect(updateAndPublishAuthStateSpy).toHaveBeenCalledOnceWith({
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({ isAuthenticated: false,
isAuthenticated: false, validationResult: ValidationResult.SecureTokenServerError,
validationResult: ValidationResult.SecureTokenServerError, isRenewProcess: false,
isRenewProcess: false, });
},
}); });
} }));
});
it('calls authStateService.updateAndPublishAuthState with login required if the error is `login_required`', async () => { it('calls authStateService.updateAndPublishAuthState with login required if the error is `login_required`', waitForAsync(() => {
const callbackContext = { const callbackContext = {
authResult: { error: 'login_required' }, authResult: { error: 'login_required' },
isRenewProcess: false, isRenewProcess: false,
} as CallbackContext; } as CallbackContext;
const allConfigs = [ const allconfigs = [
{ {
configId: 'configId1', configId: 'configId1',
historyCleanupOff: true, historyCleanupOff: true,
}, },
]; ];
const resetAuthorizationDataSpy = vi.spyOn( const resetAuthorizationDataSpy = spyOn(
resetAuthDataService, resetAuthDataService,
'resetAuthorizationData' 'resetAuthorizationData'
); );
const setNonceSpy = vi.spyOn(flowsDataService, 'setNonce'); const setNonceSpy = spyOn(flowsDataService, 'setNonce');
const updateAndPublishAuthStateSpy = vi.spyOn( const updateAndPublishAuthStateSpy = spyOn(
authStateService, authStateService,
'updateAndPublishAuthState' 'updateAndPublishAuthState'
); );
try { service
await firstValueFrom( .callbackHistoryAndResetJwtKeys(
service.callbackHistoryAndResetJwtKeys( callbackContext,
callbackContext, allconfigs[0],
allConfigs[0]!, allconfigs
allConfigs )
) .subscribe({
); error: () => {
} catch { expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1);
expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1); expect(setNonceSpy).toHaveBeenCalledTimes(1);
expect(setNonceSpy).toHaveBeenCalledTimes(1); expect(updateAndPublishAuthStateSpy).toHaveBeenCalledOnceWith({
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({ isAuthenticated: false,
isAuthenticated: false, validationResult: ValidationResult.LoginRequired,
validationResult: ValidationResult.LoginRequired, isRenewProcess: false,
isRenewProcess: false, });
},
}); });
} }));
});
it('should store jwtKeys', async () => { it('should store jwtKeys', waitForAsync(() => {
const DUMMY_AUTH_RESULT = { const DUMMY_AUTH_RESULT = {
id_token: 'some-id-token', id_token: 'some-id-token',
}; };
@ -420,41 +430,44 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
const initialCallbackContext = { const initialCallbackContext = {
authResult: DUMMY_AUTH_RESULT, authResult: DUMMY_AUTH_RESULT,
} as CallbackContext; } as CallbackContext;
const allConfigs = [ const allconfigs = [
{ {
configId: 'configId1', configId: 'configId1',
historyCleanupOff: true, historyCleanupOff: true,
}, },
]; ];
const storagePersistenceServiceSpy = vi.spyOn( const storagePersistenceServiceSpy = spyOn(
storagePersistenceService, storagePersistenceService,
'write' 'write'
); );
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
of(DUMMY_JWT_KEYS) of(DUMMY_JWT_KEYS)
); );
try { service
const callbackContext: CallbackContext = await firstValueFrom( .callbackHistoryAndResetJwtKeys(
service.callbackHistoryAndResetJwtKeys( initialCallbackContext,
initialCallbackContext, allconfigs[0],
allConfigs[0]!, allconfigs
allConfigs )
) .subscribe({
); next: (callbackContext: CallbackContext) => {
expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(2); expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(2);
expect(storagePersistenceServiceSpy.mock.calls).toEqual([ expect(storagePersistenceServiceSpy.calls.allArgs()).toEqual([
['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]], ['authnResult', DUMMY_AUTH_RESULT, allconfigs[0]],
['jwtKeys', DUMMY_JWT_KEYS, allConfigs[0]], ['jwtKeys', DUMMY_JWT_KEYS, allconfigs[0]],
]); ]);
expect(callbackContext.jwtKeys).toEqual(DUMMY_JWT_KEYS);
} catch (err: any) {
expect(err).toBeFalsy();
}
});
it('should not store jwtKeys on error', async () => { expect(callbackContext.jwtKeys).toEqual(DUMMY_JWT_KEYS);
},
error: (err) => {
expect(err).toBeFalsy();
},
});
}));
it('should not store jwtKeys on error', waitForAsync(() => {
const authResult = { const authResult = {
id_token: 'some-id-token', id_token: 'some-id-token',
access_token: 'some-access-token', access_token: 'some-access-token',
@ -463,41 +476,45 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
authResult, authResult,
} as CallbackContext; } as CallbackContext;
const allConfigs = [ const allconfigs = [
{ {
configId: 'configId1', configId: 'configId1',
historyCleanupOff: true, historyCleanupOff: true,
}, },
]; ];
const storagePersistenceServiceSpy = vi.spyOn( const storagePersistenceServiceSpy = spyOn(
storagePersistenceService, storagePersistenceService,
'write' 'write'
); );
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
throwError(() => new Error('Error')) throwError(() => new Error('Error'))
); );
try { service
const callbackContext: CallbackContext = await firstValueFrom( .callbackHistoryAndResetJwtKeys(
service.callbackHistoryAndResetJwtKeys( initialCallbackContext,
initialCallbackContext, allconfigs[0],
allConfigs[0]!, allconfigs
allConfigs )
) .subscribe({
); next: (callbackContext: CallbackContext) => {
expect(callbackContext).toBeFalsy(); expect(callbackContext).toBeFalsy();
} catch (err: any) { },
expect(err).toBeTruthy(); error: (err) => {
expect(storagePersistenceServiceSpy).toHaveBeenCalledExactlyOnceWith( expect(err).toBeTruthy();
'authnResult',
authResult,
allConfigs[0]
);
}
});
it('should fallback to stored jwtKeys on error', async () => { // storagePersistenceService.write() should not have been called with jwtKeys
expect(storagePersistenceServiceSpy).toHaveBeenCalledOnceWith(
'authnResult',
authResult,
allconfigs[0]
);
},
});
}));
it('should fallback to stored jwtKeys on error', waitForAsync(() => {
const authResult = { const authResult = {
id_token: 'some-id-token', id_token: 'some-id-token',
access_token: 'some-access-token', access_token: 'some-access-token',
@ -506,72 +523,76 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
authResult, authResult,
} as CallbackContext; } as CallbackContext;
const allConfigs = [ const allconfigs = [
{ {
configId: 'configId1', configId: 'configId1',
historyCleanupOff: true, historyCleanupOff: true,
}, },
]; ];
const storagePersistenceServiceSpy = vi.spyOn( const storagePersistenceServiceSpy = spyOn(
storagePersistenceService, storagePersistenceService,
'read' 'read'
); );
storagePersistenceServiceSpy.mockReturnValue(DUMMY_JWT_KEYS); storagePersistenceServiceSpy.and.returnValue(DUMMY_JWT_KEYS);
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
throwError(() => new Error('Error')) throwError(() => new Error('Error'))
); );
try { service
const callbackContext: CallbackContext = await firstValueFrom( .callbackHistoryAndResetJwtKeys(
service.callbackHistoryAndResetJwtKeys( initialCallbackContext,
initialCallbackContext, allconfigs[0],
allConfigs[0]!, allconfigs
allConfigs )
) .subscribe({
); next: (callbackContext: CallbackContext) => {
expect(storagePersistenceServiceSpy).toHaveBeenCalledExactlyOnceWith( expect(storagePersistenceServiceSpy).toHaveBeenCalledOnceWith(
'jwtKeys', 'jwtKeys',
allConfigs[0] allconfigs[0]
); );
expect(callbackContext.jwtKeys).toEqual(DUMMY_JWT_KEYS); expect(callbackContext.jwtKeys).toEqual(DUMMY_JWT_KEYS);
} catch (err: any) { },
expect(err).toBeFalsy(); error: (err) => {
} expect(err).toBeFalsy();
}); },
});
}));
it('should throw error if no jwtKeys are stored', async () => { it('should throw error if no jwtKeys are stored', waitForAsync(() => {
const authResult = { const authResult = {
id_token: 'some-id-token', id_token: 'some-id-token',
access_token: 'some-access-token', access_token: 'some-access-token',
} as AuthResult; } as AuthResult;
const initialCallbackContext = { authResult } as CallbackContext; const initialCallbackContext = { authResult } as CallbackContext;
const allConfigs = [ const allconfigs = [
{ {
configId: 'configId1', configId: 'configId1',
historyCleanupOff: true, historyCleanupOff: true,
}, },
]; ];
vi.spyOn(storagePersistenceService, 'read').mockReturnValue(null); spyOn(storagePersistenceService, 'read').and.returnValue(null);
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
throwError(() => new Error('Error')) throwError(() => new Error('Error'))
); );
try { service
const callbackContext: CallbackContext = await firstValueFrom( .callbackHistoryAndResetJwtKeys(
service.callbackHistoryAndResetJwtKeys( initialCallbackContext,
initialCallbackContext, allconfigs[0],
allConfigs[0]!, allconfigs
allConfigs )
) .subscribe({
); next: (callbackContext: CallbackContext) => {
expect(callbackContext).toBeFalsy(); expect(callbackContext).toBeFalsy();
} catch (err: any) { },
expect(err).toBeTruthy(); error: (err) => {
} expect(err).toBeTruthy();
}); },
});
}));
}); });
describe('historyCleanUpTurnedOn ', () => { describe('historyCleanUpTurnedOn ', () => {

View File

@ -1,14 +1,14 @@
import { Injectable, inject } from 'injection-js'; import { DOCUMENT } from '../../dom';
import { type Observable, of, throwError } from 'rxjs'; import { inject, Injectable } from 'injection-js';
import { Observable, of, throwError } from 'rxjs';
import { catchError, switchMap, tap } from 'rxjs/operators'; import { catchError, switchMap, tap } from 'rxjs/operators';
import { AuthStateService } from '../../auth-state/auth-state.service'; import { AuthStateService } from '../../auth-state/auth-state.service';
import type { OpenIdConfiguration } from '../../config/openid-configuration'; import { OpenIdConfiguration } from '../../config/openid-configuration';
import { DOCUMENT } from '../../dom';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { StoragePersistenceService } from '../../storage/storage-persistence.service'; import { StoragePersistenceService } from '../../storage/storage-persistence.service';
import type { JwtKeys } from '../../validation/jwtkeys'; import { JwtKeys } from '../../validation/jwtkeys';
import { ValidationResult } from '../../validation/validation-result'; import { ValidationResult } from '../../validation/validation-result';
import type { CallbackContext } from '../callback-context'; import { CallbackContext } from '../callback-context';
import { FlowsDataService } from '../flows-data.service'; import { FlowsDataService } from '../flows-data.service';
import { ResetAuthDataService } from '../reset-auth-data.service'; import { ResetAuthDataService } from '../reset-auth-data.service';
import { SigninKeyDataService } from '../signin-key-data.service'; import { SigninKeyDataService } from '../signin-key-data.service';
@ -98,10 +98,10 @@ export class HistoryJwtKeysCallbackHandlerService {
// fallback: try to load jwtKeys from storage // fallback: try to load jwtKeys from storage
const storedJwtKeys = this.readSigningKeys(config); const storedJwtKeys = this.readSigningKeys(config);
if (storedJwtKeys) { if (!!storedJwtKeys) {
this.loggerService.logWarning( this.loggerService.logWarning(
config, config,
'Failed to retrieve signing keys, fallback to stored keys' `Failed to retrieve signing keys, fallback to stored keys`
); );
return of(storedJwtKeys); return of(storedJwtKeys);
@ -116,7 +116,7 @@ export class HistoryJwtKeysCallbackHandlerService {
return of(callbackContext); return of(callbackContext);
} }
const errorMessage = 'Failed to retrieve signing key'; const errorMessage = `Failed to retrieve signing key`;
this.loggerService.logWarning(config, errorMessage); this.loggerService.logWarning(config, errorMessage);

View File

@ -1,10 +1,8 @@
import { TestBed } from '@/testing';
import { firstValueFrom } from 'rxjs';
import { vi } from 'vitest';
import { DOCUMENT } from '../../dom'; import { DOCUMENT } from '../../dom';
import { TestBed, waitForAsync } from '@angular/core/testing';
import { mockProvider } from '../../../test/auto-mock';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { mockProvider } from '../../testing/mock'; import { CallbackContext } from '../callback-context';
import type { CallbackContext } from '../callback-context';
import { FlowsDataService } from '../flows-data.service'; import { FlowsDataService } from '../flows-data.service';
import { ResetAuthDataService } from '../reset-auth-data.service'; import { ResetAuthDataService } from '../reset-auth-data.service';
import { ImplicitFlowCallbackHandlerService } from './implicit-flow-callback-handler.service'; import { ImplicitFlowCallbackHandlerService } from './implicit-flow-callback-handler.service';
@ -36,6 +34,9 @@ describe('ImplicitFlowCallbackHandlerService', () => {
}, },
], ],
}); });
});
beforeEach(() => {
service = TestBed.inject(ImplicitFlowCallbackHandlerService); service = TestBed.inject(ImplicitFlowCallbackHandlerService);
flowsDataService = TestBed.inject(FlowsDataService); flowsDataService = TestBed.inject(FlowsDataService);
resetAuthDataService = TestBed.inject(ResetAuthDataService); resetAuthDataService = TestBed.inject(ResetAuthDataService);
@ -46,44 +47,46 @@ describe('ImplicitFlowCallbackHandlerService', () => {
}); });
describe('implicitFlowCallback', () => { describe('implicitFlowCallback', () => {
it('calls "resetAuthorizationData" if silent renew is not running', async () => { it('calls "resetAuthorizationData" if silent renew is not running', waitForAsync(() => {
vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(false); spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false);
const resetAuthorizationDataSpy = vi.spyOn( const resetAuthorizationDataSpy = spyOn(
resetAuthDataService, resetAuthDataService,
'resetAuthorizationData' 'resetAuthorizationData'
); );
const allConfigs = [ const allconfigs = [
{ {
configId: 'configId1', configId: 'configId1',
}, },
]; ];
await firstValueFrom( service
service.implicitFlowCallback(allConfigs[0]!, allConfigs, 'any-hash') .implicitFlowCallback(allconfigs[0], allconfigs, 'any-hash')
); .subscribe(() => {
expect(resetAuthorizationDataSpy).toHaveBeenCalled(); expect(resetAuthorizationDataSpy).toHaveBeenCalled();
}); });
}));
it('does NOT calls "resetAuthorizationData" if silent renew is running', async () => { it('does NOT calls "resetAuthorizationData" if silent renew is running', waitForAsync(() => {
vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(true); spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true);
const resetAuthorizationDataSpy = vi.spyOn( const resetAuthorizationDataSpy = spyOn(
resetAuthDataService, resetAuthDataService,
'resetAuthorizationData' 'resetAuthorizationData'
); );
const allConfigs = [ const allconfigs = [
{ {
configId: 'configId1', configId: 'configId1',
}, },
]; ];
await firstValueFrom( service
service.implicitFlowCallback(allConfigs[0]!, allConfigs, 'any-hash') .implicitFlowCallback(allconfigs[0], allconfigs, 'any-hash')
); .subscribe(() => {
expect(resetAuthorizationDataSpy).not.toHaveBeenCalled(); expect(resetAuthorizationDataSpy).not.toHaveBeenCalled();
}); });
}));
it('returns callbackContext if all params are good', async () => { it('returns callbackContext if all params are good', waitForAsync(() => {
vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(true); spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true);
const expectedCallbackContext = { const expectedCallbackContext = {
code: '', code: '',
refreshToken: '', refreshToken: '',
@ -96,20 +99,21 @@ describe('ImplicitFlowCallbackHandlerService', () => {
existingIdToken: null, existingIdToken: null,
} as CallbackContext; } as CallbackContext;
const allConfigs = [ const allconfigs = [
{ {
configId: 'configId1', configId: 'configId1',
}, },
]; ];
const callbackContext = await firstValueFrom( service
service.implicitFlowCallback(allConfigs[0]!, allConfigs, 'anyHash') .implicitFlowCallback(allconfigs[0], allconfigs, 'anyHash')
); .subscribe((callbackContext) => {
expect(callbackContext).toEqual(expectedCallbackContext); expect(callbackContext).toEqual(expectedCallbackContext);
}); });
}));
it('uses window location hash if no hash is passed', async () => { it('uses window location hash if no hash is passed', waitForAsync(() => {
vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(true); spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true);
const expectedCallbackContext = { const expectedCallbackContext = {
code: '', code: '',
refreshToken: '', refreshToken: '',
@ -122,16 +126,17 @@ describe('ImplicitFlowCallbackHandlerService', () => {
existingIdToken: null, existingIdToken: null,
} as CallbackContext; } as CallbackContext;
const allConfigs = [ const allconfigs = [
{ {
configId: 'configId1', configId: 'configId1',
}, },
]; ];
const callbackContext = await firstValueFrom( service
service.implicitFlowCallback(allConfigs[0]!, allConfigs) .implicitFlowCallback(allconfigs[0], allconfigs)
); .subscribe((callbackContext) => {
expect(callbackContext).toEqual(expectedCallbackContext); expect(callbackContext).toEqual(expectedCallbackContext);
}); });
}));
}); });
}); });

View File

@ -1,9 +1,9 @@
import { Injectable, inject } from 'injection-js';
import { type Observable, of } from 'rxjs';
import type { OpenIdConfiguration } from '../../config/openid-configuration';
import { DOCUMENT } from '../../dom'; import { DOCUMENT } from '../../dom';
import { inject, Injectable } from 'injection-js';
import { Observable, of } from 'rxjs';
import { OpenIdConfiguration } from '../../config/openid-configuration';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import type { AuthResult, CallbackContext } from '../callback-context'; import { AuthResult, CallbackContext } from '../callback-context';
import { FlowsDataService } from '../flows-data.service'; import { FlowsDataService } from '../flows-data.service';
import { ResetAuthDataService } from '../reset-auth-data.service'; import { ResetAuthDataService } from '../reset-auth-data.service';

View File

@ -1,10 +1,8 @@
import { TestBed } from '@/testing'; import { TestBed, waitForAsync } from '@angular/core/testing';
import { firstValueFrom } from 'rxjs'; import { mockProvider } from '../../../test/auto-mock';
import { vi } from 'vitest';
import { AuthStateService } from '../../auth-state/auth-state.service'; import { AuthStateService } from '../../auth-state/auth-state.service';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { mockProvider } from '../../testing/mock'; import { CallbackContext } from '../callback-context';
import type { CallbackContext } from '../callback-context';
import { FlowsDataService } from '../flows-data.service'; import { FlowsDataService } from '../flows-data.service';
import { RefreshSessionCallbackHandlerService } from './refresh-session-callback-handler.service'; import { RefreshSessionCallbackHandlerService } from './refresh-session-callback-handler.service';
@ -22,6 +20,9 @@ describe('RefreshSessionCallbackHandlerService', () => {
mockProvider(FlowsDataService), mockProvider(FlowsDataService),
], ],
}); });
});
beforeEach(() => {
service = TestBed.inject(RefreshSessionCallbackHandlerService); service = TestBed.inject(RefreshSessionCallbackHandlerService);
flowsDataService = TestBed.inject(FlowsDataService); flowsDataService = TestBed.inject(FlowsDataService);
authStateService = TestBed.inject(AuthStateService); authStateService = TestBed.inject(AuthStateService);
@ -32,15 +33,15 @@ describe('RefreshSessionCallbackHandlerService', () => {
}); });
describe('refreshSessionWithRefreshTokens', () => { describe('refreshSessionWithRefreshTokens', () => {
it('returns callbackContext if all params are good', async () => { it('returns callbackContext if all params are good', waitForAsync(() => {
vi.spyOn( spyOn(
flowsDataService, flowsDataService,
'getExistingOrCreateAuthStateControl' 'getExistingOrCreateAuthStateControl'
).mockReturnValue('state-data'); ).and.returnValue('state-data');
vi.spyOn(authStateService, 'getRefreshToken').mockReturnValue( spyOn(authStateService, 'getRefreshToken').and.returnValue(
'henlo-furiend' 'henlo-furiend'
); );
vi.spyOn(authStateService, 'getIdToken').mockReturnValue('henlo-legger'); spyOn(authStateService, 'getIdToken').and.returnValue('henlo-legger');
const expectedCallbackContext = { const expectedCallbackContext = {
code: '', code: '',
@ -54,27 +55,28 @@ describe('RefreshSessionCallbackHandlerService', () => {
existingIdToken: 'henlo-legger', existingIdToken: 'henlo-legger',
} as CallbackContext; } as CallbackContext;
const callbackContext = await firstValueFrom( service
service.refreshSessionWithRefreshTokens({ configId: 'configId1' }) .refreshSessionWithRefreshTokens({ configId: 'configId1' })
); .subscribe((callbackContext) => {
expect(callbackContext).toEqual(expectedCallbackContext); expect(callbackContext).toEqual(expectedCallbackContext);
}); });
}));
it('throws error if no refresh token is given', async () => { it('throws error if no refresh token is given', waitForAsync(() => {
vi.spyOn( spyOn(
flowsDataService, flowsDataService,
'getExistingOrCreateAuthStateControl' 'getExistingOrCreateAuthStateControl'
).mockReturnValue('state-data'); ).and.returnValue('state-data');
vi.spyOn(authStateService, 'getRefreshToken').mockReturnValue(''); spyOn(authStateService, 'getRefreshToken').and.returnValue('');
vi.spyOn(authStateService, 'getIdToken').mockReturnValue('henlo-legger'); spyOn(authStateService, 'getIdToken').and.returnValue('henlo-legger');
try { service
await firstValueFrom( .refreshSessionWithRefreshTokens({ configId: 'configId1' })
service.refreshSessionWithRefreshTokens({ configId: 'configId1' }) .subscribe({
); error: (err) => {
} catch (err: any) { expect(err).toBeTruthy();
expect(err).toBeTruthy(); },
} });
}); }));
}); });
}); });

View File

@ -1,10 +1,10 @@
import { Injectable, inject } from 'injection-js'; import { inject, Injectable } from 'injection-js';
import { type Observable, of, throwError } from 'rxjs'; import { Observable, of, throwError } from 'rxjs';
import { AuthStateService } from '../../auth-state/auth-state.service'; import { AuthStateService } from '../../auth-state/auth-state.service';
import type { OpenIdConfiguration } from '../../config/openid-configuration'; import { OpenIdConfiguration } from '../../config/openid-configuration';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { TokenValidationService } from '../../validation/token-validation.service'; import { TokenValidationService } from '../../validation/token-validation.service';
import type { CallbackContext } from '../callback-context'; import { CallbackContext } from '../callback-context';
import { FlowsDataService } from '../flows-data.service'; import { FlowsDataService } from '../flows-data.service';
@Injectable() @Injectable()
@ -24,7 +24,7 @@ export class RefreshSessionCallbackHandlerService {
this.loggerService.logDebug( this.loggerService.logDebug(
config, config,
`RefreshSession created. Adding myautostate: ${stateData}` 'RefreshSession created. Adding myautostate: ' + stateData
); );
const refreshToken = this.authStateService.getRefreshToken(config); const refreshToken = this.authStateService.getRefreshToken(config);
const idToken = this.authStateService.getIdToken(config); const idToken = this.authStateService.getIdToken(config);
@ -53,11 +53,12 @@ export class RefreshSessionCallbackHandlerService {
); );
return of(callbackContext); return of(callbackContext);
} else {
const errorMessage = 'no refresh token found, please login';
this.loggerService.logError(config, errorMessage);
return throwError(() => new Error(errorMessage));
} }
const errorMessage = 'no refresh token found, please login';
this.loggerService.logError(config, errorMessage);
return throwError(() => new Error(errorMessage));
} }
} }

View File

@ -1,14 +1,13 @@
import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { HttpErrorResponse, HttpHeaders } from '@ngify/http'; import { TestBed, waitForAsync } from '@angular/core/testing';
import { firstValueFrom, of, throwError } from 'rxjs'; import { of, throwError } from 'rxjs';
import { vi } from 'vitest'; import { mockProvider } from '../../../test/auto-mock';
import { createRetriableStream } from '../../../test/create-retriable-stream.helper';
import { DataService } from '../../api/data.service'; import { DataService } from '../../api/data.service';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { StoragePersistenceService } from '../../storage/storage-persistence.service'; import { StoragePersistenceService } from '../../storage/storage-persistence.service';
import { createRetriableStream } from '../../testing/create-retriable-stream.helper';
import { mockProvider } from '../../testing/mock';
import { UrlService } from '../../utils/url/url.service'; import { UrlService } from '../../utils/url/url.service';
import type { CallbackContext } from '../callback-context'; import { CallbackContext } from '../callback-context';
import { RefreshTokenCallbackHandlerService } from './refresh-token-callback-handler.service'; import { RefreshTokenCallbackHandlerService } from './refresh-token-callback-handler.service';
describe('RefreshTokenCallbackHandlerService', () => { describe('RefreshTokenCallbackHandlerService', () => {
@ -26,6 +25,9 @@ describe('RefreshTokenCallbackHandlerService', () => {
mockProvider(StoragePersistenceService), mockProvider(StoragePersistenceService),
], ],
}); });
});
beforeEach(() => {
service = TestBed.inject(RefreshTokenCallbackHandlerService); service = TestBed.inject(RefreshTokenCallbackHandlerService);
storagePersistenceService = TestBed.inject(StoragePersistenceService); storagePersistenceService = TestBed.inject(StoragePersistenceService);
dataService = TestBed.inject(DataService); dataService = TestBed.inject(DataService);
@ -44,87 +46,83 @@ describe('RefreshTokenCallbackHandlerService', () => {
url: 'https://identity-server.test/openid-connect/token', url: 'https://identity-server.test/openid-connect/token',
}); });
it('throws error if no tokenEndpoint is given', async () => { it('throws error if no tokenEndpoint is given', waitForAsync(() => {
try { (service as any)
await firstValueFrom( .refreshTokensRequestTokens({} as CallbackContext)
(service as any).refreshTokensRequestTokens({} as CallbackContext) .subscribe({
); error: (err: unknown) => {
} catch (err: unknown) { expect(err).toBeTruthy();
expect(err).toBeTruthy(); },
} });
}); }));
it('calls data service if all params are good', async () => { it('calls data service if all params are good', waitForAsync(() => {
const postSpy = vi.spyOn(dataService, 'post').mockReturnValue(of({})); const postSpy = spyOn(dataService, 'post').and.returnValue(of({}));
mockImplementationWhenArgsEqual( spyOn(storagePersistenceService, 'read')
vi.spyOn(storagePersistenceService, 'read'), .withArgs('authWellKnownEndPoints', { configId: 'configId1' })
['authWellKnownEndPoints', { configId: 'configId1' }], .and.returnValue({ tokenEndpoint: 'tokenEndpoint' });
() => ({ tokenEndpoint: 'tokenEndpoint' })
);
await firstValueFrom( service
service.refreshTokensRequestTokens({} as CallbackContext, { .refreshTokensRequestTokens({} as CallbackContext, {
configId: 'configId1', configId: 'configId1',
}) })
); .subscribe(() => {
expect(postSpy).toHaveBeenCalledExactlyOnceWith( expect(postSpy).toHaveBeenCalledOnceWith(
'tokenEndpoint', 'tokenEndpoint',
undefined, undefined,
{ configId: 'configId1' }, { configId: 'configId1' },
expect.any(HttpHeaders) jasmine.any(HttpHeaders)
); );
const httpHeaders = postSpy.mock.calls.at(-1)?.[3] as HttpHeaders; const httpHeaders = postSpy.calls.mostRecent().args[3] as HttpHeaders;
expect(httpHeaders.has('Content-Type')).toBeTruthy();
expect(httpHeaders.get('Content-Type')).toBe(
'application/x-www-form-urlencoded'
);
});
it('calls data service with correct headers if all params are good', async () => { expect(httpHeaders.has('Content-Type')).toBeTrue();
const postSpy = vi.spyOn(dataService, 'post').mockReturnValue(of({})); expect(httpHeaders.get('Content-Type')).toBe(
'application/x-www-form-urlencoded'
);
});
}));
mockImplementationWhenArgsEqual( it('calls data service with correct headers if all params are good', waitForAsync(() => {
vi.spyOn(storagePersistenceService, 'read'), const postSpy = spyOn(dataService, 'post').and.returnValue(of({}));
['authWellKnownEndPoints', { configId: 'configId1' }],
() => ({ tokenEndpoint: 'tokenEndpoint' })
);
await firstValueFrom( spyOn(storagePersistenceService, 'read')
service.refreshTokensRequestTokens({} as CallbackContext, { .withArgs('authWellKnownEndPoints', { configId: 'configId1' })
.and.returnValue({ tokenEndpoint: 'tokenEndpoint' });
service
.refreshTokensRequestTokens({} as CallbackContext, {
configId: 'configId1', configId: 'configId1',
}) })
); .subscribe(() => {
const httpHeaders = postSpy.mock.calls.at(-1)?.[3] as HttpHeaders; const httpHeaders = postSpy.calls.mostRecent().args[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', async () => { expect(httpHeaders.has('Content-Type')).toBeTrue();
vi.spyOn(dataService, 'post').mockReturnValue( expect(httpHeaders.get('Content-Type')).toBe(
throwError(() => HTTP_ERROR) 'application/x-www-form-urlencoded'
); );
});
}));
it('returns error in case of http error', waitForAsync(() => {
spyOn(dataService, 'post').and.returnValue(throwError(() => HTTP_ERROR));
const config = { configId: 'configId1', authority: 'authority' }; const config = { configId: 'configId1', authority: 'authority' };
mockImplementationWhenArgsEqual( spyOn(storagePersistenceService, 'read')
vi.spyOn(storagePersistenceService, 'read'), .withArgs('authWellKnownEndPoints', config)
['authWellKnownEndPoints', config], .and.returnValue({ tokenEndpoint: 'tokenEndpoint' });
() => ({ tokenEndpoint: 'tokenEndpoint' })
);
try { service
await firstValueFrom( .refreshTokensRequestTokens({} as CallbackContext, config)
service.refreshTokensRequestTokens({} as CallbackContext, config) .subscribe({
); error: (err) => {
} catch (err: any) { expect(err).toBeTruthy();
expect(err).toBeTruthy(); },
} });
}); }));
it('retries request in case of no connection http error and succeeds', async () => { it('retries request in case of no connection http error and succeeds', waitForAsync(() => {
const postSpy = vi.spyOn(dataService, 'post').mockReturnValue( const postSpy = spyOn(dataService, 'post').and.returnValue(
createRetriableStream( createRetriableStream(
throwError(() => CONNECTION_ERROR), throwError(() => CONNECTION_ERROR),
of({}) of({})
@ -132,25 +130,26 @@ describe('RefreshTokenCallbackHandlerService', () => {
); );
const config = { configId: 'configId1', authority: 'authority' }; const config = { configId: 'configId1', authority: 'authority' };
mockImplementationWhenArgsEqual( spyOn(storagePersistenceService, 'read')
vi.spyOn(storagePersistenceService, 'read'), .withArgs('authWellKnownEndPoints', config)
['authWellKnownEndPoints', config], .and.returnValue({ tokenEndpoint: 'tokenEndpoint' });
() => ({ tokenEndpoint: 'tokenEndpoint' })
);
try { service
const res = await firstValueFrom( .refreshTokensRequestTokens({} as CallbackContext, config)
service.refreshTokensRequestTokens({} as CallbackContext, config) .subscribe({
); next: (res) => {
expect(res).toBeTruthy(); expect(res).toBeTruthy();
expect(postSpy).toHaveBeenCalledTimes(1); expect(postSpy).toHaveBeenCalledTimes(1);
} catch (err: any) { },
expect(err).toBeFalsy(); error: (err) => {
} // fails if there should be a result
}); expect(err).toBeFalsy();
},
});
}));
it('retries request in case of no connection http error and fails because of http error afterwards', async () => { it('retries request in case of no connection http error and fails because of http error afterwards', waitForAsync(() => {
const postSpy = vi.spyOn(dataService, 'post').mockReturnValue( const postSpy = spyOn(dataService, 'post').and.returnValue(
createRetriableStream( createRetriableStream(
throwError(() => CONNECTION_ERROR), throwError(() => CONNECTION_ERROR),
throwError(() => HTTP_ERROR) throwError(() => HTTP_ERROR)
@ -158,21 +157,22 @@ describe('RefreshTokenCallbackHandlerService', () => {
); );
const config = { configId: 'configId1', authority: 'authority' }; const config = { configId: 'configId1', authority: 'authority' };
mockImplementationWhenArgsEqual( spyOn(storagePersistenceService, 'read')
vi.spyOn(storagePersistenceService, 'read'), .withArgs('authWellKnownEndPoints', config)
['authWellKnownEndPoints', config], .and.returnValue({ tokenEndpoint: 'tokenEndpoint' });
() => ({ tokenEndpoint: 'tokenEndpoint' })
);
try { service
const res = await firstValueFrom( .refreshTokensRequestTokens({} as CallbackContext, config)
service.refreshTokensRequestTokens({} as CallbackContext, config) .subscribe({
); next: (res) => {
expect(res).toBeFalsy(); // fails if there should be a result
} catch (err: any) { expect(res).toBeFalsy();
expect(err).toBeTruthy(); },
expect(postSpy).toHaveBeenCalledTimes(1); error: (err) => {
} expect(err).toBeTruthy();
}); expect(postSpy).toHaveBeenCalledTimes(1);
},
});
}));
}); });
}); });

View File

@ -1,13 +1,13 @@
import { HttpHeaders } from '@ngify/http'; import { HttpHeaders } from '@ngify/http';
import { inject, Injectable } from 'injection-js'; import { inject, Injectable } from 'injection-js';
import { type Observable, of, throwError, timer } from 'rxjs'; import { Observable, of, throwError, timer } from 'rxjs';
import { catchError, mergeMap, retryWhen, switchMap } from 'rxjs/operators'; import { catchError, mergeMap, retryWhen, switchMap } from 'rxjs/operators';
import { DataService } from '../../api/data.service'; import { DataService } from '../../api/data.service';
import type { OpenIdConfiguration } from '../../config/openid-configuration'; import { OpenIdConfiguration } from '../../config/openid-configuration';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { StoragePersistenceService } from '../../storage/storage-persistence.service'; import { StoragePersistenceService } from '../../storage/storage-persistence.service';
import { UrlService } from '../../utils/url/url.service'; import { UrlService } from '../../utils/url/url.service';
import type { AuthResult, CallbackContext } from '../callback-context'; import { AuthResult, CallbackContext } from '../callback-context';
import { isNetworkError } from './error-helper'; import { isNetworkError } from './error-helper';
@Injectable() @Injectable()

View File

@ -1,14 +1,13 @@
import { TestBed } from '@/testing';
import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest';
import { AuthStateService } from '../../auth-state/auth-state.service';
import { DOCUMENT } from '../../dom'; import { DOCUMENT } from '../../dom';
import { TestBed, waitForAsync } from '@angular/core/testing';
import { of } from 'rxjs';
import { mockProvider } from '../../../test/auto-mock';
import { AuthStateService } from '../../auth-state/auth-state.service';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { mockProvider } from '../../testing/mock'; import { StateValidationResult } from '../../validation/state-validation-result';
import type { StateValidationResult } from '../../validation/state-validation-result';
import { StateValidationService } from '../../validation/state-validation.service'; import { StateValidationService } from '../../validation/state-validation.service';
import { ValidationResult } from '../../validation/validation-result'; import { ValidationResult } from '../../validation/validation-result';
import type { CallbackContext } from '../callback-context'; import { CallbackContext } from '../callback-context';
import { ResetAuthDataService } from '../reset-auth-data.service'; import { ResetAuthDataService } from '../reset-auth-data.service';
import { StateValidationCallbackHandlerService } from './state-validation-callback-handler.service'; import { StateValidationCallbackHandlerService } from './state-validation-callback-handler.service';
@ -42,6 +41,9 @@ describe('StateValidationCallbackHandlerService', () => {
}, },
], ],
}); });
});
beforeEach(() => {
service = TestBed.inject(StateValidationCallbackHandlerService); service = TestBed.inject(StateValidationCallbackHandlerService);
stateValidationService = TestBed.inject(StateValidationService); stateValidationService = TestBed.inject(StateValidationService);
loggerService = TestBed.inject(LoggerService); loggerService = TestBed.inject(LoggerService);
@ -54,11 +56,8 @@ describe('StateValidationCallbackHandlerService', () => {
}); });
describe('callbackStateValidation', () => { describe('callbackStateValidation', () => {
it('returns callbackContext with validationResult if validationResult is valid', async () => { it('returns callbackContext with validationResult if validationResult is valid', waitForAsync(() => {
vi.spyOn( spyOn(stateValidationService, 'getValidatedStateResult').and.returnValue(
stateValidationService,
'getValidatedStateResult'
).mockReturnValue(
of({ of({
idToken: 'idTokenJustForTesting', idToken: 'idTokenJustForTesting',
authResponseIsValid: true, authResponseIsValid: true,
@ -66,87 +65,82 @@ describe('StateValidationCallbackHandlerService', () => {
); );
const allConfigs = [{ configId: 'configId1' }]; const allConfigs = [{ configId: 'configId1' }];
const newCallbackContext = await firstValueFrom( service
service.callbackStateValidation( .callbackStateValidation(
{} as CallbackContext, {} as CallbackContext,
allConfigs[0]!, allConfigs[0],
allConfigs allConfigs
) )
); .subscribe((newCallbackContext) => {
expect(newCallbackContext).toEqual({ expect(newCallbackContext).toEqual({
validationResult: { validationResult: {
idToken: 'idTokenJustForTesting', idToken: 'idTokenJustForTesting',
authResponseIsValid: true, authResponseIsValid: true,
}, },
} as CallbackContext); } as CallbackContext);
}); });
}));
it('logs error in case of an error', async () => { it('logs error in case of an error', waitForAsync(() => {
vi.spyOn( spyOn(stateValidationService, 'getValidatedStateResult').and.returnValue(
stateValidationService,
'getValidatedStateResult'
).mockReturnValue(
of({ of({
authResponseIsValid: false, authResponseIsValid: false,
} as StateValidationResult) } as StateValidationResult)
); );
const loggerSpy = vi.spyOn(loggerService, 'logWarning'); const loggerSpy = spyOn(loggerService, 'logWarning');
const allConfigs = [{ configId: 'configId1' }]; const allConfigs = [{ configId: 'configId1' }];
try { service
await firstValueFrom( .callbackStateValidation(
service.callbackStateValidation( {} as CallbackContext,
{} as CallbackContext, allConfigs[0],
allConfigs[0]!, allConfigs
allConfigs )
) .subscribe({
); error: () => {
} catch { expect(loggerSpy).toHaveBeenCalledOnceWith(
expect(loggerSpy).toHaveBeenCalledExactlyOnceWith( allConfigs[0],
allConfigs[0]!, 'authorizedCallback, token(s) validation failed, resetting. Hash: &anyFakeHash'
'authorizedCallback, token(s) validation failed, resetting. Hash: &anyFakeHash' );
); },
} });
}); }));
it('calls resetAuthDataService.resetAuthorizationData and authStateService.updateAndPublishAuthState in case of an error', async () => { it('calls resetAuthDataService.resetAuthorizationData and authStateService.updateAndPublishAuthState in case of an error', waitForAsync(() => {
vi.spyOn( spyOn(stateValidationService, 'getValidatedStateResult').and.returnValue(
stateValidationService,
'getValidatedStateResult'
).mockReturnValue(
of({ of({
authResponseIsValid: false, authResponseIsValid: false,
state: ValidationResult.LoginRequired, state: ValidationResult.LoginRequired,
} as StateValidationResult) } as StateValidationResult)
); );
const resetAuthorizationDataSpy = vi.spyOn( const resetAuthorizationDataSpy = spyOn(
resetAuthDataService, resetAuthDataService,
'resetAuthorizationData' 'resetAuthorizationData'
); );
const updateAndPublishAuthStateSpy = vi.spyOn( const updateAndPublishAuthStateSpy = spyOn(
authStateService, authStateService,
'updateAndPublishAuthState' 'updateAndPublishAuthState'
); );
const allConfigs = [{ configId: 'configId1' }]; const allConfigs = [{ configId: 'configId1' }];
try { service
await firstValueFrom( .callbackStateValidation(
service.callbackStateValidation( { isRenewProcess: true } as CallbackContext,
{ isRenewProcess: true } as CallbackContext, allConfigs[0],
allConfigs[0]!, allConfigs
allConfigs )
) .subscribe({
); error: () => {
} catch { expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1);
expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1); expect(updateAndPublishAuthStateSpy).toHaveBeenCalledOnceWith({
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({ isAuthenticated: false,
isAuthenticated: false, validationResult: ValidationResult.LoginRequired,
validationResult: ValidationResult.LoginRequired, isRenewProcess: true,
isRenewProcess: true, });
},
}); });
} }));
});
}); });
}); });

View File

@ -1,13 +1,13 @@
import { Injectable, inject } from 'injection-js'; import { DOCUMENT } from '../../dom';
import type { Observable } from 'rxjs'; import { inject, Injectable } from 'injection-js';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { AuthStateService } from '../../auth-state/auth-state.service'; import { AuthStateService } from '../../auth-state/auth-state.service';
import type { OpenIdConfiguration } from '../../config/openid-configuration'; import { OpenIdConfiguration } from '../../config/openid-configuration';
import { DOCUMENT } from '../../dom';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import type { StateValidationResult } from '../../validation/state-validation-result'; import { StateValidationResult } from '../../validation/state-validation-result';
import { StateValidationService } from '../../validation/state-validation.service'; import { StateValidationService } from '../../validation/state-validation.service';
import type { CallbackContext } from '../callback-context'; import { CallbackContext } from '../callback-context';
import { ResetAuthDataService } from '../reset-auth-data.service'; import { ResetAuthDataService } from '../reset-auth-data.service';
@Injectable() @Injectable()
@ -43,20 +43,21 @@ export class StateValidationCallbackHandlerService {
); );
return callbackContext; return callbackContext;
} else {
const errorMessage = `authorizedCallback, token(s) validation failed, resetting. Hash: ${this.document.location.hash}`;
this.loggerService.logWarning(configuration, errorMessage);
this.resetAuthDataService.resetAuthorizationData(
configuration,
allConfigs
);
this.publishUnauthorizedState(
callbackContext.validationResult,
callbackContext.isRenewProcess
);
throw new Error(errorMessage);
} }
const errorMessage = `authorizedCallback, token(s) validation failed, resetting. Hash: ${this.document.location.hash}`;
this.loggerService.logWarning(configuration, errorMessage);
this.resetAuthDataService.resetAuthorizationData(
configuration,
allConfigs
);
this.publishUnauthorizedState(
callbackContext.validationResult,
callbackContext.isRenewProcess
);
throw new Error(errorMessage);
}) })
); );
} }

View File

@ -1,13 +1,12 @@
import { TestBed } from '@/testing'; import { TestBed, waitForAsync } from '@angular/core/testing';
import { firstValueFrom, of } from 'rxjs'; import { of } from 'rxjs';
import { vi } from 'vitest'; import { mockProvider } from '../../../test/auto-mock';
import { AuthStateService } from '../../auth-state/auth-state.service'; import { AuthStateService } from '../../auth-state/auth-state.service';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { mockProvider } from '../../testing/mock';
import { UserService } from '../../user-data/user.service'; import { UserService } from '../../user-data/user.service';
import { StateValidationResult } from '../../validation/state-validation-result'; import { StateValidationResult } from '../../validation/state-validation-result';
import { ValidationResult } from '../../validation/validation-result'; import { ValidationResult } from '../../validation/validation-result';
import type { CallbackContext } from '../callback-context'; import { CallbackContext } from '../callback-context';
import { FlowsDataService } from '../flows-data.service'; import { FlowsDataService } from '../flows-data.service';
import { ResetAuthDataService } from '../reset-auth-data.service'; import { ResetAuthDataService } from '../reset-auth-data.service';
import { UserCallbackHandlerService } from './user-callback-handler.service'; import { UserCallbackHandlerService } from './user-callback-handler.service';
@ -30,6 +29,9 @@ describe('UserCallbackHandlerService', () => {
mockProvider(ResetAuthDataService), mockProvider(ResetAuthDataService),
], ],
}); });
});
beforeEach(() => {
service = TestBed.inject(UserCallbackHandlerService); service = TestBed.inject(UserCallbackHandlerService);
flowsDataService = TestBed.inject(FlowsDataService); flowsDataService = TestBed.inject(FlowsDataService);
authStateService = TestBed.inject(AuthStateService); authStateService = TestBed.inject(AuthStateService);
@ -42,7 +44,7 @@ describe('UserCallbackHandlerService', () => {
}); });
describe('callbackUser', () => { describe('callbackUser', () => {
it('calls flowsDataService.setSessionState with correct params if autoUserInfo is false, isRenewProcess is false and refreshToken is null', async () => { it('calls flowsDataService.setSessionState with correct params if autoUserInfo is false, isRenewProcess is false and refreshToken is null', waitForAsync(() => {
const svr = new StateValidationResult( const svr = new StateValidationResult(
'accesstoken', 'accesstoken',
'idtoken', 'idtoken',
@ -68,16 +70,17 @@ describe('UserCallbackHandlerService', () => {
}, },
]; ];
const spy = vi.spyOn(flowsDataService, 'setSessionState'); const spy = spyOn(flowsDataService, 'setSessionState');
const resultCallbackContext = await firstValueFrom( service
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) .callbackUser(callbackContext, allConfigs[0], allConfigs)
); .subscribe((resultCallbackContext) => {
expect(spy).toHaveBeenCalledExactlyOnceWith('mystate', allConfigs[0]); expect(spy).toHaveBeenCalledOnceWith('mystate', allConfigs[0]);
expect(resultCallbackContext).toEqual(callbackContext); expect(resultCallbackContext).toEqual(callbackContext);
}); });
}));
it('does NOT call flowsDataService.setSessionState if autoUserInfo is false, isRenewProcess is true and refreshToken is null', async () => { it('does NOT call flowsDataService.setSessionState if autoUserInfo is false, isRenewProcess is true and refreshToken is null', waitForAsync(() => {
const svr = new StateValidationResult( const svr = new StateValidationResult(
'accesstoken', 'accesstoken',
'idtoken', 'idtoken',
@ -101,16 +104,17 @@ describe('UserCallbackHandlerService', () => {
autoUserInfo: false, autoUserInfo: false,
}, },
]; ];
const spy = vi.spyOn(flowsDataService, 'setSessionState'); const spy = spyOn(flowsDataService, 'setSessionState');
const resultCallbackContext = await firstValueFrom( service
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) .callbackUser(callbackContext, allConfigs[0], allConfigs)
); .subscribe((resultCallbackContext) => {
expect(spy).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled();
expect(resultCallbackContext).toEqual(callbackContext); expect(resultCallbackContext).toEqual(callbackContext);
}); });
}));
it('does NOT call flowsDataService.setSessionState if autoUserInfo is false isRenewProcess is false, refreshToken has value', async () => { it('does NOT call flowsDataService.setSessionState if autoUserInfo is false isRenewProcess is false, refreshToken has value', waitForAsync(() => {
const svr = new StateValidationResult( const svr = new StateValidationResult(
'accesstoken', 'accesstoken',
'idtoken', 'idtoken',
@ -134,16 +138,17 @@ describe('UserCallbackHandlerService', () => {
autoUserInfo: false, autoUserInfo: false,
}, },
]; ];
const spy = vi.spyOn(flowsDataService, 'setSessionState'); const spy = spyOn(flowsDataService, 'setSessionState');
const resultCallbackContext = await firstValueFrom( service
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) .callbackUser(callbackContext, allConfigs[0], allConfigs)
); .subscribe((resultCallbackContext) => {
expect(spy).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled();
expect(resultCallbackContext).toEqual(callbackContext); expect(resultCallbackContext).toEqual(callbackContext);
}); });
}));
it('does NOT call flowsDataService.setSessionState if autoUserInfo is false isRenewProcess is false, refreshToken has value, id_token is false', async () => { it('does NOT call flowsDataService.setSessionState if autoUserInfo is false isRenewProcess is false, refreshToken has value, id_token is false', waitForAsync(() => {
const svr = new StateValidationResult('accesstoken', '', true, ''); const svr = new StateValidationResult('accesstoken', '', true, '');
const callbackContext = { const callbackContext = {
code: '', code: '',
@ -163,16 +168,17 @@ describe('UserCallbackHandlerService', () => {
}, },
]; ];
const spy = vi.spyOn(flowsDataService, 'setSessionState'); const spy = spyOn(flowsDataService, 'setSessionState');
const resultCallbackContext = await firstValueFrom( service
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) .callbackUser(callbackContext, allConfigs[0], allConfigs)
); .subscribe((resultCallbackContext) => {
expect(spy).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled();
expect(resultCallbackContext).toEqual(callbackContext); expect(resultCallbackContext).toEqual(callbackContext);
}); });
}));
it('calls authStateService.updateAndPublishAuthState with correct params if autoUserInfo is false', async () => { it('calls authStateService.updateAndPublishAuthState with correct params if autoUserInfo is false', waitForAsync(() => {
const svr = new StateValidationResult( const svr = new StateValidationResult(
'accesstoken', 'accesstoken',
'idtoken', 'idtoken',
@ -198,23 +204,24 @@ describe('UserCallbackHandlerService', () => {
}, },
]; ];
const updateAndPublishAuthStateSpy = vi.spyOn( const updateAndPublishAuthStateSpy = spyOn(
authStateService, authStateService,
'updateAndPublishAuthState' 'updateAndPublishAuthState'
); );
const resultCallbackContext = await firstValueFrom( service
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) .callbackUser(callbackContext, allConfigs[0], allConfigs)
); .subscribe((resultCallbackContext) => {
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({ expect(updateAndPublishAuthStateSpy).toHaveBeenCalledOnceWith({
isAuthenticated: true, isAuthenticated: true,
validationResult: ValidationResult.NotSet, validationResult: ValidationResult.NotSet,
isRenewProcess: false, isRenewProcess: false,
}); });
expect(resultCallbackContext).toEqual(callbackContext); expect(resultCallbackContext).toEqual(callbackContext);
}); });
}));
it('calls userService.getAndPersistUserDataInStore with correct params if autoUserInfo is true', async () => { it('calls userService.getAndPersistUserDataInStore with correct params if autoUserInfo is true', waitForAsync(() => {
const svr = new StateValidationResult( const svr = new StateValidationResult(
'accesstoken', 'accesstoken',
'idtoken', 'idtoken',
@ -240,24 +247,26 @@ describe('UserCallbackHandlerService', () => {
}, },
]; ];
const getAndPersistUserDataInStoreSpy = vi const getAndPersistUserDataInStoreSpy = spyOn(
.spyOn(userService, 'getAndPersistUserDataInStore') userService,
.mockReturnValue(of({ user: 'some_data' })); 'getAndPersistUserDataInStore'
).and.returnValue(of({ user: 'some_data' }));
const resultCallbackContext = await firstValueFrom( service
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) .callbackUser(callbackContext, allConfigs[0], allConfigs)
); .subscribe((resultCallbackContext) => {
expect(getAndPersistUserDataInStoreSpy).toHaveBeenCalledExactlyOnceWith( expect(getAndPersistUserDataInStoreSpy).toHaveBeenCalledOnceWith(
allConfigs[0]!, allConfigs[0],
allConfigs, allConfigs,
false, false,
'idtoken', 'idtoken',
'decoded' 'decoded'
); );
expect(resultCallbackContext).toEqual(callbackContext); expect(resultCallbackContext).toEqual(callbackContext);
}); });
}));
it('calls authStateService.updateAndPublishAuthState with correct params if autoUserInfo is true', async () => { it('calls authStateService.updateAndPublishAuthState with correct params if autoUserInfo is true', waitForAsync(() => {
const svr = new StateValidationResult( const svr = new StateValidationResult(
'accesstoken', 'accesstoken',
'idtoken', 'idtoken',
@ -284,26 +293,27 @@ describe('UserCallbackHandlerService', () => {
}, },
]; ];
vi.spyOn(userService, 'getAndPersistUserDataInStore').mockReturnValue( spyOn(userService, 'getAndPersistUserDataInStore').and.returnValue(
of({ user: 'some_data' }) of({ user: 'some_data' })
); );
const updateAndPublishAuthStateSpy = vi.spyOn( const updateAndPublishAuthStateSpy = spyOn(
authStateService, authStateService,
'updateAndPublishAuthState' 'updateAndPublishAuthState'
); );
const resultCallbackContext = await firstValueFrom( service
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) .callbackUser(callbackContext, allConfigs[0], allConfigs)
); .subscribe((resultCallbackContext) => {
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({ expect(updateAndPublishAuthStateSpy).toHaveBeenCalledOnceWith({
isAuthenticated: true, isAuthenticated: true,
validationResult: ValidationResult.MaxOffsetExpired, validationResult: ValidationResult.MaxOffsetExpired,
isRenewProcess: false, isRenewProcess: false,
}); });
expect(resultCallbackContext).toEqual(callbackContext); expect(resultCallbackContext).toEqual(callbackContext);
}); });
}));
it('calls flowsDataService.setSessionState with correct params if user data is present and NOT refresh token', async () => { it('calls flowsDataService.setSessionState with correct params if user data is present and NOT refresh token', waitForAsync(() => {
const svr = new StateValidationResult( const svr = new StateValidationResult(
'accesstoken', 'accesstoken',
'idtoken', 'idtoken',
@ -330,22 +340,23 @@ describe('UserCallbackHandlerService', () => {
}, },
]; ];
vi.spyOn(userService, 'getAndPersistUserDataInStore').mockReturnValue( spyOn(userService, 'getAndPersistUserDataInStore').and.returnValue(
of({ user: 'some_data' }) of({ user: 'some_data' })
); );
const setSessionStateSpy = vi.spyOn(flowsDataService, 'setSessionState'); const setSessionStateSpy = spyOn(flowsDataService, 'setSessionState');
const resultCallbackContext = await firstValueFrom( service
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) .callbackUser(callbackContext, allConfigs[0], allConfigs)
); .subscribe((resultCallbackContext) => {
expect(setSessionStateSpy).toHaveBeenCalledExactlyOnceWith( expect(setSessionStateSpy).toHaveBeenCalledOnceWith(
'mystate', 'mystate',
allConfigs[0] allConfigs[0]
); );
expect(resultCallbackContext).toEqual(callbackContext); expect(resultCallbackContext).toEqual(callbackContext);
}); });
}));
it('calls authStateService.publishUnauthorizedState with correct params if user info which are coming back are null', async () => { it('calls authStateService.publishUnauthorizedState with correct params if user info which are coming back are null', waitForAsync(() => {
const svr = new StateValidationResult( const svr = new StateValidationResult(
'accesstoken', 'accesstoken',
'idtoken', 'idtoken',
@ -372,31 +383,31 @@ describe('UserCallbackHandlerService', () => {
}, },
]; ];
vi.spyOn(userService, 'getAndPersistUserDataInStore').mockReturnValue( spyOn(userService, 'getAndPersistUserDataInStore').and.returnValue(
of(null) of(null)
); );
const updateAndPublishAuthStateSpy = vi.spyOn( const updateAndPublishAuthStateSpy = spyOn(
authStateService, authStateService,
'updateAndPublishAuthState' 'updateAndPublishAuthState'
); );
try { service
await firstValueFrom( .callbackUser(callbackContext, allConfigs[0], allConfigs)
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) .subscribe({
); error: (err) => {
} catch (err: any) { expect(updateAndPublishAuthStateSpy).toHaveBeenCalledOnceWith({
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({ isAuthenticated: false,
isAuthenticated: false, validationResult: ValidationResult.MaxOffsetExpired,
validationResult: ValidationResult.MaxOffsetExpired, isRenewProcess: false,
isRenewProcess: false, });
expect(err.message).toEqual(
'Failed to retrieve user info with error: Error: Called for userData but they were null'
);
},
}); });
expect(err.message).toEqual( }));
'Failed to retrieve user info with error: Error: Called for userData but they were null'
);
}
});
it('calls resetAuthDataService.resetAuthorizationData if user info which are coming back are null', async () => { it('calls resetAuthDataService.resetAuthorizationData if user info which are coming back are null', waitForAsync(() => {
const svr = new StateValidationResult( const svr = new StateValidationResult(
'accesstoken', 'accesstoken',
'idtoken', 'idtoken',
@ -423,24 +434,24 @@ describe('UserCallbackHandlerService', () => {
}, },
]; ];
vi.spyOn(userService, 'getAndPersistUserDataInStore').mockReturnValue( spyOn(userService, 'getAndPersistUserDataInStore').and.returnValue(
of(null) of(null)
); );
const resetAuthorizationDataSpy = vi.spyOn( const resetAuthorizationDataSpy = spyOn(
resetAuthDataService, resetAuthDataService,
'resetAuthorizationData' 'resetAuthorizationData'
); );
try { service
await firstValueFrom( .callbackUser(callbackContext, allConfigs[0], allConfigs)
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) .subscribe({
); error: (err) => {
} catch (err: any) { expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1);
expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1); expect(err.message).toEqual(
expect(err.message).toEqual( 'Failed to retrieve user info with error: Error: Called for userData but they were null'
'Failed to retrieve user info with error: Error: Called for userData but they were null' );
); },
} });
}); }));
}); });
}); });

View File

@ -1,12 +1,12 @@
import { Injectable, inject } from 'injection-js'; import { inject, Injectable } from 'injection-js';
import { type Observable, of, throwError } from 'rxjs'; import { Observable, of, throwError } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators'; import { catchError, switchMap } from 'rxjs/operators';
import { AuthStateService } from '../../auth-state/auth-state.service'; import { AuthStateService } from '../../auth-state/auth-state.service';
import type { OpenIdConfiguration } from '../../config/openid-configuration'; import { OpenIdConfiguration } from '../../config/openid-configuration';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { UserService } from '../../user-data/user.service'; import { UserService } from '../../user-data/user.service';
import type { StateValidationResult } from '../../validation/state-validation-result'; import { StateValidationResult } from '../../validation/state-validation-result';
import type { CallbackContext } from '../callback-context'; import { CallbackContext } from '../callback-context';
import { FlowsDataService } from '../flows-data.service'; import { FlowsDataService } from '../flows-data.service';
import { ResetAuthDataService } from '../reset-auth-data.service'; import { ResetAuthDataService } from '../reset-auth-data.service';
@ -35,7 +35,6 @@ export class UserCallbackHandlerService {
if (!autoUserInfo) { if (!autoUserInfo) {
if (!isRenewProcess || renewUserInfoAfterTokenRenew) { if (!isRenewProcess || renewUserInfoAfterTokenRenew) {
// userData is set to the id_token decoded, auto get user data set to false // userData is set to the id_token decoded, auto get user data set to false
// biome-ignore lint/nursery/useCollapsedIf: <explanation>
if (validationResult?.decodedIdToken) { if (validationResult?.decodedIdToken) {
this.userService.setUserDataToStore( this.userService.setUserDataToStore(
validationResult.decodedIdToken, validationResult.decodedIdToken,
@ -67,7 +66,7 @@ export class UserCallbackHandlerService {
) )
.pipe( .pipe(
switchMap((userData) => { switchMap((userData) => {
if (userData) { if (!!userData) {
if (!refreshToken) { if (!refreshToken) {
this.flowsDataService.setSessionState( this.flowsDataService.setSessionState(
authResult?.session_state, authResult?.session_state,
@ -78,17 +77,18 @@ export class UserCallbackHandlerService {
this.publishAuthState(validationResult, isRenewProcess); this.publishAuthState(validationResult, isRenewProcess);
return of(callbackContext); return of(callbackContext);
} else {
this.resetAuthDataService.resetAuthorizationData(
configuration,
allConfigs
);
this.publishUnauthenticatedState(validationResult, isRenewProcess);
const errorMessage = `Called for userData but they were ${userData}`;
this.loggerService.logWarning(configuration, errorMessage);
return throwError(() => new Error(errorMessage));
} }
this.resetAuthDataService.resetAuthorizationData(
configuration,
allConfigs
);
this.publishUnauthenticatedState(validationResult, isRenewProcess);
const errorMessage = `Called for userData but they were ${userData}`;
this.loggerService.logWarning(configuration, errorMessage);
return throwError(() => new Error(errorMessage));
}), }),
catchError((err) => { catchError((err) => {
const errorMessage = `Failed to retrieve user info with error: ${err}`; const errorMessage = `Failed to retrieve user info with error: ${err}`;

View File

@ -1,8 +1,7 @@
import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { TestBed } from '@angular/core/testing';
import { vi } from 'vitest'; import { mockProvider } from '../../test/auto-mock';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
import { StoragePersistenceService } from '../storage/storage-persistence.service'; import { StoragePersistenceService } from '../storage/storage-persistence.service';
import { mockProvider } from '../testing/mock';
import { CryptoService } from '../utils/crypto/crypto.service'; import { CryptoService } from '../utils/crypto/crypto.service';
import { FlowsDataService } from './flows-data.service'; import { FlowsDataService } from './flows-data.service';
import { RandomService } from './random/random.service'; import { RandomService } from './random/random.service';
@ -21,13 +20,15 @@ describe('Flows Data Service', () => {
mockProvider(StoragePersistenceService), mockProvider(StoragePersistenceService),
], ],
}); });
});
beforeEach(() => {
service = TestBed.inject(FlowsDataService); service = TestBed.inject(FlowsDataService);
storagePersistenceService = TestBed.inject(StoragePersistenceService); storagePersistenceService = TestBed.inject(StoragePersistenceService);
}); });
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => { afterEach(() => {
vi.useRealTimers(); jasmine.clock().uninstall();
}); });
it('should create', () => { it('should create', () => {
@ -36,12 +37,12 @@ describe('Flows Data Service', () => {
describe('createNonce', () => { describe('createNonce', () => {
it('createNonce returns nonce and stores it', () => { it('createNonce returns nonce and stores it', () => {
const spy = vi.spyOn(storagePersistenceService, 'write'); const spy = spyOn(storagePersistenceService, 'write');
const result = service.createNonce({ configId: 'configId1' }); const result = service.createNonce({ configId: 'configId1' });
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(spy).toHaveBeenCalledExactlyOnceWith('authNonce', result, { expect(spy).toHaveBeenCalledOnceWith('authNonce', result, {
configId: 'configId1', configId: 'configId1',
}); });
}); });
@ -49,38 +50,32 @@ describe('Flows Data Service', () => {
describe('AuthStateControl', () => { describe('AuthStateControl', () => {
it('getAuthStateControl returns property from store', () => { it('getAuthStateControl returns property from store', () => {
const spy = vi.spyOn(storagePersistenceService, 'read'); const spy = spyOn(storagePersistenceService, 'read');
service.getAuthStateControl({ configId: 'configId1' }); service.getAuthStateControl({ configId: 'configId1' });
expect(spy).toHaveBeenCalledExactlyOnceWith('authStateControl', { expect(spy).toHaveBeenCalledOnceWith('authStateControl', {
configId: 'configId1', configId: 'configId1',
}); });
}); });
it('setAuthStateControl saves property in store', () => { it('setAuthStateControl saves property in store', () => {
const spy = vi.spyOn(storagePersistenceService, 'write'); const spy = spyOn(storagePersistenceService, 'write');
service.setAuthStateControl('ToSave', { configId: 'configId1' }); service.setAuthStateControl('ToSave', { configId: 'configId1' });
expect(spy).toHaveBeenCalledExactlyOnceWith( expect(spy).toHaveBeenCalledOnceWith('authStateControl', 'ToSave', {
'authStateControl', configId: 'configId1',
'ToSave', });
{
configId: 'configId1',
}
);
}); });
}); });
describe('getExistingOrCreateAuthStateControl', () => { describe('getExistingOrCreateAuthStateControl', () => {
it('if nothing stored it creates a 40 char one and saves the authStateControl', () => { it('if nothing stored it creates a 40 char one and saves the authStateControl', () => {
mockImplementationWhenArgsEqual( spyOn(storagePersistenceService, 'read')
vi.spyOn(storagePersistenceService, 'read'), .withArgs('authStateControl', { configId: 'configId1' })
['authStateControl', { configId: 'configId1' }], .and.returnValue(null);
() => null const setSpy = spyOn(storagePersistenceService, 'write');
);
const setSpy = vi.spyOn(storagePersistenceService, 'write');
const result = service.getExistingOrCreateAuthStateControl({ const result = service.getExistingOrCreateAuthStateControl({
configId: 'configId1', configId: 'configId1',
@ -88,22 +83,16 @@ describe('Flows Data Service', () => {
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(result.length).toBe(41); expect(result.length).toBe(41);
expect(setSpy).toHaveBeenCalledExactlyOnceWith( expect(setSpy).toHaveBeenCalledOnceWith('authStateControl', result, {
'authStateControl', configId: 'configId1',
result, });
{
configId: 'configId1',
}
);
}); });
it('if stored it returns the value and does NOT Store the value again', () => { it('if stored it returns the value and does NOT Store the value again', () => {
mockImplementationWhenArgsEqual( spyOn(storagePersistenceService, 'read')
vi.spyOn(storagePersistenceService, 'read'), .withArgs('authStateControl', { configId: 'configId1' })
['authStateControl', { configId: 'configId1' }], .and.returnValue('someAuthStateControl');
() => 'someAuthStateControl' const setSpy = spyOn(storagePersistenceService, 'write');
);
const setSpy = vi.spyOn(storagePersistenceService, 'write');
const result = service.getExistingOrCreateAuthStateControl({ const result = service.getExistingOrCreateAuthStateControl({
configId: 'configId1', configId: 'configId1',
@ -117,11 +106,11 @@ describe('Flows Data Service', () => {
describe('setSessionState', () => { describe('setSessionState', () => {
it('setSessionState saves the value in the storage', () => { it('setSessionState saves the value in the storage', () => {
const spy = vi.spyOn(storagePersistenceService, 'write'); const spy = spyOn(storagePersistenceService, 'write');
service.setSessionState('Genesis', { configId: 'configId1' }); service.setSessionState('Genesis', { configId: 'configId1' });
expect(spy).toHaveBeenCalledExactlyOnceWith('session_state', 'Genesis', { expect(spy).toHaveBeenCalledOnceWith('session_state', 'Genesis', {
configId: 'configId1', configId: 'configId1',
}); });
}); });
@ -129,7 +118,7 @@ describe('Flows Data Service', () => {
describe('resetStorageFlowData', () => { describe('resetStorageFlowData', () => {
it('resetStorageFlowData calls correct method on storagePersistenceService', () => { it('resetStorageFlowData calls correct method on storagePersistenceService', () => {
const spy = vi.spyOn(storagePersistenceService, 'resetStorageFlowData'); const spy = spyOn(storagePersistenceService, 'resetStorageFlowData');
service.resetStorageFlowData({ configId: 'configId1' }); service.resetStorageFlowData({ configId: 'configId1' });
@ -139,28 +128,26 @@ describe('Flows Data Service', () => {
describe('codeVerifier', () => { describe('codeVerifier', () => {
it('getCodeVerifier returns value from the store', () => { it('getCodeVerifier returns value from the store', () => {
const spy = mockImplementationWhenArgsEqual( const spy = spyOn(storagePersistenceService, 'read')
vi.spyOn(storagePersistenceService, 'read'), .withArgs('codeVerifier', { configId: 'configId1' })
['codeVerifier', { configId: 'configId1' }], .and.returnValue('Genesis');
() => 'Genesis'
);
const result = service.getCodeVerifier({ configId: 'configId1' }); const result = service.getCodeVerifier({ configId: 'configId1' });
expect(result).toBe('Genesis'); expect(result).toBe('Genesis');
expect(spy).toHaveBeenCalledExactlyOnceWith('codeVerifier', { expect(spy).toHaveBeenCalledOnceWith('codeVerifier', {
configId: 'configId1', configId: 'configId1',
}); });
}); });
it('createCodeVerifier returns random createCodeVerifier and stores it', () => { it('createCodeVerifier returns random createCodeVerifier and stores it', () => {
const setSpy = vi.spyOn(storagePersistenceService, 'write'); const setSpy = spyOn(storagePersistenceService, 'write');
const result = service.createCodeVerifier({ configId: 'configId1' }); const result = service.createCodeVerifier({ configId: 'configId1' });
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(result.length).toBe(67); expect(result.length).toBe(67);
expect(setSpy).toHaveBeenCalledExactlyOnceWith('codeVerifier', result, { expect(setSpy).toHaveBeenCalledOnceWith('codeVerifier', result, {
configId: 'configId1', configId: 'configId1',
}); });
}); });
@ -172,33 +159,28 @@ describe('Flows Data Service', () => {
configId: 'configId1', configId: 'configId1',
}; };
vi.useRealTimers(); jasmine.clock().uninstall();
vi.useFakeTimers(); jasmine.clock().install();
const baseTime = new Date(); const baseTime = new Date();
vi.setSystemTime(baseTime); jasmine.clock().mockDate(baseTime);
mockImplementationWhenArgsEqual( spyOn(storagePersistenceService, 'read')
vi.spyOn(storagePersistenceService, 'read'), .withArgs('storageCodeFlowInProgress', config)
['storageCodeFlowInProgress', config], .and.returnValue(true);
() => true const spyWrite = spyOn(storagePersistenceService, 'write');
);
const spyWrite = vi.spyOn(storagePersistenceService, 'write');
const isCodeFlowInProgressResult = service.isCodeFlowInProgress(config); const isCodeFlowInProgressResult = service.isCodeFlowInProgress(config);
expect(spyWrite).not.toHaveBeenCalled(); expect(spyWrite).not.toHaveBeenCalled();
expect(isCodeFlowInProgressResult).toBeTruthy(); expect(isCodeFlowInProgressResult).toBeTrue();
}); });
it('state object does not exist returns false result', () => { it('state object does not exist returns false result', () => {
// arrange // arrange
mockImplementationWhenArgsEqual( spyOn(storagePersistenceService, 'read')
vi.spyOn(storagePersistenceService, 'read'), .withArgs('storageCodeFlowInProgress', { configId: 'configId1' })
['storageCodeFlowInProgress', { configId: 'configId1' }], .and.returnValue(null);
() => null
);
// act // act
const isCodeFlowInProgressResult = service.isCodeFlowInProgress({ const isCodeFlowInProgressResult = service.isCodeFlowInProgress({
@ -206,83 +188,71 @@ describe('Flows Data Service', () => {
}); });
// assert // assert
expect(isCodeFlowInProgressResult).toBeFalsy(); expect(isCodeFlowInProgressResult).toBeFalse();
}); });
}); });
describe('setCodeFlowInProgress', () => { describe('setCodeFlowInProgress', () => {
it('set setCodeFlowInProgress to `in progress` when called', () => { it('set setCodeFlowInProgress to `in progress` when called', () => {
vi.useRealTimers(); jasmine.clock().uninstall();
vi.useFakeTimers(); jasmine.clock().install();
const baseTime = new Date(); const baseTime = new Date();
vi.setSystemTime(baseTime); jasmine.clock().mockDate(baseTime);
const spy = vi.spyOn(storagePersistenceService, 'write'); const spy = spyOn(storagePersistenceService, 'write');
service.setCodeFlowInProgress({ configId: 'configId1' }); service.setCodeFlowInProgress({ configId: 'configId1' });
expect(spy).toHaveBeenCalledExactlyOnceWith( expect(spy).toHaveBeenCalledOnceWith('storageCodeFlowInProgress', true, {
'storageCodeFlowInProgress', configId: 'configId1',
true, });
{
configId: 'configId1',
}
);
}); });
}); });
describe('resetCodeFlowInProgress', () => { describe('resetCodeFlowInProgress', () => {
it('set resetCodeFlowInProgress to false when called', () => { it('set resetCodeFlowInProgress to false when called', () => {
const spy = vi.spyOn(storagePersistenceService, 'write'); const spy = spyOn(storagePersistenceService, 'write');
service.resetCodeFlowInProgress({ configId: 'configId1' }); service.resetCodeFlowInProgress({ configId: 'configId1' });
expect(spy).toHaveBeenCalledExactlyOnceWith( expect(spy).toHaveBeenCalledOnceWith('storageCodeFlowInProgress', false, {
'storageCodeFlowInProgress', configId: 'configId1',
false, });
{
configId: 'configId1',
}
);
}); });
}); });
describe('isSilentRenewRunning', () => { describe('isSilentRenewRunning', () => {
it('silent renew process timeout exceeded reset state object and returns false result', async () => { it('silent renew process timeout exceeded reset state object and returns false result', () => {
const config = { const config = {
silentRenewTimeoutInSeconds: 10, silentRenewTimeoutInSeconds: 10,
configId: 'configId1', configId: 'configId1',
}; };
vi.useRealTimers(); jasmine.clock().uninstall();
jasmine.clock().install();
const baseTime = new Date(); const baseTime = new Date();
vi.useFakeTimers();
vi.setSystemTime(baseTime); jasmine.clock().mockDate(baseTime);
const storageObject = { const storageObject = {
state: 'running', state: 'running',
dateOfLaunchedProcessUtc: baseTime.toISOString(), dateOfLaunchedProcessUtc: baseTime.toISOString(),
}; };
mockImplementationWhenArgsEqual( spyOn(storagePersistenceService, 'read')
vi.spyOn(storagePersistenceService, 'read'), .withArgs('storageSilentRenewRunning', config)
['storageSilentRenewRunning', config], .and.returnValue(JSON.stringify(storageObject));
() => JSON.stringify(storageObject) const spyWrite = spyOn(storagePersistenceService, 'write');
);
const spyWrite = vi.spyOn(storagePersistenceService, 'write');
await vi.advanceTimersByTimeAsync( jasmine.clock().tick((config.silentRenewTimeoutInSeconds + 1) * 1000);
(config.silentRenewTimeoutInSeconds + 1) * 1000
);
const isSilentRenewRunningResult = service.isSilentRenewRunning(config); const isSilentRenewRunningResult = service.isSilentRenewRunning(config);
expect(spyWrite).toHaveBeenCalledExactlyOnceWith( expect(spyWrite).toHaveBeenCalledOnceWith(
'storageSilentRenewRunning', 'storageSilentRenewRunning',
'', '',
config config
); );
expect(isSilentRenewRunningResult).toBeFalsy(); expect(isSilentRenewRunningResult).toBeFalse();
}); });
it('checks silent renew process and returns result', () => { it('checks silent renew process and returns result', () => {
@ -291,62 +261,58 @@ describe('Flows Data Service', () => {
configId: 'configId1', configId: 'configId1',
}; };
vi.useRealTimers(); jasmine.clock().uninstall();
vi.useFakeTimers(); jasmine.clock().install();
const baseTime = new Date(); const baseTime = new Date();
vi.setSystemTime(baseTime); jasmine.clock().mockDate(baseTime);
const storageObject = { const storageObject = {
state: 'running', state: 'running',
dateOfLaunchedProcessUtc: baseTime.toISOString(), dateOfLaunchedProcessUtc: baseTime.toISOString(),
}; };
mockImplementationWhenArgsEqual( spyOn(storagePersistenceService, 'read')
vi.spyOn(storagePersistenceService, 'read'), .withArgs('storageSilentRenewRunning', config)
['storageSilentRenewRunning', config], .and.returnValue(JSON.stringify(storageObject));
() => JSON.stringify(storageObject) const spyWrite = spyOn(storagePersistenceService, 'write');
);
const spyWrite = vi.spyOn(storagePersistenceService, 'write');
const isSilentRenewRunningResult = service.isSilentRenewRunning(config); const isSilentRenewRunningResult = service.isSilentRenewRunning(config);
expect(spyWrite).not.toHaveBeenCalled(); expect(spyWrite).not.toHaveBeenCalled();
expect(isSilentRenewRunningResult).toBeTruthy(); expect(isSilentRenewRunningResult).toBeTrue();
}); });
it('state object does not exist returns false result', () => { it('state object does not exist returns false result', () => {
mockImplementationWhenArgsEqual( spyOn(storagePersistenceService, 'read')
vi.spyOn(storagePersistenceService, 'read'), .withArgs('storageSilentRenewRunning', { configId: 'configId1' })
['storageSilentRenewRunning', { configId: 'configId1' }], .and.returnValue(null);
() => null
);
const isSilentRenewRunningResult = service.isSilentRenewRunning({ const isSilentRenewRunningResult = service.isSilentRenewRunning({
configId: 'configId1', configId: 'configId1',
}); });
expect(isSilentRenewRunningResult).toBeFalsy(); expect(isSilentRenewRunningResult).toBeFalse();
}); });
}); });
describe('setSilentRenewRunning', () => { describe('setSilentRenewRunning', () => {
it('set setSilentRenewRunning to `running` with lauched time when called', () => { it('set setSilentRenewRunning to `running` with lauched time when called', () => {
vi.useRealTimers(); jasmine.clock().uninstall();
vi.useFakeTimers(); jasmine.clock().install();
const baseTime = new Date(); const baseTime = new Date();
vi.setSystemTime(baseTime); jasmine.clock().mockDate(baseTime);
const storageObject = { const storageObject = {
state: 'running', state: 'running',
dateOfLaunchedProcessUtc: baseTime.toISOString(), dateOfLaunchedProcessUtc: baseTime.toISOString(),
}; };
const spy = vi.spyOn(storagePersistenceService, 'write'); const spy = spyOn(storagePersistenceService, 'write');
service.setSilentRenewRunning({ configId: 'configId1' }); service.setSilentRenewRunning({ configId: 'configId1' });
expect(spy).toHaveBeenCalledExactlyOnceWith( expect(spy).toHaveBeenCalledOnceWith(
'storageSilentRenewRunning', 'storageSilentRenewRunning',
JSON.stringify(storageObject), JSON.stringify(storageObject),
{ configId: 'configId1' } { configId: 'configId1' }
@ -356,16 +322,12 @@ describe('Flows Data Service', () => {
describe('resetSilentRenewRunning', () => { describe('resetSilentRenewRunning', () => {
it('set resetSilentRenewRunning to empty string when called', () => { it('set resetSilentRenewRunning to empty string when called', () => {
const spy = vi.spyOn(storagePersistenceService, 'write'); const spy = spyOn(storagePersistenceService, 'write');
service.resetSilentRenewRunning({ configId: 'configId1' }); service.resetSilentRenewRunning({ configId: 'configId1' });
expect(spy).toHaveBeenCalledExactlyOnceWith( expect(spy).toHaveBeenCalledOnceWith('storageSilentRenewRunning', '', {
'storageSilentRenewRunning', configId: 'configId1',
'', });
{
configId: 'configId1',
}
);
}); });
}); });
}); });

View File

@ -1,8 +1,8 @@
import { Injectable, inject } from 'injection-js'; import { inject, Injectable } from 'injection-js';
import type { OpenIdConfiguration } from '../config/openid-configuration'; import { OpenIdConfiguration } from '../config/openid-configuration';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
import { StoragePersistenceService } from '../storage/storage-persistence.service'; import { StoragePersistenceService } from '../storage/storage-persistence.service';
import type { SilentRenewRunning } from './flows.models'; import { SilentRenewRunning } from './flows.models';
import { RandomService } from './random/random.service'; import { RandomService } from './random/random.service';
@Injectable() @Injectable()
@ -18,7 +18,7 @@ export class FlowsDataService {
createNonce(configuration: OpenIdConfiguration): string { createNonce(configuration: OpenIdConfiguration): string {
const nonce = this.randomService.createRandom(40, configuration); const nonce = this.randomService.createRandom(40, configuration);
this.loggerService.logDebug(configuration, `Nonce created. nonce:${nonce}`); this.loggerService.logDebug(configuration, 'Nonce created. nonce:' + nonce);
this.setNonce(nonce, configuration); this.setNonce(nonce, configuration);
return nonce; return nonce;

View File

@ -1,8 +1,7 @@
import { TestBed } from '@/testing'; import { TestBed, waitForAsync } from '@angular/core/testing';
import { firstValueFrom, of } from 'rxjs'; import { of } from 'rxjs';
import { vi } from 'vitest'; import { mockProvider } from '../../test/auto-mock';
import { mockProvider } from '../testing/mock'; import { CallbackContext } from './callback-context';
import type { CallbackContext } from './callback-context';
import { CodeFlowCallbackHandlerService } from './callback-handling/code-flow-callback-handler.service'; import { CodeFlowCallbackHandlerService } from './callback-handling/code-flow-callback-handler.service';
import { HistoryJwtKeysCallbackHandlerService } from './callback-handling/history-jwt-keys-callback-handler.service'; import { HistoryJwtKeysCallbackHandlerService } from './callback-handling/history-jwt-keys-callback-handler.service';
import { ImplicitFlowCallbackHandlerService } from './callback-handling/implicit-flow-callback-handler.service'; import { ImplicitFlowCallbackHandlerService } from './callback-handling/implicit-flow-callback-handler.service';
@ -35,6 +34,9 @@ describe('Flows Service', () => {
mockProvider(RefreshTokenCallbackHandlerService), mockProvider(RefreshTokenCallbackHandlerService),
], ],
}); });
});
beforeEach(() => {
service = TestBed.inject(FlowsService); service = TestBed.inject(FlowsService);
codeFlowCallbackHandlerService = TestBed.inject( codeFlowCallbackHandlerService = TestBed.inject(
CodeFlowCallbackHandlerService CodeFlowCallbackHandlerService
@ -62,164 +64,163 @@ describe('Flows Service', () => {
}); });
describe('processCodeFlowCallback', () => { describe('processCodeFlowCallback', () => {
it('calls all methods correctly', async () => { it('calls all methods correctly', waitForAsync(() => {
const codeFlowCallbackSpy = vi const codeFlowCallbackSpy = spyOn(
.spyOn(codeFlowCallbackHandlerService, 'codeFlowCallback') codeFlowCallbackHandlerService,
.mockReturnValue(of({} as CallbackContext)); 'codeFlowCallback'
const codeFlowCodeRequestSpy = vi ).and.returnValue(of({} as CallbackContext));
.spyOn(codeFlowCallbackHandlerService, 'codeFlowCodeRequest') const codeFlowCodeRequestSpy = spyOn(
.mockReturnValue(of({} as CallbackContext)); codeFlowCallbackHandlerService,
const callbackHistoryAndResetJwtKeysSpy = vi 'codeFlowCodeRequest'
.spyOn( ).and.returnValue(of({} as CallbackContext));
historyJwtKeysCallbackHandlerService, const callbackHistoryAndResetJwtKeysSpy = spyOn(
'callbackHistoryAndResetJwtKeys' historyJwtKeysCallbackHandlerService,
) 'callbackHistoryAndResetJwtKeys'
.mockReturnValue(of({} as CallbackContext)); ).and.returnValue(of({} as CallbackContext));
const callbackStateValidationSpy = vi const callbackStateValidationSpy = spyOn(
.spyOn(stateValidationCallbackHandlerService, 'callbackStateValidation') stateValidationCallbackHandlerService,
.mockReturnValue(of({} as CallbackContext)); 'callbackStateValidation'
const callbackUserSpy = vi ).and.returnValue(of({} as CallbackContext));
.spyOn(userCallbackHandlerService, 'callbackUser') const callbackUserSpy = spyOn(
.mockReturnValue(of({} as CallbackContext)); userCallbackHandlerService,
'callbackUser'
).and.returnValue(of({} as CallbackContext));
const allConfigs = [ const allConfigs = [
{ {
configId: 'configId1', configId: 'configId1',
}, },
]; ];
const value = await firstValueFrom( service
service.processCodeFlowCallback( .processCodeFlowCallback('some-url1234', allConfigs[0], allConfigs)
'some-url1234', .subscribe((value) => {
allConfigs[0]!, expect(value).toEqual({} as CallbackContext);
allConfigs expect(codeFlowCallbackSpy).toHaveBeenCalledOnceWith(
) 'some-url1234',
); allConfigs[0]
expect(value).toEqual({} as CallbackContext); );
expect(codeFlowCallbackSpy).toHaveBeenCalledExactlyOnceWith( expect(codeFlowCodeRequestSpy).toHaveBeenCalledTimes(1);
'some-url1234', expect(callbackHistoryAndResetJwtKeysSpy).toHaveBeenCalledTimes(1);
allConfigs[0] expect(callbackStateValidationSpy).toHaveBeenCalledTimes(1);
); expect(callbackUserSpy).toHaveBeenCalledTimes(1);
expect(codeFlowCodeRequestSpy).toHaveBeenCalledTimes(1); });
expect(callbackHistoryAndResetJwtKeysSpy).toHaveBeenCalledTimes(1); }));
expect(callbackStateValidationSpy).toHaveBeenCalledTimes(1);
expect(callbackUserSpy).toHaveBeenCalledTimes(1);
});
}); });
describe('processSilentRenewCodeFlowCallback', () => { describe('processSilentRenewCodeFlowCallback', () => {
it('calls all methods correctly', async () => { it('calls all methods correctly', waitForAsync(() => {
const codeFlowCodeRequestSpy = vi const codeFlowCodeRequestSpy = spyOn(
.spyOn(codeFlowCallbackHandlerService, 'codeFlowCodeRequest') codeFlowCallbackHandlerService,
.mockReturnValue(of({} as CallbackContext)); 'codeFlowCodeRequest'
const callbackHistoryAndResetJwtKeysSpy = vi ).and.returnValue(of({} as CallbackContext));
.spyOn( const callbackHistoryAndResetJwtKeysSpy = spyOn(
historyJwtKeysCallbackHandlerService, historyJwtKeysCallbackHandlerService,
'callbackHistoryAndResetJwtKeys' 'callbackHistoryAndResetJwtKeys'
) ).and.returnValue(of({} as CallbackContext));
.mockReturnValue(of({} as CallbackContext)); const callbackStateValidationSpy = spyOn(
const callbackStateValidationSpy = vi stateValidationCallbackHandlerService,
.spyOn(stateValidationCallbackHandlerService, 'callbackStateValidation') 'callbackStateValidation'
.mockReturnValue(of({} as CallbackContext)); ).and.returnValue(of({} as CallbackContext));
const callbackUserSpy = vi const callbackUserSpy = spyOn(
.spyOn(userCallbackHandlerService, 'callbackUser') userCallbackHandlerService,
.mockReturnValue(of({} as CallbackContext)); 'callbackUser'
).and.returnValue(of({} as CallbackContext));
const allConfigs = [ const allConfigs = [
{ {
configId: 'configId1', configId: 'configId1',
}, },
]; ];
const value = await firstValueFrom( service
service.processSilentRenewCodeFlowCallback( .processSilentRenewCodeFlowCallback(
{} as CallbackContext, {} as CallbackContext,
allConfigs[0]!, allConfigs[0],
allConfigs allConfigs
) )
); .subscribe((value) => {
expect(value).toEqual({} as CallbackContext); expect(value).toEqual({} as CallbackContext);
expect(codeFlowCodeRequestSpy).toHaveBeenCalled(); expect(codeFlowCodeRequestSpy).toHaveBeenCalled();
expect(callbackHistoryAndResetJwtKeysSpy).toHaveBeenCalled(); expect(callbackHistoryAndResetJwtKeysSpy).toHaveBeenCalled();
expect(callbackStateValidationSpy).toHaveBeenCalled(); expect(callbackStateValidationSpy).toHaveBeenCalled();
expect(callbackUserSpy).toHaveBeenCalled(); expect(callbackUserSpy).toHaveBeenCalled();
}); });
}));
}); });
describe('processImplicitFlowCallback', () => { describe('processImplicitFlowCallback', () => {
it('calls all methods correctly', async () => { it('calls all methods correctly', waitForAsync(() => {
const implicitFlowCallbackSpy = vi const implicitFlowCallbackSpy = spyOn(
.spyOn(implicitFlowCallbackHandlerService, 'implicitFlowCallback') implicitFlowCallbackHandlerService,
.mockReturnValue(of({} as CallbackContext)); 'implicitFlowCallback'
const callbackHistoryAndResetJwtKeysSpy = vi ).and.returnValue(of({} as CallbackContext));
.spyOn( const callbackHistoryAndResetJwtKeysSpy = spyOn(
historyJwtKeysCallbackHandlerService, historyJwtKeysCallbackHandlerService,
'callbackHistoryAndResetJwtKeys' 'callbackHistoryAndResetJwtKeys'
) ).and.returnValue(of({} as CallbackContext));
.mockReturnValue(of({} as CallbackContext)); const callbackStateValidationSpy = spyOn(
const callbackStateValidationSpy = vi stateValidationCallbackHandlerService,
.spyOn(stateValidationCallbackHandlerService, 'callbackStateValidation') 'callbackStateValidation'
.mockReturnValue(of({} as CallbackContext)); ).and.returnValue(of({} as CallbackContext));
const callbackUserSpy = vi const callbackUserSpy = spyOn(
.spyOn(userCallbackHandlerService, 'callbackUser') userCallbackHandlerService,
.mockReturnValue(of({} as CallbackContext)); 'callbackUser'
).and.returnValue(of({} as CallbackContext));
const allConfigs = [ const allConfigs = [
{ {
configId: 'configId1', configId: 'configId1',
}, },
]; ];
const value = await firstValueFrom( service
service.processImplicitFlowCallback( .processImplicitFlowCallback(allConfigs[0], allConfigs, 'any-hash')
allConfigs[0]!, .subscribe((value) => {
allConfigs, expect(value).toEqual({} as CallbackContext);
'any-hash' expect(implicitFlowCallbackSpy).toHaveBeenCalled();
) expect(callbackHistoryAndResetJwtKeysSpy).toHaveBeenCalled();
); expect(callbackStateValidationSpy).toHaveBeenCalled();
expect(value).toEqual({} as CallbackContext); expect(callbackUserSpy).toHaveBeenCalled();
expect(implicitFlowCallbackSpy).toHaveBeenCalled(); });
expect(callbackHistoryAndResetJwtKeysSpy).toHaveBeenCalled(); }));
expect(callbackStateValidationSpy).toHaveBeenCalled();
expect(callbackUserSpy).toHaveBeenCalled();
});
}); });
describe('processRefreshToken', () => { describe('processRefreshToken', () => {
it('calls all methods correctly', async () => { it('calls all methods correctly', waitForAsync(() => {
const refreshSessionWithRefreshTokensSpy = vi const refreshSessionWithRefreshTokensSpy = spyOn(
.spyOn( refreshSessionCallbackHandlerService,
refreshSessionCallbackHandlerService, 'refreshSessionWithRefreshTokens'
'refreshSessionWithRefreshTokens' ).and.returnValue(of({} as CallbackContext));
) const refreshTokensRequestTokensSpy = spyOn(
.mockReturnValue(of({} as CallbackContext)); refreshTokenCallbackHandlerService,
const refreshTokensRequestTokensSpy = vi 'refreshTokensRequestTokens'
.spyOn(refreshTokenCallbackHandlerService, 'refreshTokensRequestTokens') ).and.returnValue(of({} as CallbackContext));
.mockReturnValue(of({} as CallbackContext)); const callbackHistoryAndResetJwtKeysSpy = spyOn(
const callbackHistoryAndResetJwtKeysSpy = vi historyJwtKeysCallbackHandlerService,
.spyOn( 'callbackHistoryAndResetJwtKeys'
historyJwtKeysCallbackHandlerService, ).and.returnValue(of({} as CallbackContext));
'callbackHistoryAndResetJwtKeys' const callbackStateValidationSpy = spyOn(
) stateValidationCallbackHandlerService,
.mockReturnValue(of({} as CallbackContext)); 'callbackStateValidation'
const callbackStateValidationSpy = vi ).and.returnValue(of({} as CallbackContext));
.spyOn(stateValidationCallbackHandlerService, 'callbackStateValidation') const callbackUserSpy = spyOn(
.mockReturnValue(of({} as CallbackContext)); userCallbackHandlerService,
const callbackUserSpy = vi 'callbackUser'
.spyOn(userCallbackHandlerService, 'callbackUser') ).and.returnValue(of({} as CallbackContext));
.mockReturnValue(of({} as CallbackContext));
const allConfigs = [ const allConfigs = [
{ {
configId: 'configId1', configId: 'configId1',
}, },
]; ];
const value = await firstValueFrom( service
service.processRefreshToken(allConfigs[0]!, allConfigs) .processRefreshToken(allConfigs[0], allConfigs)
); .subscribe((value) => {
expect(value).toEqual({} as CallbackContext); expect(value).toEqual({} as CallbackContext);
expect(refreshSessionWithRefreshTokensSpy).toHaveBeenCalled(); expect(refreshSessionWithRefreshTokensSpy).toHaveBeenCalled();
expect(refreshTokensRequestTokensSpy).toHaveBeenCalled(); expect(refreshTokensRequestTokensSpy).toHaveBeenCalled();
expect(callbackHistoryAndResetJwtKeysSpy).toHaveBeenCalled(); expect(callbackHistoryAndResetJwtKeysSpy).toHaveBeenCalled();
expect(callbackStateValidationSpy).toHaveBeenCalled(); expect(callbackStateValidationSpy).toHaveBeenCalled();
expect(callbackUserSpy).toHaveBeenCalled(); expect(callbackUserSpy).toHaveBeenCalled();
}); });
}));
}); });
}); });

View File

@ -1,8 +1,8 @@
import { Injectable, inject } from 'injection-js'; import { inject, Injectable } from 'injection-js';
import type { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { concatMap } from 'rxjs/operators'; import { concatMap } from 'rxjs/operators';
import type { OpenIdConfiguration } from '../config/openid-configuration'; import { OpenIdConfiguration } from '../config/openid-configuration';
import type { CallbackContext } from './callback-context'; import { CallbackContext } from './callback-context';
import { CodeFlowCallbackHandlerService } from './callback-handling/code-flow-callback-handler.service'; import { CodeFlowCallbackHandlerService } from './callback-handling/code-flow-callback-handler.service';
import { HistoryJwtKeysCallbackHandlerService } from './callback-handling/history-jwt-keys-callback-handler.service'; import { HistoryJwtKeysCallbackHandlerService } from './callback-handling/history-jwt-keys-callback-handler.service';
import { ImplicitFlowCallbackHandlerService } from './callback-handling/implicit-flow-callback-handler.service'; import { ImplicitFlowCallbackHandlerService } from './callback-handling/implicit-flow-callback-handler.service';

View File

@ -1,6 +1,6 @@
import { TestBed } from '@/testing'; import { TestBed } from '@angular/core/testing';
import { mockProvider } from '../../../test/auto-mock';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { mockProvider } from '../../testing/mock';
import { CryptoService } from '../../utils/crypto/crypto.service'; import { CryptoService } from '../../utils/crypto/crypto.service';
import { RandomService } from './random.service'; import { RandomService } from './random.service';
@ -11,6 +11,9 @@ describe('RandomService Tests', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [RandomService, mockProvider(LoggerService), CryptoService], providers: [RandomService, mockProvider(LoggerService), CryptoService],
}); });
});
beforeEach(() => {
randomService = TestBed.inject(RandomService); randomService = TestBed.inject(RandomService);
}); });

View File

@ -1,5 +1,5 @@
import { Injectable, inject } from 'injection-js'; import { inject, Injectable } from 'injection-js';
import type { OpenIdConfiguration } from '../../config/openid-configuration'; import { OpenIdConfiguration } from '../../config/openid-configuration';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { CryptoService } from '../../utils/crypto/crypto.service'; import { CryptoService } from '../../utils/crypto/crypto.service';
@ -37,7 +37,7 @@ export class RandomService {
} }
private toHex(dec: number): string { private toHex(dec: number): string {
return `0${dec.toString(16)}`.substr(-2); return ('0' + dec.toString(16)).substr(-2);
} }
private randomString(length: number): string { private randomString(length: number): string {

View File

@ -1,8 +1,7 @@
import { TestBed } from '@/testing'; import { TestBed } from '@angular/core/testing';
import { vi } from 'vitest'; import { mockProvider } from '../../test/auto-mock';
import { AuthStateService } from '../auth-state/auth-state.service'; import { AuthStateService } from '../auth-state/auth-state.service';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
import { mockProvider } from '../testing/mock';
import { UserService } from '../user-data/user.service'; import { UserService } from '../user-data/user.service';
import { FlowsDataService } from './flows-data.service'; import { FlowsDataService } from './flows-data.service';
import { ResetAuthDataService } from './reset-auth-data.service'; import { ResetAuthDataService } from './reset-auth-data.service';
@ -23,6 +22,9 @@ describe('ResetAuthDataService', () => {
mockProvider(LoggerService), mockProvider(LoggerService),
], ],
}); });
});
beforeEach(() => {
service = TestBed.inject(ResetAuthDataService); service = TestBed.inject(ResetAuthDataService);
userService = TestBed.inject(UserService); userService = TestBed.inject(UserService);
flowsDataService = TestBed.inject(FlowsDataService); flowsDataService = TestBed.inject(FlowsDataService);
@ -35,7 +37,7 @@ describe('ResetAuthDataService', () => {
describe('resetAuthorizationData', () => { describe('resetAuthorizationData', () => {
it('calls resetUserDataInStore when autoUserInfo is true', () => { it('calls resetUserDataInStore when autoUserInfo is true', () => {
const resetUserDataInStoreSpy = vi.spyOn( const resetUserDataInStoreSpy = spyOn(
userService, userService,
'resetUserDataInStore' 'resetUserDataInStore'
); );
@ -45,16 +47,16 @@ describe('ResetAuthDataService', () => {
}, },
]; ];
service.resetAuthorizationData(allConfigs[0]!, allConfigs); service.resetAuthorizationData(allConfigs[0], allConfigs);
expect(resetUserDataInStoreSpy).toHaveBeenCalled(); expect(resetUserDataInStoreSpy).toHaveBeenCalled();
}); });
it('calls correct methods', () => { it('calls correct methods', () => {
const resetStorageFlowDataSpy = vi.spyOn( const resetStorageFlowDataSpy = spyOn(
flowsDataService, flowsDataService,
'resetStorageFlowData' 'resetStorageFlowData'
); );
const setUnauthorizedAndFireEventSpy = vi.spyOn( const setUnauthorizedAndFireEventSpy = spyOn(
authStateService, authStateService,
'setUnauthenticatedAndFireEvent' 'setUnauthenticatedAndFireEvent'
); );
@ -64,7 +66,7 @@ describe('ResetAuthDataService', () => {
}, },
]; ];
service.resetAuthorizationData(allConfigs[0]!, allConfigs); service.resetAuthorizationData(allConfigs[0], allConfigs);
expect(resetStorageFlowDataSpy).toHaveBeenCalled(); expect(resetStorageFlowDataSpy).toHaveBeenCalled();
expect(setUnauthorizedAndFireEventSpy).toHaveBeenCalled(); expect(setUnauthorizedAndFireEventSpy).toHaveBeenCalled();

View File

@ -1,6 +1,6 @@
import { Injectable, inject } from 'injection-js'; import { inject, Injectable } from 'injection-js';
import { AuthStateService } from '../auth-state/auth-state.service'; import { AuthStateService } from '../auth-state/auth-state.service';
import type { OpenIdConfiguration } from '../config/openid-configuration'; import { OpenIdConfiguration } from '../config/openid-configuration';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
import { UserService } from '../user-data/user.service'; import { UserService } from '../user-data/user.service';
import { FlowsDataService } from './flows-data.service'; import { FlowsDataService } from './flows-data.service';

View File

@ -1,12 +1,11 @@
import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { HttpResponse } from '@angular/common/http';
import { HttpResponse } from '@ngify/http'; import { TestBed, waitForAsync } from '@angular/core/testing';
import { EmptyError, firstValueFrom, isObservable, of, throwError } from 'rxjs'; import { isObservable, of, throwError } from 'rxjs';
import { vi } from 'vitest'; import { mockProvider } from '../../test/auto-mock';
import { createRetriableStream } from '../../test/create-retriable-stream.helper';
import { DataService } from '../api/data.service'; import { DataService } from '../api/data.service';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
import { StoragePersistenceService } from '../storage/storage-persistence.service'; import { StoragePersistenceService } from '../storage/storage-persistence.service';
import { createRetriableStream } from '../testing/create-retriable-stream.helper';
import { mockProvider } from '../testing/mock';
import { SigninKeyDataService } from './signin-key-data.service'; import { SigninKeyDataService } from './signin-key-data.service';
const DUMMY_JWKS = { const DUMMY_JWKS = {
@ -40,6 +39,9 @@ describe('Signin Key Data Service', () => {
mockProvider(StoragePersistenceService), mockProvider(StoragePersistenceService),
], ],
}); });
});
beforeEach(() => {
service = TestBed.inject(SigninKeyDataService); service = TestBed.inject(SigninKeyDataService);
storagePersistenceService = TestBed.inject(StoragePersistenceService); storagePersistenceService = TestBed.inject(StoragePersistenceService);
dataService = TestBed.inject(DataService); dataService = TestBed.inject(DataService);
@ -51,84 +53,73 @@ describe('Signin Key Data Service', () => {
}); });
describe('getSigningKeys', () => { describe('getSigningKeys', () => {
it('throws error when no wellKnownEndpoints given', async () => { it('throws error when no wellKnownEndpoints given', waitForAsync(() => {
mockImplementationWhenArgsEqual( spyOn(storagePersistenceService, 'read')
vi.spyOn(storagePersistenceService, 'read'), .withArgs('authWellKnownEndPoints', { configId: 'configId1' })
['authWellKnownEndPoints', { configId: 'configId1' }], .and.returnValue(null);
() => null
);
const result = service.getSigningKeys({ configId: 'configId1' }); const result = service.getSigningKeys({ configId: 'configId1' });
try { result.subscribe({
await firstValueFrom(result); error: (err) => {
} catch (err: any) { expect(err).toBeTruthy();
expect(err).toBeTruthy(); },
} });
}); }));
it('throws error when no jwksUri given', async () => { it('throws error when no jwksUri given', waitForAsync(() => {
mockImplementationWhenArgsEqual( spyOn(storagePersistenceService, 'read')
vi.spyOn(storagePersistenceService, 'read'), .withArgs('authWellKnownEndPoints', { configId: 'configId1' })
['authWellKnownEndPoints', { configId: 'configId1' }], .and.returnValue({ jwksUri: null });
() => ({ jwksUri: null })
);
const result = service.getSigningKeys({ configId: 'configId1' }); const result = service.getSigningKeys({ configId: 'configId1' });
try { result.subscribe({
await firstValueFrom(result); error: (err) => {
} catch (err: any) { expect(err).toBeTruthy();
expect(err).toBeTruthy(); },
} });
}); }));
it('calls dataservice if jwksurl is given', async () => { it('calls dataservice if jwksurl is given', waitForAsync(() => {
mockImplementationWhenArgsEqual( spyOn(storagePersistenceService, 'read')
vi.spyOn(storagePersistenceService, 'read'), .withArgs('authWellKnownEndPoints', { configId: 'configId1' })
['authWellKnownEndPoints', { configId: 'configId1' }], .and.returnValue({ jwksUri: 'someUrl' });
() => ({ jwksUri: 'someUrl' }) const spy = spyOn(dataService, 'get').and.callFake(() => of());
);
const spy = vi.spyOn(dataService, 'get').mockImplementation(() => of());
const result = service.getSigningKeys({ configId: 'configId1' }); const result = service.getSigningKeys({ configId: 'configId1' });
try { result.subscribe({
await firstValueFrom(result); complete: () => {
} catch (err: any) { expect(spy).toHaveBeenCalledOnceWith('someUrl', {
if (err instanceof EmptyError) {
expect(spy).toHaveBeenCalledExactlyOnceWith('someUrl', {
configId: 'configId1', configId: 'configId1',
}); });
} },
} });
}); }));
it('should retry once', async () => { it('should retry once', waitForAsync(() => {
mockImplementationWhenArgsEqual( spyOn(storagePersistenceService, 'read')
vi.spyOn(storagePersistenceService, 'read'), .withArgs('authWellKnownEndPoints', { configId: 'configId1' })
['authWellKnownEndPoints', { configId: 'configId1' }], .and.returnValue({ jwksUri: 'someUrl' });
() => ({ jwksUri: 'someUrl' }) spyOn(dataService, 'get').and.returnValue(
);
vi.spyOn(dataService, 'get').mockReturnValue(
createRetriableStream( createRetriableStream(
throwError(() => new Error('Error')), throwError(() => new Error('Error')),
of(DUMMY_JWKS) of(DUMMY_JWKS)
) )
); );
const res = await firstValueFrom( service.getSigningKeys({ configId: 'configId1' }).subscribe({
service.getSigningKeys({ configId: 'configId1' }) next: (res) => {
); expect(res).toBeTruthy();
expect(res).toBeTruthy(); expect(res).toEqual(DUMMY_JWKS);
expect(res).toEqual(DUMMY_JWKS); },
}); });
}));
it('should retry twice', async () => { it('should retry twice', waitForAsync(() => {
mockImplementationWhenArgsEqual( spyOn(storagePersistenceService, 'read')
vi.spyOn(storagePersistenceService, 'read'), .withArgs('authWellKnownEndPoints', { configId: 'configId1' })
['authWellKnownEndPoints', { configId: 'configId1' }], .and.returnValue({ jwksUri: 'someUrl' });
() => ({ jwksUri: 'someUrl' }) spyOn(dataService, 'get').and.returnValue(
);
vi.spyOn(dataService, 'get').mockReturnValue(
createRetriableStream( createRetriableStream(
throwError(() => new Error('Error')), throwError(() => new Error('Error')),
throwError(() => new Error('Error')), throwError(() => new Error('Error')),
@ -136,20 +127,19 @@ describe('Signin Key Data Service', () => {
) )
); );
const res = await firstValueFrom( service.getSigningKeys({ configId: 'configId1' }).subscribe({
service.getSigningKeys({ configId: 'configId1' }) next: (res) => {
); expect(res).toBeTruthy();
expect(res).toBeTruthy(); expect(res).toEqual(DUMMY_JWKS);
expect(res).toEqual(DUMMY_JWKS); },
}); });
}));
it('should fail after three tries', async () => { it('should fail after three tries', waitForAsync(() => {
mockImplementationWhenArgsEqual( spyOn(storagePersistenceService, 'read')
vi.spyOn(storagePersistenceService, 'read'), .withArgs('authWellKnownEndPoints', { configId: 'configId1' })
['authWellKnownEndPoints', { configId: 'configId1' }], .and.returnValue({ jwksUri: 'someUrl' });
() => ({ jwksUri: 'someUrl' }) spyOn(dataService, 'get').and.returnValue(
);
vi.spyOn(dataService, 'get').mockReturnValue(
createRetriableStream( createRetriableStream(
throwError(() => new Error('Error')), throwError(() => new Error('Error')),
throwError(() => new Error('Error')), throwError(() => new Error('Error')),
@ -158,75 +148,73 @@ describe('Signin Key Data Service', () => {
) )
); );
try { service.getSigningKeys({ configId: 'configId1' }).subscribe({
await firstValueFrom(service.getSigningKeys({ configId: 'configId1' })); error: (err) => {
} catch (err: any) { expect(err).toBeTruthy();
expect(err).toBeTruthy(); },
} });
}); }));
}); });
describe('handleErrorGetSigningKeys', () => { describe('handleErrorGetSigningKeys', () => {
it('keeps observable if error is catched', () => { it('keeps observable if error is catched', waitForAsync(() => {
const result = (service as any).handleErrorGetSigningKeys( const result = (service as any).handleErrorGetSigningKeys(
new HttpResponse() new HttpResponse()
); );
const hasTypeObservable = isObservable(result); const hasTypeObservable = isObservable(result);
expect(hasTypeObservable).toBeTruthy(); expect(hasTypeObservable).toBeTrue();
}); }));
it('logs error if error is response', async () => { it('logs error if error is response', waitForAsync(() => {
const logSpy = vi.spyOn(loggerService, 'logError'); const logSpy = spyOn(loggerService, 'logError');
try { (service as any)
await firstValueFrom( .handleErrorGetSigningKeys(
(service as any).handleErrorGetSigningKeys( new HttpResponse({ status: 400, statusText: 'nono' }),
new HttpResponse({ status: 400, statusText: 'nono' }), { configId: 'configId1' }
{ configId: 'configId1' } )
) .subscribe({
); error: () => {
} catch { expect(logSpy).toHaveBeenCalledOnceWith(
expect(logSpy).toHaveBeenCalledExactlyOnceWith( { configId: 'configId1' },
{ configId: 'configId1' }, '400 - nono {}'
'400 - nono {}' );
); },
} });
}); }));
it('logs error if error is not a response', async () => { it('logs error if error is not a response', waitForAsync(() => {
const logSpy = vi.spyOn(loggerService, 'logError'); const logSpy = spyOn(loggerService, 'logError');
try { (service as any)
await firstValueFrom( .handleErrorGetSigningKeys('Just some Error', { configId: 'configId1' })
(service as any).handleErrorGetSigningKeys('Just some Error', { .subscribe({
configId: 'configId1', error: () => {
}) expect(logSpy).toHaveBeenCalledOnceWith(
); { configId: 'configId1' },
} catch { 'Just some Error'
expect(logSpy).toHaveBeenCalledExactlyOnceWith( );
{ configId: 'configId1' }, },
'Just some Error' });
); }));
}
});
it('logs error if error with message property is not a response', async () => { it('logs error if error with message property is not a response', waitForAsync(() => {
const logSpy = vi.spyOn(loggerService, 'logError'); const logSpy = spyOn(loggerService, 'logError');
try { (service as any)
await firstValueFrom( .handleErrorGetSigningKeys(
(service as any).handleErrorGetSigningKeys( { message: 'Just some Error' },
{ message: 'Just some Error' }, { configId: 'configId1' }
{ configId: 'configId1' } )
) .subscribe({
); error: () => {
} catch { expect(logSpy).toHaveBeenCalledOnceWith(
expect(logSpy).toHaveBeenCalledExactlyOnceWith( { configId: 'configId1' },
{ configId: 'configId1' }, 'Just some Error'
'Just some Error' );
); },
} });
}); }));
}); });
}); });

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