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/*
# misc
/.angular/cache
/.sass-cache
/connect.lock
/coverage
@ -54,4 +55,3 @@ debug.log
/playwright-report/
/blob-report/
/playwright/.cache/
/.vitest

210
README.md
View File

@ -1,17 +1,207 @@
<h1 align="center">
<img src="./assets/logo-512.png" height="150" alt="OUTPOSTS">
<div style="color: #232848; font-weight: 700;">OIDC-CLIENT-RX</div>
<div align="center">
<img src="https://img.shields.io/badge/status-work--in--progress-blue" alt="status-badge" />
</div>
</h1>
# Angular Lib for OpenID Connect & OAuth2
<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
[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",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist"],
"files": [
"dist"
],
"scripts": {
"build": "rslib build",
"dev": "rslib build --watch",
"test": "vitest --coverage",
"test-ci": "vitest --watch=false --coverage",
"test": "vitest --code-coverage",
"test-ci": "vitest --watch=false --browsers=ChromeHeadlessNoSandbox --code-coverage",
"pack": "npm run build && npm pack ./dist",
"publish": "npm run build && npm publish ./dist",
"coverage": "vitest run --coverage",
"lint": "ultracite lint",
"format": "ultracite format",
"cli": "tsx scripts/cli.ts"
"format": "ultracite format"
},
"dependencies": {
"@ngify/http": "^2.0.4",
"injection-js": "git+https://github.com/mgechev/injection-js.git#81a10e0",
"reflect-metadata": "^0.2.2"
},
"peerDependencies": {
"rxjs": "^7.4.0||>=8.0.0"
"rxjs": ">=7.4.0"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@biomejs/js-api": "0.7.1",
"@biomejs/wasm-nodejs": "^1.9.4",
"@evilmartians/lefthook": "^1.0.3",
"@playwright/test": "^1.49.1",
"@rslib/core": "^0.3.1",
"@swc/core": "^1.10.12",
"@types/jsdom": "^21.1.7",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.12.0",
"@vitest/browser": "^3.0.4",
"@vitest/coverage-v8": "^3.0.4",
"commander": "^13.1.0",
"jsdom": "^26.0.0",
"lodash-es": "^4.17.21",
"oxc-parser": "^0.48.1",
"oxc-walker": "^0.2.2",
"playwright": "^1.50.0",
"@types/jasmine": "^4.0.0",
"@types/node": "^22.10.1",
"@vitest/coverage-v8": "^3.0.1",
"rfc4648": "^1.5.0",
"rxjs": "^7.4.0",
"tsx": "^4.19.2",
"typescript": "^5.7.3",
"ultracite": "^4.1.15",
"unplugin-swc": "^1.5.1",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.4"
"vitest": "^3.0.1"
},
"keywords": [
"rxjs",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import type { OpenIdConfiguration } from '../../openid-configuration';
import { POSITIVE_VALIDATION_RESULT, type RuleValidationResult } from '../rule';
import { OpenIdConfiguration } from '../../openid-configuration';
import { POSITIVE_VALIDATION_RESULT, RuleValidationResult } from '../rule';
export const useOfflineScopeWithSilentRenew = (
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 {
JwkExtractor,
@ -93,6 +93,9 @@ describe('JwkExtractor', () => {
imports: [],
providers: [JwkExtractor, CryptoService],
});
});
beforeEach(() => {
service = TestBed.inject(JwkExtractor);
});
@ -102,30 +105,21 @@ describe('JwkExtractor', () => {
describe('extractJwk', () => {
it('throws error if no keys are present in array', () => {
try {
expect(() => {
service.extractJwk([]);
expect.fail('should error');
} catch (error: any) {
expect(error).toBe(JwkExtractorInvalidArgumentError);
}
}).toThrow(JwkExtractorInvalidArgumentError);
});
it('throws error if spec.kid is present, but no key was matching', () => {
try {
expect(() => {
service.extractJwk(keys, { kid: 'doot' });
expect.fail('should error');
} catch (error: any) {
expect(error).toBe(JwkExtractorNoMatchingKeysError);
}
}).toThrow(JwkExtractorNoMatchingKeysError);
});
it('throws error if spec.use is present, but no key was matching', () => {
try {
expect(() => {
service.extractJwk(keys, { use: 'blorp' });
expect.fail('should error');
} catch (error: any) {
expect(error).toBe(JwkExtractorNoMatchingKeysError);
}
}).toThrow(JwkExtractorNoMatchingKeysError);
});
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', () => {
try {
expect(() => {
service.extractJwk(keys);
expect.fail('should error');
} catch (error: any) {
expect(error).toBe(JwkExtractorSeveralMatchingKeysError);
}
}).toThrow(JwkExtractorSeveralMatchingKeysError);
});
it('returns array of keys matching spec.kid', () => {

View File

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

View File

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

View File

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

View File

@ -1,14 +1,14 @@
import { HttpHeaders } from '@ngify/http';
import { Injectable, inject } from 'injection-js';
import { type Observable, of, throwError, timer } from 'rxjs';
import { inject, Injectable } from 'injection-js';
import { Observable, of, throwError, timer } from 'rxjs';
import { catchError, mergeMap, retryWhen, switchMap } from 'rxjs/operators';
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 { StoragePersistenceService } from '../../storage/storage-persistence.service';
import { UrlService } from '../../utils/url/url.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 { isNetworkError } from './error-helper';
@ -116,7 +116,7 @@ export class CodeFlowCallbackHandlerService {
switchMap((response) => {
if (response) {
const authResult: AuthResult = {
...(response as any),
...response,
state: callbackContext.state,
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';
describe('error helper', () => {
@ -27,31 +27,31 @@ describe('error helper', () => {
});
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', () => {
expect(isNetworkError(UNKNOWN_CONNECTION_ERROR)).toBeTruthy();
expect(isNetworkError(UNKNOWN_CONNECTION_ERROR)).toBeTrue();
});
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', () => {
expect(isNetworkError(new Error('not a HttpErrorResponse'))).toBeFalsy();
expect(isNetworkError(new Error('not a HttpErrorResponse'))).toBeFalse();
});
it('returns false on string error', () => {
expect(isNetworkError('not a HttpErrorResponse')).toBeFalsy();
expect(isNetworkError('not a HttpErrorResponse')).toBeFalse();
});
it('returns false on undefined', () => {
expect(isNetworkError(undefined)).toBeFalsy();
expect(isNetworkError(undefined)).toBeFalse();
});
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 { firstValueFrom, of, throwError } from 'rxjs';
import { vi } from 'vitest';
import { TestBed, waitForAsync } from '@angular/core/testing';
import { of, throwError } from 'rxjs';
import { mockProvider } from '../../../test/auto-mock';
import { AuthStateService } from '../../auth-state/auth-state.service';
import { LoggerService } from '../../logging/logger.service';
import { StoragePersistenceService } from '../../storage/storage-persistence.service';
import { mockProvider } from '../../testing/mock';
import type { JwtKey, JwtKeys } from '../../validation/jwtkeys';
import { JwtKey, JwtKeys } from '../../validation/jwtkeys';
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 { ResetAuthDataService } from '../reset-auth-data.service';
import { SigninKeyDataService } from '../signin-key-data.service';
@ -47,6 +46,9 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
mockProvider(ResetAuthDataService),
],
});
});
beforeEach(() => {
service = TestBed.inject(HistoryJwtKeysCallbackHandlerService);
storagePersistenceService = TestBed.inject(StoragePersistenceService);
resetAuthDataService = TestBed.inject(ResetAuthDataService);
@ -60,8 +62,8 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
});
describe('callbackHistoryAndResetJwtKeys', () => {
it('writes authResult into the storage', async () => {
const storagePersistenceServiceSpy = vi.spyOn(
it('writes authResult into the storage', waitForAsync(() => {
const storagePersistenceServiceSpy = spyOn(
storagePersistenceService,
'write'
);
@ -73,82 +75,86 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
const callbackContext = {
authResult: DUMMY_AUTH_RESULT,
} as CallbackContext;
const allConfigs = [
const allconfigs = [
{
configId: 'configId1',
historyCleanupOff: true,
},
];
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue(
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
of({ keys: [] } as JwtKeys)
);
await firstValueFrom(
service.callbackHistoryAndResetJwtKeys(
service
.callbackHistoryAndResetJwtKeys(
callbackContext,
allConfigs[0]!,
allConfigs
allconfigs[0],
allconfigs
)
);
expect(storagePersistenceServiceSpy.mock.calls).toEqual([
['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]],
['jwtKeys', { keys: [] }, allConfigs[0]],
.subscribe(() => {
expect(storagePersistenceServiceSpy.calls.allArgs()).toEqual([
['authnResult', DUMMY_AUTH_RESULT, allconfigs[0]],
['jwtKeys', { keys: [] }, allconfigs[0]],
]);
// 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 = {
refresh_token: 'dummy_refresh_token',
id_token: 'some-id-token',
};
const storagePersistenceServiceSpy = vi.spyOn(
const storagePersistenceServiceSpy = spyOn(
storagePersistenceService,
'write'
);
const callbackContext = {
authResult: DUMMY_AUTH_RESULT,
} as CallbackContext;
const allConfigs = [
const allconfigs = [
{
configId: 'configId1',
historyCleanupOff: true,
},
];
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue(
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
of({ keys: [] } as JwtKeys)
);
await firstValueFrom(
service.callbackHistoryAndResetJwtKeys(
service
.callbackHistoryAndResetJwtKeys(
callbackContext,
allConfigs[0]!,
allConfigs
allconfigs[0],
allconfigs
)
);
expect(storagePersistenceServiceSpy.mock.calls).toEqual([
['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]],
['jwtKeys', { keys: [] }, allConfigs[0]],
.subscribe(() => {
expect(storagePersistenceServiceSpy.calls.allArgs()).toEqual([
['authnResult', DUMMY_AUTH_RESULT, allconfigs[0]],
['jwtKeys', { keys: [] }, allconfigs[0]],
]);
// 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 = {
refresh_token: 'dummy_refresh_token',
id_token: 'some-id-token',
};
const storagePersistenceServiceSpy = vi.spyOn(
const storagePersistenceServiceSpy = spyOn(
storagePersistenceService,
'write'
);
const callbackContext = {
authResult: DUMMY_AUTH_RESULT,
} as CallbackContext;
const allConfigs = [
const allconfigs = [
{
configId: 'configId1',
historyCleanupOff: true,
@ -156,25 +162,27 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
},
];
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue(
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
of({ keys: [] } as JwtKeys)
);
await firstValueFrom(
service.callbackHistoryAndResetJwtKeys(
service
.callbackHistoryAndResetJwtKeys(
callbackContext,
allConfigs[0]!,
allConfigs
allconfigs[0],
allconfigs
)
);
expect(storagePersistenceServiceSpy.mock.calls).toEqual([
['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]],
['reusable_refresh_token', 'dummy_refresh_token', allConfigs[0]],
['jwtKeys', { keys: [] }, allConfigs[0]],
.subscribe(() => {
expect(storagePersistenceServiceSpy.calls.allArgs()).toEqual([
['authnResult', DUMMY_AUTH_RESULT, allconfigs[0]],
['reusable_refresh_token', 'dummy_refresh_token', allconfigs[0]],
['jwtKeys', { keys: [] }, allconfigs[0]],
]);
// 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 = {
id_token: 'some-id-token',
};
@ -182,29 +190,30 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
isRenewProcess: false,
authResult: DUMMY_AUTH_RESULT,
} as CallbackContext;
const allConfigs = [
const allconfigs = [
{
configId: 'configId1',
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)
);
await firstValueFrom(
service.callbackHistoryAndResetJwtKeys(
service
.callbackHistoryAndResetJwtKeys(
callbackContext,
allConfigs[0]!,
allConfigs
allconfigs[0],
allconfigs
)
);
.subscribe(() => {
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 = {
id_token: 'some-id-token',
};
@ -213,31 +222,32 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
isRenewProcess: false,
authResult: DUMMY_AUTH_RESULT,
} as CallbackContext;
const allConfigs = [
const allconfigs = [
{
configId: 'configId1',
historyCleanupOff: false,
},
];
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue(
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
of({ keys: [{ kty: 'henlo' } as JwtKey] } as JwtKeys)
);
const result = await firstValueFrom(
service.callbackHistoryAndResetJwtKeys(
service
.callbackHistoryAndResetJwtKeys(
callbackContext,
allConfigs[0]!,
allConfigs
allconfigs[0],
allconfigs
)
);
.subscribe((result) => {
expect(result).toEqual({
isRenewProcess: false,
authResult: DUMMY_AUTH_RESULT,
jwtKeys: { keys: [{ kty: 'henlo' }] },
} 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 = {
id_token: 'some-id-token',
};
@ -246,32 +256,32 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
isRenewProcess: false,
authResult: DUMMY_AUTH_RESULT,
} as CallbackContext;
const allConfigs = [
const allconfigs = [
{
configId: 'configId1',
historyCleanupOff: false,
},
];
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue(
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
of({} as JwtKeys)
);
try {
await firstValueFrom(
service.callbackHistoryAndResetJwtKeys(
service
.callbackHistoryAndResetJwtKeys(
callbackContext,
allConfigs[0]!,
allConfigs
allconfigs[0],
allconfigs
)
);
} catch (err: any) {
.subscribe({
error: (err) => {
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 = {
id_token: 'some-id-token',
};
@ -279,140 +289,140 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
isRenewProcess: false,
authResult: DUMMY_AUTH_RESULT,
} as CallbackContext;
const allConfigs = [
const allconfigs = [
{
configId: 'configId1',
historyCleanupOff: false,
},
];
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue(
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
throwError(() => new Error('error'))
);
try {
await firstValueFrom(
service.callbackHistoryAndResetJwtKeys(
service
.callbackHistoryAndResetJwtKeys(
callbackContext,
allConfigs[0]!,
allConfigs
allconfigs[0],
allconfigs
)
);
} catch (err: any) {
.subscribe({
error: (err) => {
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 = {
authResult: { error: 'someError' },
} as CallbackContext;
const allConfigs = [
const allconfigs = [
{
configId: 'configId1',
historyCleanupOff: true,
},
];
try {
await firstValueFrom(
service.callbackHistoryAndResetJwtKeys(
service
.callbackHistoryAndResetJwtKeys(
callbackContext,
allConfigs[0]!,
allConfigs
allconfigs[0],
allconfigs
)
);
} catch (err: any) {
.subscribe({
error: (err) => {
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 = {
authResult: { error: 'someError' },
isRenewProcess: false,
} as CallbackContext;
const allConfigs = [
const allconfigs = [
{
configId: 'configId1',
historyCleanupOff: true,
},
];
const resetAuthorizationDataSpy = vi.spyOn(
const resetAuthorizationDataSpy = spyOn(
resetAuthDataService,
'resetAuthorizationData'
);
const setNonceSpy = vi.spyOn(flowsDataService, 'setNonce');
const updateAndPublishAuthStateSpy = vi.spyOn(
const setNonceSpy = spyOn(flowsDataService, 'setNonce');
const updateAndPublishAuthStateSpy = spyOn(
authStateService,
'updateAndPublishAuthState'
);
try {
await firstValueFrom(
service.callbackHistoryAndResetJwtKeys(
service
.callbackHistoryAndResetJwtKeys(
callbackContext,
allConfigs[0]!,
allConfigs
allconfigs[0],
allconfigs
)
);
} catch {
.subscribe({
error: () => {
expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1);
expect(setNonceSpy).toHaveBeenCalledTimes(1);
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledOnceWith({
isAuthenticated: false,
validationResult: ValidationResult.SecureTokenServerError,
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 = {
authResult: { error: 'login_required' },
isRenewProcess: false,
} as CallbackContext;
const allConfigs = [
const allconfigs = [
{
configId: 'configId1',
historyCleanupOff: true,
},
];
const resetAuthorizationDataSpy = vi.spyOn(
const resetAuthorizationDataSpy = spyOn(
resetAuthDataService,
'resetAuthorizationData'
);
const setNonceSpy = vi.spyOn(flowsDataService, 'setNonce');
const updateAndPublishAuthStateSpy = vi.spyOn(
const setNonceSpy = spyOn(flowsDataService, 'setNonce');
const updateAndPublishAuthStateSpy = spyOn(
authStateService,
'updateAndPublishAuthState'
);
try {
await firstValueFrom(
service.callbackHistoryAndResetJwtKeys(
service
.callbackHistoryAndResetJwtKeys(
callbackContext,
allConfigs[0]!,
allConfigs
allconfigs[0],
allconfigs
)
);
} catch {
.subscribe({
error: () => {
expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1);
expect(setNonceSpy).toHaveBeenCalledTimes(1);
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledOnceWith({
isAuthenticated: false,
validationResult: ValidationResult.LoginRequired,
isRenewProcess: false,
});
}
},
});
}));
it('should store jwtKeys', async () => {
it('should store jwtKeys', waitForAsync(() => {
const DUMMY_AUTH_RESULT = {
id_token: 'some-id-token',
};
@ -420,41 +430,44 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
const initialCallbackContext = {
authResult: DUMMY_AUTH_RESULT,
} as CallbackContext;
const allConfigs = [
const allconfigs = [
{
configId: 'configId1',
historyCleanupOff: true,
},
];
const storagePersistenceServiceSpy = vi.spyOn(
const storagePersistenceServiceSpy = spyOn(
storagePersistenceService,
'write'
);
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue(
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
of(DUMMY_JWT_KEYS)
);
try {
const callbackContext: CallbackContext = await firstValueFrom(
service.callbackHistoryAndResetJwtKeys(
service
.callbackHistoryAndResetJwtKeys(
initialCallbackContext,
allConfigs[0]!,
allConfigs
allconfigs[0],
allconfigs
)
);
.subscribe({
next: (callbackContext: CallbackContext) => {
expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(2);
expect(storagePersistenceServiceSpy.mock.calls).toEqual([
['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]],
['jwtKeys', DUMMY_JWT_KEYS, allConfigs[0]],
expect(storagePersistenceServiceSpy.calls.allArgs()).toEqual([
['authnResult', DUMMY_AUTH_RESULT, 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 = {
id_token: 'some-id-token',
access_token: 'some-access-token',
@ -463,41 +476,45 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
authResult,
} as CallbackContext;
const allConfigs = [
const allconfigs = [
{
configId: 'configId1',
historyCleanupOff: true,
},
];
const storagePersistenceServiceSpy = vi.spyOn(
const storagePersistenceServiceSpy = spyOn(
storagePersistenceService,
'write'
);
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue(
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
throwError(() => new Error('Error'))
);
try {
const callbackContext: CallbackContext = await firstValueFrom(
service.callbackHistoryAndResetJwtKeys(
service
.callbackHistoryAndResetJwtKeys(
initialCallbackContext,
allConfigs[0]!,
allConfigs
allconfigs[0],
allconfigs
)
);
.subscribe({
next: (callbackContext: CallbackContext) => {
expect(callbackContext).toBeFalsy();
} catch (err: any) {
},
error: (err) => {
expect(err).toBeTruthy();
expect(storagePersistenceServiceSpy).toHaveBeenCalledExactlyOnceWith(
// storagePersistenceService.write() should not have been called with jwtKeys
expect(storagePersistenceServiceSpy).toHaveBeenCalledOnceWith(
'authnResult',
authResult,
allConfigs[0]
allconfigs[0]
);
}
},
});
}));
it('should fallback to stored jwtKeys on error', async () => {
it('should fallback to stored jwtKeys on error', waitForAsync(() => {
const authResult = {
id_token: 'some-id-token',
access_token: 'some-access-token',
@ -506,72 +523,76 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
authResult,
} as CallbackContext;
const allConfigs = [
const allconfigs = [
{
configId: 'configId1',
historyCleanupOff: true,
},
];
const storagePersistenceServiceSpy = vi.spyOn(
const storagePersistenceServiceSpy = spyOn(
storagePersistenceService,
'read'
);
storagePersistenceServiceSpy.mockReturnValue(DUMMY_JWT_KEYS);
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue(
storagePersistenceServiceSpy.and.returnValue(DUMMY_JWT_KEYS);
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
throwError(() => new Error('Error'))
);
try {
const callbackContext: CallbackContext = await firstValueFrom(
service.callbackHistoryAndResetJwtKeys(
service
.callbackHistoryAndResetJwtKeys(
initialCallbackContext,
allConfigs[0]!,
allConfigs
allconfigs[0],
allconfigs
)
);
expect(storagePersistenceServiceSpy).toHaveBeenCalledExactlyOnceWith(
.subscribe({
next: (callbackContext: CallbackContext) => {
expect(storagePersistenceServiceSpy).toHaveBeenCalledOnceWith(
'jwtKeys',
allConfigs[0]
allconfigs[0]
);
expect(callbackContext.jwtKeys).toEqual(DUMMY_JWT_KEYS);
} catch (err: any) {
},
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 = {
id_token: 'some-id-token',
access_token: 'some-access-token',
} as AuthResult;
const initialCallbackContext = { authResult } as CallbackContext;
const allConfigs = [
const allconfigs = [
{
configId: 'configId1',
historyCleanupOff: true,
},
];
vi.spyOn(storagePersistenceService, 'read').mockReturnValue(null);
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue(
spyOn(storagePersistenceService, 'read').and.returnValue(null);
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
throwError(() => new Error('Error'))
);
try {
const callbackContext: CallbackContext = await firstValueFrom(
service.callbackHistoryAndResetJwtKeys(
service
.callbackHistoryAndResetJwtKeys(
initialCallbackContext,
allConfigs[0]!,
allConfigs
allconfigs[0],
allconfigs
)
);
.subscribe({
next: (callbackContext: CallbackContext) => {
expect(callbackContext).toBeFalsy();
} catch (err: any) {
},
error: (err) => {
expect(err).toBeTruthy();
}
},
});
}));
});
describe('historyCleanUpTurnedOn ', () => {

View File

@ -1,14 +1,14 @@
import { Injectable, inject } from 'injection-js';
import { type Observable, of, throwError } from 'rxjs';
import { DOCUMENT } from '../../dom';
import { inject, Injectable } from 'injection-js';
import { Observable, of, throwError } from 'rxjs';
import { catchError, switchMap, tap } from 'rxjs/operators';
import { AuthStateService } from '../../auth-state/auth-state.service';
import type { OpenIdConfiguration } from '../../config/openid-configuration';
import { DOCUMENT } from '../../dom';
import { OpenIdConfiguration } from '../../config/openid-configuration';
import { LoggerService } from '../../logging/logger.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 type { CallbackContext } from '../callback-context';
import { CallbackContext } from '../callback-context';
import { FlowsDataService } from '../flows-data.service';
import { ResetAuthDataService } from '../reset-auth-data.service';
import { SigninKeyDataService } from '../signin-key-data.service';
@ -98,10 +98,10 @@ export class HistoryJwtKeysCallbackHandlerService {
// fallback: try to load jwtKeys from storage
const storedJwtKeys = this.readSigningKeys(config);
if (storedJwtKeys) {
if (!!storedJwtKeys) {
this.loggerService.logWarning(
config,
'Failed to retrieve signing keys, fallback to stored keys'
`Failed to retrieve signing keys, fallback to stored keys`
);
return of(storedJwtKeys);
@ -116,7 +116,7 @@ export class HistoryJwtKeysCallbackHandlerService {
return of(callbackContext);
}
const errorMessage = 'Failed to retrieve signing key';
const errorMessage = `Failed to retrieve signing key`;
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 { TestBed, waitForAsync } from '@angular/core/testing';
import { mockProvider } from '../../../test/auto-mock';
import { LoggerService } from '../../logging/logger.service';
import { mockProvider } from '../../testing/mock';
import type { CallbackContext } from '../callback-context';
import { CallbackContext } from '../callback-context';
import { FlowsDataService } from '../flows-data.service';
import { ResetAuthDataService } from '../reset-auth-data.service';
import { ImplicitFlowCallbackHandlerService } from './implicit-flow-callback-handler.service';
@ -36,6 +34,9 @@ describe('ImplicitFlowCallbackHandlerService', () => {
},
],
});
});
beforeEach(() => {
service = TestBed.inject(ImplicitFlowCallbackHandlerService);
flowsDataService = TestBed.inject(FlowsDataService);
resetAuthDataService = TestBed.inject(ResetAuthDataService);
@ -46,44 +47,46 @@ describe('ImplicitFlowCallbackHandlerService', () => {
});
describe('implicitFlowCallback', () => {
it('calls "resetAuthorizationData" if silent renew is not running', async () => {
vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(false);
const resetAuthorizationDataSpy = vi.spyOn(
it('calls "resetAuthorizationData" if silent renew is not running', waitForAsync(() => {
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false);
const resetAuthorizationDataSpy = spyOn(
resetAuthDataService,
'resetAuthorizationData'
);
const allConfigs = [
const allconfigs = [
{
configId: 'configId1',
},
];
await firstValueFrom(
service.implicitFlowCallback(allConfigs[0]!, allConfigs, 'any-hash')
);
service
.implicitFlowCallback(allconfigs[0], allconfigs, 'any-hash')
.subscribe(() => {
expect(resetAuthorizationDataSpy).toHaveBeenCalled();
});
}));
it('does NOT calls "resetAuthorizationData" if silent renew is running', async () => {
vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(true);
const resetAuthorizationDataSpy = vi.spyOn(
it('does NOT calls "resetAuthorizationData" if silent renew is running', waitForAsync(() => {
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true);
const resetAuthorizationDataSpy = spyOn(
resetAuthDataService,
'resetAuthorizationData'
);
const allConfigs = [
const allconfigs = [
{
configId: 'configId1',
},
];
await firstValueFrom(
service.implicitFlowCallback(allConfigs[0]!, allConfigs, 'any-hash')
);
service
.implicitFlowCallback(allconfigs[0], allconfigs, 'any-hash')
.subscribe(() => {
expect(resetAuthorizationDataSpy).not.toHaveBeenCalled();
});
}));
it('returns callbackContext if all params are good', async () => {
vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(true);
it('returns callbackContext if all params are good', waitForAsync(() => {
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true);
const expectedCallbackContext = {
code: '',
refreshToken: '',
@ -96,20 +99,21 @@ describe('ImplicitFlowCallbackHandlerService', () => {
existingIdToken: null,
} as CallbackContext;
const allConfigs = [
const allconfigs = [
{
configId: 'configId1',
},
];
const callbackContext = await firstValueFrom(
service.implicitFlowCallback(allConfigs[0]!, allConfigs, 'anyHash')
);
service
.implicitFlowCallback(allconfigs[0], allconfigs, 'anyHash')
.subscribe((callbackContext) => {
expect(callbackContext).toEqual(expectedCallbackContext);
});
}));
it('uses window location hash if no hash is passed', async () => {
vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(true);
it('uses window location hash if no hash is passed', waitForAsync(() => {
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true);
const expectedCallbackContext = {
code: '',
refreshToken: '',
@ -122,16 +126,17 @@ describe('ImplicitFlowCallbackHandlerService', () => {
existingIdToken: null,
} as CallbackContext;
const allConfigs = [
const allconfigs = [
{
configId: 'configId1',
},
];
const callbackContext = await firstValueFrom(
service.implicitFlowCallback(allConfigs[0]!, allConfigs)
);
service
.implicitFlowCallback(allconfigs[0], allconfigs)
.subscribe((callbackContext) => {
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 { inject, Injectable } from 'injection-js';
import { Observable, of } from 'rxjs';
import { OpenIdConfiguration } from '../../config/openid-configuration';
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 { ResetAuthDataService } from '../reset-auth-data.service';

View File

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

View File

@ -1,10 +1,10 @@
import { Injectable, inject } from 'injection-js';
import { type Observable, of, throwError } from 'rxjs';
import { inject, Injectable } from 'injection-js';
import { Observable, of, throwError } from 'rxjs';
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 { TokenValidationService } from '../../validation/token-validation.service';
import type { CallbackContext } from '../callback-context';
import { CallbackContext } from '../callback-context';
import { FlowsDataService } from '../flows-data.service';
@Injectable()
@ -24,7 +24,7 @@ export class RefreshSessionCallbackHandlerService {
this.loggerService.logDebug(
config,
`RefreshSession created. Adding myautostate: ${stateData}`
'RefreshSession created. Adding myautostate: ' + stateData
);
const refreshToken = this.authStateService.getRefreshToken(config);
const idToken = this.authStateService.getIdToken(config);
@ -53,7 +53,7 @@ export class RefreshSessionCallbackHandlerService {
);
return of(callbackContext);
}
} else {
const errorMessage = 'no refresh token found, please login';
this.loggerService.logError(config, errorMessage);
@ -61,3 +61,4 @@ export class RefreshSessionCallbackHandlerService {
return throwError(() => new Error(errorMessage));
}
}
}

View File

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

View File

@ -1,13 +1,13 @@
import { HttpHeaders } from '@ngify/http';
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 { 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 { StoragePersistenceService } from '../../storage/storage-persistence.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';
@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 { 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 { mockProvider } from '../../testing/mock';
import type { StateValidationResult } from '../../validation/state-validation-result';
import { StateValidationResult } from '../../validation/state-validation-result';
import { StateValidationService } from '../../validation/state-validation.service';
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 { StateValidationCallbackHandlerService } from './state-validation-callback-handler.service';
@ -42,6 +41,9 @@ describe('StateValidationCallbackHandlerService', () => {
},
],
});
});
beforeEach(() => {
service = TestBed.inject(StateValidationCallbackHandlerService);
stateValidationService = TestBed.inject(StateValidationService);
loggerService = TestBed.inject(LoggerService);
@ -54,11 +56,8 @@ describe('StateValidationCallbackHandlerService', () => {
});
describe('callbackStateValidation', () => {
it('returns callbackContext with validationResult if validationResult is valid', async () => {
vi.spyOn(
stateValidationService,
'getValidatedStateResult'
).mockReturnValue(
it('returns callbackContext with validationResult if validationResult is valid', waitForAsync(() => {
spyOn(stateValidationService, 'getValidatedStateResult').and.returnValue(
of({
idToken: 'idTokenJustForTesting',
authResponseIsValid: true,
@ -66,13 +65,13 @@ describe('StateValidationCallbackHandlerService', () => {
);
const allConfigs = [{ configId: 'configId1' }];
const newCallbackContext = await firstValueFrom(
service.callbackStateValidation(
service
.callbackStateValidation(
{} as CallbackContext,
allConfigs[0]!,
allConfigs[0],
allConfigs
)
);
.subscribe((newCallbackContext) => {
expect(newCallbackContext).toEqual({
validationResult: {
idToken: 'idTokenJustForTesting',
@ -80,73 +79,68 @@ describe('StateValidationCallbackHandlerService', () => {
},
} as CallbackContext);
});
}));
it('logs error in case of an error', async () => {
vi.spyOn(
stateValidationService,
'getValidatedStateResult'
).mockReturnValue(
it('logs error in case of an error', waitForAsync(() => {
spyOn(stateValidationService, 'getValidatedStateResult').and.returnValue(
of({
authResponseIsValid: false,
} as StateValidationResult)
);
const loggerSpy = vi.spyOn(loggerService, 'logWarning');
const loggerSpy = spyOn(loggerService, 'logWarning');
const allConfigs = [{ configId: 'configId1' }];
try {
await firstValueFrom(
service.callbackStateValidation(
service
.callbackStateValidation(
{} as CallbackContext,
allConfigs[0]!,
allConfigs[0],
allConfigs
)
);
} catch {
expect(loggerSpy).toHaveBeenCalledExactlyOnceWith(
allConfigs[0]!,
.subscribe({
error: () => {
expect(loggerSpy).toHaveBeenCalledOnceWith(
allConfigs[0],
'authorizedCallback, token(s) validation failed, resetting. Hash: &anyFakeHash'
);
}
},
});
}));
it('calls resetAuthDataService.resetAuthorizationData and authStateService.updateAndPublishAuthState in case of an error', async () => {
vi.spyOn(
stateValidationService,
'getValidatedStateResult'
).mockReturnValue(
it('calls resetAuthDataService.resetAuthorizationData and authStateService.updateAndPublishAuthState in case of an error', waitForAsync(() => {
spyOn(stateValidationService, 'getValidatedStateResult').and.returnValue(
of({
authResponseIsValid: false,
state: ValidationResult.LoginRequired,
} as StateValidationResult)
);
const resetAuthorizationDataSpy = vi.spyOn(
const resetAuthorizationDataSpy = spyOn(
resetAuthDataService,
'resetAuthorizationData'
);
const updateAndPublishAuthStateSpy = vi.spyOn(
const updateAndPublishAuthStateSpy = spyOn(
authStateService,
'updateAndPublishAuthState'
);
const allConfigs = [{ configId: 'configId1' }];
try {
await firstValueFrom(
service.callbackStateValidation(
service
.callbackStateValidation(
{ isRenewProcess: true } as CallbackContext,
allConfigs[0]!,
allConfigs[0],
allConfigs
)
);
} catch {
.subscribe({
error: () => {
expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1);
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledOnceWith({
isAuthenticated: false,
validationResult: ValidationResult.LoginRequired,
isRenewProcess: true,
});
}
});
},
});
}));
});
});

View File

@ -1,13 +1,13 @@
import { Injectable, inject } from 'injection-js';
import type { Observable } from 'rxjs';
import { DOCUMENT } from '../../dom';
import { inject, Injectable } from 'injection-js';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { AuthStateService } from '../../auth-state/auth-state.service';
import type { OpenIdConfiguration } from '../../config/openid-configuration';
import { DOCUMENT } from '../../dom';
import { OpenIdConfiguration } from '../../config/openid-configuration';
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 type { CallbackContext } from '../callback-context';
import { CallbackContext } from '../callback-context';
import { ResetAuthDataService } from '../reset-auth-data.service';
@Injectable()
@ -43,7 +43,7 @@ export class StateValidationCallbackHandlerService {
);
return callbackContext;
}
} else {
const errorMessage = `authorizedCallback, token(s) validation failed, resetting. Hash: ${this.document.location.hash}`;
this.loggerService.logWarning(configuration, errorMessage);
@ -57,6 +57,7 @@ export class StateValidationCallbackHandlerService {
);
throw new Error(errorMessage);
}
})
);
}

View File

@ -1,13 +1,12 @@
import { TestBed } from '@/testing';
import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest';
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 { mockProvider } from '../../testing/mock';
import { UserService } from '../../user-data/user.service';
import { StateValidationResult } from '../../validation/state-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 { ResetAuthDataService } from '../reset-auth-data.service';
import { UserCallbackHandlerService } from './user-callback-handler.service';
@ -30,6 +29,9 @@ describe('UserCallbackHandlerService', () => {
mockProvider(ResetAuthDataService),
],
});
});
beforeEach(() => {
service = TestBed.inject(UserCallbackHandlerService);
flowsDataService = TestBed.inject(FlowsDataService);
authStateService = TestBed.inject(AuthStateService);
@ -42,7 +44,7 @@ describe('UserCallbackHandlerService', () => {
});
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(
'accesstoken',
'idtoken',
@ -68,16 +70,17 @@ describe('UserCallbackHandlerService', () => {
},
];
const spy = vi.spyOn(flowsDataService, 'setSessionState');
const spy = spyOn(flowsDataService, 'setSessionState');
const resultCallbackContext = await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
);
expect(spy).toHaveBeenCalledExactlyOnceWith('mystate', allConfigs[0]);
service
.callbackUser(callbackContext, allConfigs[0], allConfigs)
.subscribe((resultCallbackContext) => {
expect(spy).toHaveBeenCalledOnceWith('mystate', allConfigs[0]);
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(
'accesstoken',
'idtoken',
@ -101,16 +104,17 @@ describe('UserCallbackHandlerService', () => {
autoUserInfo: false,
},
];
const spy = vi.spyOn(flowsDataService, 'setSessionState');
const spy = spyOn(flowsDataService, 'setSessionState');
const resultCallbackContext = await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
);
service
.callbackUser(callbackContext, allConfigs[0], allConfigs)
.subscribe((resultCallbackContext) => {
expect(spy).not.toHaveBeenCalled();
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(
'accesstoken',
'idtoken',
@ -134,16 +138,17 @@ describe('UserCallbackHandlerService', () => {
autoUserInfo: false,
},
];
const spy = vi.spyOn(flowsDataService, 'setSessionState');
const spy = spyOn(flowsDataService, 'setSessionState');
const resultCallbackContext = await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
);
service
.callbackUser(callbackContext, allConfigs[0], allConfigs)
.subscribe((resultCallbackContext) => {
expect(spy).not.toHaveBeenCalled();
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 callbackContext = {
code: '',
@ -163,16 +168,17 @@ describe('UserCallbackHandlerService', () => {
},
];
const spy = vi.spyOn(flowsDataService, 'setSessionState');
const spy = spyOn(flowsDataService, 'setSessionState');
const resultCallbackContext = await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
);
service
.callbackUser(callbackContext, allConfigs[0], allConfigs)
.subscribe((resultCallbackContext) => {
expect(spy).not.toHaveBeenCalled();
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(
'accesstoken',
'idtoken',
@ -198,23 +204,24 @@ describe('UserCallbackHandlerService', () => {
},
];
const updateAndPublishAuthStateSpy = vi.spyOn(
const updateAndPublishAuthStateSpy = spyOn(
authStateService,
'updateAndPublishAuthState'
);
const resultCallbackContext = await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
);
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({
service
.callbackUser(callbackContext, allConfigs[0], allConfigs)
.subscribe((resultCallbackContext) => {
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledOnceWith({
isAuthenticated: true,
validationResult: ValidationResult.NotSet,
isRenewProcess: false,
});
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(
'accesstoken',
'idtoken',
@ -240,15 +247,16 @@ describe('UserCallbackHandlerService', () => {
},
];
const getAndPersistUserDataInStoreSpy = vi
.spyOn(userService, 'getAndPersistUserDataInStore')
.mockReturnValue(of({ user: 'some_data' }));
const getAndPersistUserDataInStoreSpy = spyOn(
userService,
'getAndPersistUserDataInStore'
).and.returnValue(of({ user: 'some_data' }));
const resultCallbackContext = await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
);
expect(getAndPersistUserDataInStoreSpy).toHaveBeenCalledExactlyOnceWith(
allConfigs[0]!,
service
.callbackUser(callbackContext, allConfigs[0], allConfigs)
.subscribe((resultCallbackContext) => {
expect(getAndPersistUserDataInStoreSpy).toHaveBeenCalledOnceWith(
allConfigs[0],
allConfigs,
false,
'idtoken',
@ -256,8 +264,9 @@ describe('UserCallbackHandlerService', () => {
);
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(
'accesstoken',
'idtoken',
@ -284,26 +293,27 @@ describe('UserCallbackHandlerService', () => {
},
];
vi.spyOn(userService, 'getAndPersistUserDataInStore').mockReturnValue(
spyOn(userService, 'getAndPersistUserDataInStore').and.returnValue(
of({ user: 'some_data' })
);
const updateAndPublishAuthStateSpy = vi.spyOn(
const updateAndPublishAuthStateSpy = spyOn(
authStateService,
'updateAndPublishAuthState'
);
const resultCallbackContext = await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
);
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({
service
.callbackUser(callbackContext, allConfigs[0], allConfigs)
.subscribe((resultCallbackContext) => {
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledOnceWith({
isAuthenticated: true,
validationResult: ValidationResult.MaxOffsetExpired,
isRenewProcess: false,
});
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(
'accesstoken',
'idtoken',
@ -330,22 +340,23 @@ describe('UserCallbackHandlerService', () => {
},
];
vi.spyOn(userService, 'getAndPersistUserDataInStore').mockReturnValue(
spyOn(userService, 'getAndPersistUserDataInStore').and.returnValue(
of({ user: 'some_data' })
);
const setSessionStateSpy = vi.spyOn(flowsDataService, 'setSessionState');
const setSessionStateSpy = spyOn(flowsDataService, 'setSessionState');
const resultCallbackContext = await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
);
expect(setSessionStateSpy).toHaveBeenCalledExactlyOnceWith(
service
.callbackUser(callbackContext, allConfigs[0], allConfigs)
.subscribe((resultCallbackContext) => {
expect(setSessionStateSpy).toHaveBeenCalledOnceWith(
'mystate',
allConfigs[0]
);
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(
'accesstoken',
'idtoken',
@ -372,20 +383,19 @@ describe('UserCallbackHandlerService', () => {
},
];
vi.spyOn(userService, 'getAndPersistUserDataInStore').mockReturnValue(
spyOn(userService, 'getAndPersistUserDataInStore').and.returnValue(
of(null)
);
const updateAndPublishAuthStateSpy = vi.spyOn(
const updateAndPublishAuthStateSpy = spyOn(
authStateService,
'updateAndPublishAuthState'
);
try {
await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
);
} catch (err: any) {
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({
service
.callbackUser(callbackContext, allConfigs[0], allConfigs)
.subscribe({
error: (err) => {
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledOnceWith({
isAuthenticated: false,
validationResult: ValidationResult.MaxOffsetExpired,
isRenewProcess: false,
@ -393,10 +403,11 @@ describe('UserCallbackHandlerService', () => {
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(
'accesstoken',
'idtoken',
@ -423,24 +434,24 @@ describe('UserCallbackHandlerService', () => {
},
];
vi.spyOn(userService, 'getAndPersistUserDataInStore').mockReturnValue(
spyOn(userService, 'getAndPersistUserDataInStore').and.returnValue(
of(null)
);
const resetAuthorizationDataSpy = vi.spyOn(
const resetAuthorizationDataSpy = spyOn(
resetAuthDataService,
'resetAuthorizationData'
);
try {
await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
);
} catch (err: any) {
service
.callbackUser(callbackContext, allConfigs[0], allConfigs)
.subscribe({
error: (err) => {
expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1);
expect(err.message).toEqual(
'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 { type Observable, of, throwError } from 'rxjs';
import { inject, Injectable } from 'injection-js';
import { Observable, of, throwError } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators';
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 { UserService } from '../../user-data/user.service';
import type { StateValidationResult } from '../../validation/state-validation-result';
import type { CallbackContext } from '../callback-context';
import { StateValidationResult } from '../../validation/state-validation-result';
import { CallbackContext } from '../callback-context';
import { FlowsDataService } from '../flows-data.service';
import { ResetAuthDataService } from '../reset-auth-data.service';
@ -35,7 +35,6 @@ export class UserCallbackHandlerService {
if (!autoUserInfo) {
if (!isRenewProcess || renewUserInfoAfterTokenRenew) {
// userData is set to the id_token decoded, auto get user data set to false
// biome-ignore lint/nursery/useCollapsedIf: <explanation>
if (validationResult?.decodedIdToken) {
this.userService.setUserDataToStore(
validationResult.decodedIdToken,
@ -67,7 +66,7 @@ export class UserCallbackHandlerService {
)
.pipe(
switchMap((userData) => {
if (userData) {
if (!!userData) {
if (!refreshToken) {
this.flowsDataService.setSessionState(
authResult?.session_state,
@ -78,7 +77,7 @@ export class UserCallbackHandlerService {
this.publishAuthState(validationResult, isRenewProcess);
return of(callbackContext);
}
} else {
this.resetAuthDataService.resetAuthorizationData(
configuration,
allConfigs
@ -89,6 +88,7 @@ export class UserCallbackHandlerService {
this.loggerService.logWarning(configuration, errorMessage);
return throwError(() => new Error(errorMessage));
}
}),
catchError((err) => {
const errorMessage = `Failed to retrieve user info with error: ${err}`;

View File

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

View File

@ -1,8 +1,8 @@
import { Injectable, inject } from 'injection-js';
import type { OpenIdConfiguration } from '../config/openid-configuration';
import { inject, Injectable } from 'injection-js';
import { OpenIdConfiguration } from '../config/openid-configuration';
import { LoggerService } from '../logging/logger.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';
@Injectable()
@ -18,7 +18,7 @@ export class FlowsDataService {
createNonce(configuration: OpenIdConfiguration): string {
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);
return nonce;

View File

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

View File

@ -1,8 +1,8 @@
import { Injectable, inject } from 'injection-js';
import type { Observable } from 'rxjs';
import { inject, Injectable } from 'injection-js';
import { Observable } from 'rxjs';
import { concatMap } from 'rxjs/operators';
import type { OpenIdConfiguration } from '../config/openid-configuration';
import type { CallbackContext } from './callback-context';
import { OpenIdConfiguration } from '../config/openid-configuration';
import { CallbackContext } from './callback-context';
import { CodeFlowCallbackHandlerService } from './callback-handling/code-flow-callback-handler.service';
import { HistoryJwtKeysCallbackHandlerService } from './callback-handling/history-jwt-keys-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 { mockProvider } from '../../testing/mock';
import { CryptoService } from '../../utils/crypto/crypto.service';
import { RandomService } from './random.service';
@ -11,6 +11,9 @@ describe('RandomService Tests', () => {
TestBed.configureTestingModule({
providers: [RandomService, mockProvider(LoggerService), CryptoService],
});
});
beforeEach(() => {
randomService = TestBed.inject(RandomService);
});

View File

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

View File

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

View File

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

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