fix: fix some tests

This commit is contained in:
master 2025-02-02 00:45:46 +08:00
parent 28da493462
commit 6a03a2bd62
93 changed files with 2671 additions and 1622 deletions

View File

@ -135,7 +135,7 @@ export class AppComponent implements OnInit {
} }
login() { login() {
this.oidcSecurityService.authorize(); this.oidcSecurityService.authorize().subscribe();
} }
logout() { logout() {

View File

@ -26,15 +26,14 @@
"scripts": { "scripts": {
"build": "rslib build", "build": "rslib build",
"dev": "rslib build --watch", "dev": "rslib build --watch",
"test": "vitest --browser.headless", "test": "vitest --coverage",
"test-ci": "vitest --watch=false --browser.headless --coverage", "test-ci": "vitest --watch=false --coverage",
"pack": "npm run build && npm pack ./dist", "pack": "npm run build && npm pack ./dist",
"publish": "npm run build && npm publish ./dist", "publish": "npm run build && npm publish ./dist",
"coverage": "vitest run --coverage", "coverage": "vitest run --coverage",
"lint": "ultracite lint", "lint": "ultracite lint",
"format": "ultracite format", "format": "ultracite format",
"cli": "tsx scripts/cli.ts", "cli": "tsx scripts/cli.ts"
"test:browser": "vitest --workspace=vitest.workspace.ts"
}, },
"dependencies": { "dependencies": {
"@ngify/http": "^2.0.4", "@ngify/http": "^2.0.4",
@ -51,11 +50,14 @@
"@evilmartians/lefthook": "^1.0.3", "@evilmartians/lefthook": "^1.0.3",
"@playwright/test": "^1.49.1", "@playwright/test": "^1.49.1",
"@rslib/core": "^0.3.1", "@rslib/core": "^0.3.1",
"@swc/core": "^1.10.12",
"@types/jsdom": "^21.1.7",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^22.12.0", "@types/node": "^22.12.0",
"@vitest/browser": "^3.0.4", "@vitest/browser": "^3.0.4",
"@vitest/coverage-v8": "^3.0.4", "@vitest/coverage-v8": "^3.0.4",
"commander": "^13.1.0", "commander": "^13.1.0",
"jsdom": "^26.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"oxc-parser": "^0.48.1", "oxc-parser": "^0.48.1",
"oxc-walker": "^0.2.2", "oxc-walker": "^0.2.2",
@ -65,9 +67,9 @@
"tsx": "^4.19.2", "tsx": "^4.19.2",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"ultracite": "^4.1.15", "ultracite": "^4.1.15",
"unplugin-swc": "^1.5.1",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.4", "vitest": "^3.0.4"
"webdriverio": "^9.7.2"
}, },
"keywords": [ "keywords": [
"rxjs", "rxjs",

805
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
import { Command } from 'commander'; import { Command } from 'commander';
import { rewriteAllObservableSubscribeToLastValueFrom } from './code-transform'; import { rewriteAllObservableSubscribeTofirstValueFrom } from './code-transform';
const program = new Command(); const program = new Command();
@ -13,7 +13,7 @@ program
.command('rewrite <pattern>') .command('rewrite <pattern>')
.description('Rewrite files matching the given glob pattern') .description('Rewrite files matching the given glob pattern')
.action(async (pattern: string) => { .action(async (pattern: string) => {
await rewriteAllObservableSubscribeToLastValueFrom(pattern); await rewriteAllObservableSubscribeTofirstValueFrom(pattern);
}); });
program.parse(process.argv); program.parse(process.argv);

View File

@ -1,11 +1,11 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { describe, it } from 'node:test'; import { describe, it } from 'node:test';
import { Biome, Distribution } from '@biomejs/js-api'; import { Biome, Distribution } from '@biomejs/js-api';
import { rewriteObservableSubscribeToLastValueFrom } from './code-transform'; import { rewriteObservableSubscribeTofirstValueFrom } from './code-transform';
describe('rewriteSpecObservableSubscribeToLastValueFrom', () => { describe('rewriteSpecObservableSubscribeTofirstValueFrom', () => {
it('should transform simple example valid string', async () => { it('should transform simple example valid string', async () => {
const actual = await rewriteObservableSubscribeToLastValueFrom( const actual = await rewriteObservableSubscribeTofirstValueFrom(
'index.ts', 'index.ts',
`refreshSessionIframeService `refreshSessionIframeService
.refreshSessionWithIframe(allConfigs[0]!, allConfigs) .refreshSessionWithIframe(allConfigs[0]!, allConfigs)
@ -20,7 +20,7 @@ describe('rewriteSpecObservableSubscribeToLastValueFrom', () => {
});` });`
); );
const expect = `const result = await lastValueFrom(refreshSessionIframeService.refreshSessionWithIframe(allConfigs[0]!, allConfigs)); const expect = `const result = await firstValueFrom(refreshSessionIframeService.refreshSessionWithIframe(allConfigs[0]!, allConfigs));
expect(result).toHaveBeenCalledExactlyOnceWith('a-url',allConfigs[0]!,allConfigs);`; expect(result).toHaveBeenCalledExactlyOnceWith('a-url',allConfigs[0]!,allConfigs);`;
const biome = await Biome.create({ const biome = await Biome.create({
@ -34,7 +34,7 @@ describe('rewriteSpecObservableSubscribeToLastValueFrom', () => {
}); });
it('should rewrite complex exmaple to valid string', async () => { it('should rewrite complex exmaple to valid string', async () => {
const actual = await rewriteObservableSubscribeToLastValueFrom( const actual = await rewriteObservableSubscribeTofirstValueFrom(
'index.ts', 'index.ts',
`codeFlowCallbackService `codeFlowCallbackService
.authenticatedCallbackWithCode('some-url4', config, [config]) .authenticatedCallbackWithCode('some-url4', config, [config])
@ -56,7 +56,7 @@ describe('rewriteSpecObservableSubscribeToLastValueFrom', () => {
const expect = ` const expect = `
try { try {
const abc = await lastValueFrom(codeFlowCallbackService.authenticatedCallbackWithCode('some-url4', config, [config])); const abc = await firstValueFrom(codeFlowCallbackService.authenticatedCallbackWithCode('some-url4', config, [config]));
expect(abc).toBeTruthy(); expect(abc).toBeTruthy();
} catch (err: any) { } catch (err: any) {
if (err instanceof EmptyError) { if (err instanceof EmptyError) {

View File

@ -21,7 +21,7 @@ function sourceTextFromNode(
return magicString.getSourceText(start, end); return magicString.getSourceText(start, end);
} }
export async function rewriteObservableSubscribeToLastValueFrom( export async function rewriteObservableSubscribeTofirstValueFrom(
filename: string, filename: string,
content?: string content?: string
) { ) {
@ -83,7 +83,7 @@ export async function rewriteObservableSubscribeToLastValueFrom(
error = args[1]; error = args[1];
complete = args[2]; complete = args[2];
} }
let newContent = `await lastValueFrom(${sourceTextFromNode(context, child.expression.callee.object)});`; let newContent = `await firstValueFrom(${sourceTextFromNode(context, child.expression.callee.object)});`;
if (next) { if (next) {
const nextParam = const nextParam =
@ -161,12 +161,12 @@ export async function rewriteObservableSubscribeToLastValueFrom(
return result; return result;
} }
export async function rewriteAllObservableSubscribeToLastValueFrom( export async function rewriteAllObservableSubscribeTofirstValueFrom(
pattern: string | string[] pattern: string | string[]
) { ) {
const files = fsp.glob(pattern); const files = fsp.glob(pattern);
for await (const file of files) { for await (const file of files) {
const result = await rewriteObservableSubscribeToLastValueFrom(file); const result = await rewriteObservableSubscribeTofirstValueFrom(file);
await fsp.writeFile(file, result, 'utf-8'); await fsp.writeFile(file, result, 'utf-8');
} }

View File

@ -3,7 +3,7 @@ import { provideHttpClientTesting } from '@/testing/http';
import { HttpHeaders } from '@ngify/http'; import { HttpHeaders } from '@ngify/http';
import { HttpTestingController } from '@ngify/http/testing'; import { HttpTestingController } from '@ngify/http/testing';
import { provideHttpClient, withInterceptorsFromDi } from 'oidc-client-rx'; import { provideHttpClient, withInterceptorsFromDi } from 'oidc-client-rx';
import { lastValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { DataService } from './data.service'; import { DataService } from './data.service';
import { HttpBaseService } from './http-base.service'; import { HttpBaseService } from './http-base.service';
@ -33,7 +33,7 @@ describe('Data Service', () => {
it('get call sets the accept header', async () => { it('get call sets the accept header', async () => {
const url = 'testurl'; const url = 'testurl';
const data = await lastValueFrom( const data = await firstValueFrom(
dataService.get(url, { configId: 'configId1' }) dataService.get(url, { configId: 'configId1' })
); );
expect(data).toBe('bodyData'); expect(data).toBe('bodyData');
@ -51,7 +51,7 @@ describe('Data Service', () => {
const url = 'testurl'; const url = 'testurl';
const token = 'token'; const token = 'token';
const data = await lastValueFrom( const data = await firstValueFrom(
dataService.get(url, { configId: 'configId1' }, token) dataService.get(url, { configId: 'configId1' }, token)
); );
expect(data).toBe('bodyData'); expect(data).toBe('bodyData');
@ -69,7 +69,7 @@ describe('Data Service', () => {
it('call without ngsw-bypass param by default', async () => { it('call without ngsw-bypass param by default', async () => {
const url = 'testurl'; const url = 'testurl';
const data = await lastValueFrom( const data = await firstValueFrom(
dataService.get(url, { configId: 'configId1' }) dataService.get(url, { configId: 'configId1' })
); );
expect(data).toBe('bodyData'); expect(data).toBe('bodyData');
@ -87,7 +87,7 @@ describe('Data Service', () => {
it('call with ngsw-bypass param', async () => { it('call with ngsw-bypass param', async () => {
const url = 'testurl'; const url = 'testurl';
const data = await lastValueFrom( const data = await firstValueFrom(
dataService.get(url, { configId: 'configId1', ngswBypass: true }) dataService.get(url, { configId: 'configId1', ngswBypass: true })
); );
expect(data).toBe('bodyData'); expect(data).toBe('bodyData');
@ -107,8 +107,9 @@ describe('Data Service', () => {
it('call sets the accept header when no other params given', async () => { it('call sets the accept header when no other params given', async () => {
const url = 'testurl'; const url = 'testurl';
await lastValueFrom(dataService await firstValueFrom(
.post(url, { some: 'thing' }, { configId: 'configId1' })); dataService.post(url, { some: 'thing' }, { configId: 'configId1' })
);
const req = httpMock.expectOne(url); const req = httpMock.expectOne(url);
expect(req.request.method).toBe('POST'); expect(req.request.method).toBe('POST');
@ -125,7 +126,7 @@ describe('Data Service', () => {
headers = headers.set('X-MyHeader', 'Genesis'); headers = headers.set('X-MyHeader', 'Genesis');
await lastValueFrom( await firstValueFrom(
dataService.post( dataService.post(
url, url,
{ some: 'thing' }, { some: 'thing' },
@ -147,7 +148,7 @@ describe('Data Service', () => {
it('call without ngsw-bypass param by default', async () => { it('call without ngsw-bypass param by default', async () => {
const url = 'testurl'; const url = 'testurl';
await lastValueFrom( await firstValueFrom(
dataService.post(url, { some: 'thing' }, { configId: 'configId1' }) dataService.post(url, { some: 'thing' }, { configId: 'configId1' })
); );
const req = httpMock.expectOne(url); const req = httpMock.expectOne(url);
@ -164,7 +165,7 @@ describe('Data Service', () => {
it('call with ngsw-bypass param', async () => { it('call with ngsw-bypass param', async () => {
const url = 'testurl'; const url = 'testurl';
await lastValueFrom( await firstValueFrom(
dataService.post( dataService.post(
url, url,
{ some: 'thing' }, { some: 'thing' },

View File

@ -1,7 +1,8 @@
import { HttpHeaders, HttpParams } from '@ngify/http'; import { HttpHeaders } from '@ngify/http';
import { Injectable, inject } from 'injection-js'; import { Injectable, inject } from 'injection-js';
import type { Observable } from 'rxjs'; import type { Observable } from 'rxjs';
import type { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
import { HttpParams } from '../http';
import { HttpBaseService } from './http-base.service'; import { HttpBaseService } from './http-base.service';
const NGSW_CUSTOM_PARAM = 'ngsw-bypass'; const NGSW_CUSTOM_PARAM = 'ngsw-bypass';

View File

@ -257,7 +257,7 @@ describe('Auth State Service', () => {
[{ configId: 'configId1' }] [{ configId: 'configId1' }]
); );
expect(spy).toHaveBeenCalledTimes(2); expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenCalledWith([ expect(spy.mock.calls).toEqual([
['authzData', 'accesstoken', { configId: 'configId1' }], ['authzData', 'accesstoken', { configId: 'configId1' }],
[ [
'access_token_expires_at', 'access_token_expires_at',

View File

@ -4,7 +4,7 @@ import {
mockRouterProvider, mockRouterProvider,
spyOnProperty, spyOnProperty,
} from '@/testing'; } from '@/testing';
import { lastValueFrom, of, throwError } from 'rxjs'; import { firstValueFrom, of, throwError } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { AutoLoginService } from '../auto-login/auto-login.service'; import { AutoLoginService } from '../auto-login/auto-login.service';
import { CallbackService } from '../callback/callback.service'; import { CallbackService } from '../callback/callback.service';
@ -49,6 +49,8 @@ describe('CheckAuthService', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [],
providers: [ providers: [
CheckAuthService,
StoragePersistenceService,
mockRouterProvider(), mockRouterProvider(),
mockProvider(CheckSessionService), mockProvider(CheckSessionService),
mockProvider(SilentRenewService), mockProvider(SilentRenewService),
@ -109,7 +111,7 @@ describe('CheckAuthService', () => {
); );
const spy = vi.spyOn(checkAuthService as any, 'checkAuthWithConfig'); const spy = vi.spyOn(checkAuthService as any, 'checkAuthWithConfig');
await lastValueFrom( await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(spy).toHaveBeenCalledExactlyOnceWith( expect(spy).toHaveBeenCalledExactlyOnceWith(
@ -136,7 +138,7 @@ describe('CheckAuthService', () => {
const spy = vi.spyOn(checkAuthService as any, 'checkAuthWithConfig'); const spy = vi.spyOn(checkAuthService as any, 'checkAuthWithConfig');
try { try {
await lastValueFrom( await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
} catch (err: any) { } catch (err: any) {
@ -155,7 +157,7 @@ describe('CheckAuthService', () => {
]; ];
const spy = vi.spyOn(checkAuthService as any, 'checkAuthWithConfig'); const spy = vi.spyOn(checkAuthService as any, 'checkAuthWithConfig');
await lastValueFrom( await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(spy).toHaveBeenCalledExactlyOnceWith( expect(spy).toHaveBeenCalledExactlyOnceWith(
@ -184,7 +186,7 @@ describe('CheckAuthService', () => {
vi.spyOn(popUpService, 'isCurrentlyInPopup').mockReturnValue(true); vi.spyOn(popUpService, 'isCurrentlyInPopup').mockReturnValue(true);
const popupSpy = vi.spyOn(popUpService, 'sendMessageToMainWindow'); const popupSpy = vi.spyOn(popUpService, 'sendMessageToMainWindow');
const result = await lastValueFrom( const result = await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(result).toEqual({ expect(result).toEqual({
@ -216,7 +218,7 @@ describe('CheckAuthService', () => {
'http://localhost:4200' 'http://localhost:4200'
); );
const result = await lastValueFrom( const result = await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(result).toEqual({ expect(result).toEqual({
@ -249,7 +251,7 @@ describe('CheckAuthService', () => {
.spyOn(callBackService, 'handleCallbackAndFireEvents') .spyOn(callBackService, 'handleCallbackAndFireEvents')
.mockReturnValue(of({} as CallbackContext)); .mockReturnValue(of({} as CallbackContext));
const result = await lastValueFrom( const result = await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(result).toEqual({ expect(result).toEqual({
@ -282,7 +284,7 @@ describe('CheckAuthService', () => {
vi.spyOn(authStateService, 'getAccessToken').mockReturnValue('at'); vi.spyOn(authStateService, 'getAccessToken').mockReturnValue('at');
vi.spyOn(authStateService, 'getIdToken').mockReturnValue('idt'); vi.spyOn(authStateService, 'getIdToken').mockReturnValue('idt');
const result = await lastValueFrom( const result = await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(result).toEqual({ expect(result).toEqual({
@ -322,7 +324,7 @@ describe('CheckAuthService', () => {
); );
const userServiceSpy = vi.spyOn(userService, 'publishUserDataIfExists'); const userServiceSpy = vi.spyOn(userService, 'publishUserDataIfExists');
const result = await lastValueFrom( const result = await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(result).toEqual({ expect(result).toEqual({
@ -362,7 +364,7 @@ describe('CheckAuthService', () => {
); );
const userServiceSpy = vi.spyOn(userService, 'publishUserDataIfExists'); const userServiceSpy = vi.spyOn(userService, 'publishUserDataIfExists');
const result = await lastValueFrom( const result = await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(result).toEqual({ expect(result).toEqual({
@ -393,7 +395,7 @@ describe('CheckAuthService', () => {
true true
); );
const result = await lastValueFrom( const result = await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(result).toEqual({ expect(result).toEqual({
@ -420,7 +422,7 @@ describe('CheckAuthService', () => {
const spy = vi.spyOn(authStateService, 'setAuthenticatedAndFireEvent'); const spy = vi.spyOn(authStateService, 'setAuthenticatedAndFireEvent');
await lastValueFrom( await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
@ -443,7 +445,7 @@ describe('CheckAuthService', () => {
const spy = vi.spyOn(userService, 'publishUserDataIfExists'); const spy = vi.spyOn(userService, 'publishUserDataIfExists');
await lastValueFrom( await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
@ -470,7 +472,7 @@ describe('CheckAuthService', () => {
'startTokenValidationPeriodically' 'startTokenValidationPeriodically'
); );
await lastValueFrom( await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
@ -495,7 +497,7 @@ describe('CheckAuthService', () => {
); );
const spy = vi.spyOn(checkSessionService, 'start'); const spy = vi.spyOn(checkSessionService, 'start');
await lastValueFrom( await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
@ -520,7 +522,7 @@ describe('CheckAuthService', () => {
); );
const spy = vi.spyOn(silentRenewService, 'getOrCreateIframe'); const spy = vi.spyOn(silentRenewService, 'getOrCreateIframe');
await lastValueFrom( await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
@ -545,7 +547,7 @@ describe('CheckAuthService', () => {
'checkSavedRedirectRouteAndNavigate' 'checkSavedRedirectRouteAndNavigate'
); );
await lastValueFrom( await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(1);
@ -568,7 +570,7 @@ describe('CheckAuthService', () => {
'checkSavedRedirectRouteAndNavigate' 'checkSavedRedirectRouteAndNavigate'
); );
await lastValueFrom( await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(spy).toHaveBeenCalledTimes(0); expect(spy).toHaveBeenCalledTimes(0);
@ -588,10 +590,10 @@ describe('CheckAuthService', () => {
const fireEventSpy = vi.spyOn(publicEventsService, 'fireEvent'); const fireEventSpy = vi.spyOn(publicEventsService, 'fireEvent');
await lastValueFrom( await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(fireEventSpy).toHaveBeenCalledWith([ expect(fireEventSpy.mock.calls).toEqual([
[EventTypes.CheckingAuth], [EventTypes.CheckingAuth],
[EventTypes.CheckingAuthFinished], [EventTypes.CheckingAuthFinished],
]); ]);
@ -611,10 +613,10 @@ describe('CheckAuthService', () => {
'http://localhost:4200' 'http://localhost:4200'
); );
await lastValueFrom( await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(fireEventSpy).toHaveBeenCalledWith([ expect(fireEventSpy.mock.calls).toEqual([
[EventTypes.CheckingAuth], [EventTypes.CheckingAuth],
[EventTypes.CheckingAuthFinishedWithError, 'ERROR'], [EventTypes.CheckingAuthFinishedWithError, 'ERROR'],
]); ]);
@ -634,10 +636,10 @@ describe('CheckAuthService', () => {
const fireEventSpy = vi.spyOn(publicEventsService, 'fireEvent'); const fireEventSpy = vi.spyOn(publicEventsService, 'fireEvent');
await lastValueFrom( await firstValueFrom(
checkAuthService.checkAuth(allConfigs[0]!, allConfigs) checkAuthService.checkAuth(allConfigs[0]!, allConfigs)
); );
expect(fireEventSpy).toBeCalledWith([ expect(fireEventSpy.mock.calls).toEqual([
[EventTypes.CheckingAuth], [EventTypes.CheckingAuth],
[EventTypes.CheckingAuthFinished], [EventTypes.CheckingAuthFinished],
]); ]);
@ -665,7 +667,7 @@ describe('CheckAuthService', () => {
); );
const spy = vi.spyOn(silentRenewService, 'getOrCreateIframe'); const spy = vi.spyOn(silentRenewService, 'getOrCreateIframe');
await lastValueFrom( await firstValueFrom(
checkAuthService.checkAuthIncludingServer(allConfigs[0]!, allConfigs) checkAuthService.checkAuthIncludingServer(allConfigs[0]!, allConfigs)
); );
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
@ -694,7 +696,7 @@ describe('CheckAuthService', () => {
}) })
); );
const result = await lastValueFrom( const result = await firstValueFrom(
checkAuthService.checkAuthIncludingServer(allConfigs[0]!, allConfigs) checkAuthService.checkAuthIncludingServer(allConfigs[0]!, allConfigs)
); );
expect(result).toBeTruthy(); expect(result).toBeTruthy();
@ -742,7 +744,7 @@ describe('CheckAuthService', () => {
}) })
); );
await lastValueFrom( await firstValueFrom(
checkAuthService.checkAuthIncludingServer(allConfigs[0]!, allConfigs) checkAuthService.checkAuthIncludingServer(allConfigs[0]!, allConfigs)
); );
expect(checkSessionServiceStartSpy).toHaveBeenCalledExactlyOnceWith( expect(checkSessionServiceStartSpy).toHaveBeenCalledExactlyOnceWith(
@ -796,7 +798,7 @@ describe('CheckAuthService', () => {
}) })
); );
await lastValueFrom( await firstValueFrom(
checkAuthService.checkAuthIncludingServer(allConfigs[0]!, allConfigs) checkAuthService.checkAuthIncludingServer(allConfigs[0]!, allConfigs)
); );
expect(checkSessionServiceStartSpy).toHaveBeenCalledExactlyOnceWith( expect(checkSessionServiceStartSpy).toHaveBeenCalledExactlyOnceWith(
@ -825,7 +827,7 @@ describe('CheckAuthService', () => {
); );
const spy = vi.spyOn(checkAuthService as any, 'checkAuthWithConfig'); const spy = vi.spyOn(checkAuthService as any, 'checkAuthWithConfig');
const result = await lastValueFrom( const result = await firstValueFrom(
checkAuthService.checkAuthMultiple(allConfigs) checkAuthService.checkAuthMultiple(allConfigs)
); );
expect(Array.isArray(result)).toBe(true); expect(Array.isArray(result)).toBe(true);
@ -855,11 +857,11 @@ describe('CheckAuthService', () => {
const spy = vi.spyOn(checkAuthService as any, 'checkAuthWithConfig'); const spy = vi.spyOn(checkAuthService as any, 'checkAuthWithConfig');
const result = await lastValueFrom( const result = await firstValueFrom(
checkAuthService.checkAuthMultiple(allConfigs) checkAuthService.checkAuthMultiple(allConfigs)
); );
expect(Array.isArray(result)).toBe(true); expect(Array.isArray(result)).toBe(true);
expect(spy).toBeCalledWith([ expect(spy.mock.calls).toEqual([
[ [
{ configId: 'configId1', authority: 'some-authority1' }, { configId: 'configId1', authority: 'some-authority1' },
allConfigs, allConfigs,
@ -886,7 +888,7 @@ describe('CheckAuthService', () => {
const spy = vi.spyOn(checkAuthService as any, 'checkAuthWithConfig'); const spy = vi.spyOn(checkAuthService as any, 'checkAuthWithConfig');
const result = await lastValueFrom( const result = await firstValueFrom(
checkAuthService.checkAuthMultiple(allConfigs) checkAuthService.checkAuthMultiple(allConfigs)
); );
expect(Array.isArray(result)).toBe(true); expect(Array.isArray(result)).toBe(true);
@ -912,7 +914,7 @@ describe('CheckAuthService', () => {
const allConfigs: OpenIdConfiguration[] = []; const allConfigs: OpenIdConfiguration[] = [];
try { try {
await lastValueFrom(checkAuthService.checkAuthMultiple(allConfigs)); await firstValueFrom(checkAuthService.checkAuthMultiple(allConfigs));
} catch (error: any) { } catch (error: any) {
expect(error.message).toEqual( expect(error.message).toEqual(
'could not find matching config for state the-state-param' 'could not find matching config for state the-state-param'

View File

@ -4,7 +4,7 @@ import {
type ActivatedRouteSnapshot, type ActivatedRouteSnapshot,
type RouterStateSnapshot, type RouterStateSnapshot,
} from 'oidc-client-rx'; } from 'oidc-client-rx';
import { lastValueFrom, of } from 'rxjs'; import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { AuthStateService } from '../auth-state/auth-state.service'; import { AuthStateService } from '../auth-state/auth-state.service';
import { CheckAuthService } from '../auth-state/check-auth.service'; import { CheckAuthService } from '../auth-state/check-auth.service';
@ -24,6 +24,7 @@ describe('AutoLoginPartialRoutesGuard', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [],
providers: [ providers: [
AutoLoginPartialRoutesGuard,
mockRouterProvider(), mockRouterProvider(),
AutoLoginService, AutoLoginService,
mockProvider(AuthStateService), mockProvider(AuthStateService),
@ -83,21 +84,20 @@ describe('AutoLoginPartialRoutesGuard', () => {
); );
const loginSpy = vi.spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
await lastValueFrom(guard await firstValueFrom(
.canActivate( guard.canActivate(
{} as ActivatedRouteSnapshot, {} as ActivatedRouteSnapshot,
{ url: 'some-url1' } as RouterStateSnapshot { url: 'some-url1' } as RouterStateSnapshot
)); )
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( );
{ configId: 'configId1' }, expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
'some-url1' { configId: 'configId1' },
);; 'some-url1'
expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ );
configId: 'configId1', expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
});; configId: 'configId1',
expect( });
checkSavedRedirectRouteAndNavigateSpy expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
).not.toHaveBeenCalled();
}); });
it('should save current route and call `login` if not authenticated already and add custom params', async () => { it('should save current route and call `login` if not authenticated already and add custom params', async () => {
@ -114,22 +114,21 @@ expect(
); );
const loginSpy = vi.spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
await lastValueFrom(guard await firstValueFrom(
.canActivate( guard.canActivate(
{ data: { custom: 'param' } } as unknown as ActivatedRouteSnapshot, { data: { custom: 'param' } } as unknown as ActivatedRouteSnapshot,
{ url: 'some-url1' } as RouterStateSnapshot { url: 'some-url1' } as RouterStateSnapshot
)); )
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( );
{ configId: 'configId1' }, expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
'some-url1' { configId: 'configId1' },
);; 'some-url1'
expect(loginSpy).toHaveBeenCalledExactlyOnceWith( );
{ configId: 'configId1' }, expect(loginSpy).toHaveBeenCalledExactlyOnceWith(
{ customParams: { custom: 'param' } } { configId: 'configId1' },
);; { customParams: { custom: 'param' } }
expect( );
checkSavedRedirectRouteAndNavigateSpy expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
).not.toHaveBeenCalled();
}); });
it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => { it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => {
@ -146,16 +145,17 @@ expect(
); );
const loginSpy = vi.spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
await lastValueFrom(guard await firstValueFrom(
.canActivate( guard.canActivate(
{} as ActivatedRouteSnapshot, {} as ActivatedRouteSnapshot,
{ url: 'some-url1' } as RouterStateSnapshot { url: 'some-url1' } as RouterStateSnapshot
)); )
expect(saveRedirectRouteSpy).not.toHaveBeenCalled();; );
expect(loginSpy).not.toHaveBeenCalled();; expect(saveRedirectRouteSpy).not.toHaveBeenCalled();
expect( expect(loginSpy).not.toHaveBeenCalled();
checkSavedRedirectRouteAndNavigateSpy expect(
).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' }); checkSavedRedirectRouteAndNavigateSpy
).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' });
}); });
}); });
@ -174,21 +174,20 @@ expect(
); );
const loginSpy = vi.spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
await lastValueFrom(guard await firstValueFrom(
.canActivateChild( guard.canActivateChild(
{} as ActivatedRouteSnapshot, {} as ActivatedRouteSnapshot,
{ url: 'some-url1' } as RouterStateSnapshot { url: 'some-url1' } as RouterStateSnapshot
)); )
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( );
{ configId: 'configId1' }, expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
'some-url1' { configId: 'configId1' },
);; 'some-url1'
expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ );
configId: 'configId1', expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
});; configId: 'configId1',
expect( });
checkSavedRedirectRouteAndNavigateSpy expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
).not.toHaveBeenCalled();
}); });
it('should save current route and call `login` if not authenticated already with custom params', async () => { it('should save current route and call `login` if not authenticated already with custom params', async () => {
@ -205,22 +204,21 @@ expect(
); );
const loginSpy = vi.spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
await lastValueFrom(guard await firstValueFrom(
.canActivateChild( guard.canActivateChild(
{ data: { custom: 'param' } } as unknown as ActivatedRouteSnapshot, { data: { custom: 'param' } } as unknown as ActivatedRouteSnapshot,
{ url: 'some-url1' } as RouterStateSnapshot { url: 'some-url1' } as RouterStateSnapshot
)); )
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( );
{ configId: 'configId1' }, expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
'some-url1' { configId: 'configId1' },
);; 'some-url1'
expect(loginSpy).toHaveBeenCalledExactlyOnceWith( );
{ configId: 'configId1' }, expect(loginSpy).toHaveBeenCalledExactlyOnceWith(
{ customParams: { custom: 'param' } } { configId: 'configId1' },
);; { customParams: { custom: 'param' } }
expect( );
checkSavedRedirectRouteAndNavigateSpy expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
).not.toHaveBeenCalled();
}); });
it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => { it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => {
@ -237,16 +235,17 @@ expect(
); );
const loginSpy = vi.spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
await lastValueFrom(guard await firstValueFrom(
.canActivateChild( guard.canActivateChild(
{} as ActivatedRouteSnapshot, {} as ActivatedRouteSnapshot,
{ url: 'some-url1' } as RouterStateSnapshot { url: 'some-url1' } as RouterStateSnapshot
)); )
expect(saveRedirectRouteSpy).not.toHaveBeenCalled();; );
expect(loginSpy).not.toHaveBeenCalled();; expect(saveRedirectRouteSpy).not.toHaveBeenCalled();
expect( expect(loginSpy).not.toHaveBeenCalled();
checkSavedRedirectRouteAndNavigateSpy expect(
).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' }); checkSavedRedirectRouteAndNavigateSpy
).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' });
}); });
}); });
@ -265,15 +264,15 @@ expect(
); );
const loginSpy = vi.spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
await lastValueFrom(guard.canLoad()); await firstValueFrom(guard.canLoad());
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
'' ''
);; );
expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
configId: 'configId1', configId: 'configId1',
});; });
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
}); });
it('should save current route (with router extractedUrl) and call `login` if not authenticated already', async () => { it('should save current route (with router extractedUrl) and call `login` if not authenticated already', async () => {
@ -301,15 +300,15 @@ expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
trigger: 'imperative', trigger: 'imperative',
}); });
await lastValueFrom(guard.canLoad()); await firstValueFrom(guard.canLoad());
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
'some-url12/with/some-param?queryParam=true' 'some-url12/with/some-param?queryParam=true'
);; );
expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
configId: 'configId1', configId: 'configId1',
});; });
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
}); });
it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => { it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => {
@ -326,12 +325,12 @@ expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
); );
const loginSpy = vi.spyOn(loginService, 'login'); const loginSpy = vi.spyOn(loginService, 'login');
await lastValueFrom(guard.canLoad()); await firstValueFrom(guard.canLoad());
expect(saveRedirectRouteSpy).not.toHaveBeenCalled();; expect(saveRedirectRouteSpy).not.toHaveBeenCalled();
expect(loginSpy).not.toHaveBeenCalled();; expect(loginSpy).not.toHaveBeenCalled();
expect( expect(
checkSavedRedirectRouteAndNavigateSpy checkSavedRedirectRouteAndNavigateSpy
).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' }); ).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' });
}); });
}); });
}); });
@ -383,15 +382,15 @@ expect(
autoLoginPartialRoutesGuard autoLoginPartialRoutesGuard
); );
await lastValueFrom(guard$); await firstValueFrom(guard$);
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
'' ''
);; );
expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
configId: 'configId1', configId: 'configId1',
});; });
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
}); });
it('should save current route (with router extractedUrl) and call `login` if not authenticated already', async () => { it('should save current route (with router extractedUrl) and call `login` if not authenticated already', async () => {
@ -423,15 +422,15 @@ expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
autoLoginPartialRoutesGuard autoLoginPartialRoutesGuard
); );
await lastValueFrom(guard$); await firstValueFrom(guard$);
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
'some-url12/with/some-param?queryParam=true' 'some-url12/with/some-param?queryParam=true'
);; );
expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
configId: 'configId1', configId: 'configId1',
});; });
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
}); });
it('should save current route and call `login` if not authenticated already and add custom params', async () => { it('should save current route and call `login` if not authenticated already and add custom params', async () => {
@ -454,16 +453,16 @@ expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
} as unknown as ActivatedRouteSnapshot) } as unknown as ActivatedRouteSnapshot)
); );
await lastValueFrom(guard$); await firstValueFrom(guard$);
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
'' ''
);; );
expect(loginSpy).toHaveBeenCalledExactlyOnceWith( expect(loginSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
{ customParams: { custom: 'param' } } { customParams: { custom: 'param' } }
);; );
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
}); });
it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => { it('should call `checkSavedRedirectRouteAndNavigate` if authenticated already', async () => {
@ -484,12 +483,12 @@ expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
autoLoginPartialRoutesGuard autoLoginPartialRoutesGuard
); );
await lastValueFrom(guard$); await firstValueFrom(guard$);
expect(saveRedirectRouteSpy).not.toHaveBeenCalled();; expect(saveRedirectRouteSpy).not.toHaveBeenCalled();
expect(loginSpy).not.toHaveBeenCalled();; expect(loginSpy).not.toHaveBeenCalled();
expect( expect(
checkSavedRedirectRouteAndNavigateSpy checkSavedRedirectRouteAndNavigateSpy
).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' }); ).toHaveBeenCalledExactlyOnceWith({ configId: 'configId1' });
}); });
}); });
@ -537,15 +536,15 @@ expect(
autoLoginPartialRoutesGuardWithConfig('configId1') autoLoginPartialRoutesGuardWithConfig('configId1')
); );
await lastValueFrom(guard$); await firstValueFrom(guard$);
expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith( expect(saveRedirectRouteSpy).toHaveBeenCalledExactlyOnceWith(
{ configId: 'configId1' }, { configId: 'configId1' },
'' ''
);; );
expect(loginSpy).toHaveBeenCalledExactlyOnceWith({ expect(loginSpy).toHaveBeenCalledExactlyOnceWith({
configId: 'configId1', configId: 'configId1',
});; });
expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled(); expect(checkSavedRedirectRouteAndNavigateSpy).not.toHaveBeenCalled();
}); });
}); });
}); });

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { Observable, lastValueFrom, of } from 'rxjs'; import { Observable, firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import type { CallbackContext } from '../flows/callback-context'; import type { CallbackContext } from '../flows/callback-context';
import { mockProvider } from '../testing/mock'; import { mockProvider } from '../testing/mock';
@ -59,7 +59,7 @@ describe('CallbackService ', () => {
.spyOn(codeFlowCallbackService, 'authenticatedCallbackWithCode') .spyOn(codeFlowCallbackService, 'authenticatedCallbackWithCode')
.mockReturnValue(of({} as CallbackContext)); .mockReturnValue(of({} as CallbackContext));
await lastValueFrom( await firstValueFrom(
callbackService.handleCallbackAndFireEvents( callbackService.handleCallbackAndFireEvents(
'anyUrl', 'anyUrl',
{ configId: 'configId1' }, { configId: 'configId1' },
@ -83,17 +83,16 @@ describe('CallbackService ', () => {
.spyOn(implicitFlowCallbackService, 'authenticatedImplicitFlowCallback') .spyOn(implicitFlowCallbackService, 'authenticatedImplicitFlowCallback')
.mockReturnValue(of({} as CallbackContext)); .mockReturnValue(of({} as CallbackContext));
await lastValueFrom( await firstValueFrom(
callbackService.handleCallbackAndFireEvents( callbackService.handleCallbackAndFireEvents(
'anyUrl', 'anyUrl',
{ configId: 'configId1' }, { configId: 'configId1' },
[{ configId: 'configId1' }] [{ configId: 'configId1' }]
) )
); );
expect(authorizedCallbackWithCodeSpy).toHaveBeenCalledWith( expect(authorizedCallbackWithCodeSpy.mock.calls).toEqual([
{ configId: 'configId1' }, [{ configId: 'configId1' }, [{ configId: 'configId1' }]],
[{ configId: 'configId1' }] ]);
);
}); });
it('calls authorizedImplicitFlowCallback with hash if current flow is implicit flow and callbackurl does include a hash', async () => { it('calls authorizedImplicitFlowCallback with hash if current flow is implicit flow and callbackurl does include a hash', async () => {
@ -105,7 +104,7 @@ describe('CallbackService ', () => {
.spyOn(implicitFlowCallbackService, 'authenticatedImplicitFlowCallback') .spyOn(implicitFlowCallbackService, 'authenticatedImplicitFlowCallback')
.mockReturnValue(of({} as CallbackContext)); .mockReturnValue(of({} as CallbackContext));
await lastValueFrom( await firstValueFrom(
callbackService.handleCallbackAndFireEvents( callbackService.handleCallbackAndFireEvents(
'anyUrlWithAHash#some-string', 'anyUrlWithAHash#some-string',
{ configId: 'configId1' }, { configId: 'configId1' },
@ -113,11 +112,9 @@ describe('CallbackService ', () => {
) )
); );
expect(authorizedCallbackWithCodeSpy).toHaveBeenCalledWith( expect(authorizedCallbackWithCodeSpy.mock.calls).toEqual([
{ configId: 'configId1' }, [{ configId: 'configId1' }, [{ configId: 'configId1' }], 'some-string'],
[{ configId: 'configId1' }], ]);
'some-string'
);
}); });
it('emits callbackinternal no matter which flow it is', async () => { it('emits callbackinternal no matter which flow it is', async () => {
@ -131,7 +128,7 @@ describe('CallbackService ', () => {
.spyOn(codeFlowCallbackService, 'authenticatedCallbackWithCode') .spyOn(codeFlowCallbackService, 'authenticatedCallbackWithCode')
.mockReturnValue(of({} as CallbackContext)); .mockReturnValue(of({} as CallbackContext));
await lastValueFrom( await firstValueFrom(
callbackService.handleCallbackAndFireEvents( callbackService.handleCallbackAndFireEvents(
'anyUrl', 'anyUrl',
{ configId: 'configId1' }, { configId: 'configId1' },

View File

@ -1,6 +1,6 @@
import { TestBed, mockRouterProvider } from '@/testing'; import { TestBed, mockRouterProvider } from '@/testing';
import { AbstractRouter } from 'oidc-client-rx'; import { AbstractRouter } from 'oidc-client-rx';
import { lastValueFrom, of, throwError } from 'rxjs'; import { firstValueFrom, of, throwError } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import type { CallbackContext } from '../flows/callback-context'; import type { CallbackContext } from '../flows/callback-context';
import { FlowsDataService } from '../flows/flows-data.service'; import { FlowsDataService } from '../flows/flows-data.service';
@ -85,7 +85,7 @@ describe('CodeFlowCallbackService ', () => {
triggerAuthorizationResultEvent: true, triggerAuthorizationResultEvent: true,
}; };
await lastValueFrom( await firstValueFrom(
codeFlowCallbackService.authenticatedCallbackWithCode( codeFlowCallbackService.authenticatedCallbackWithCode(
'some-url2', 'some-url2',
config, config,
@ -125,7 +125,7 @@ describe('CodeFlowCallbackService ', () => {
postLoginRoute: 'postLoginRoute', postLoginRoute: 'postLoginRoute',
}; };
await lastValueFrom( await firstValueFrom(
codeFlowCallbackService.authenticatedCallbackWithCode( codeFlowCallbackService.authenticatedCallbackWithCode(
'some-url3', 'some-url3',
config, config,
@ -163,7 +163,7 @@ describe('CodeFlowCallbackService ', () => {
}; };
try { try {
await lastValueFrom( await firstValueFrom(
codeFlowCallbackService.authenticatedCallbackWithCode( codeFlowCallbackService.authenticatedCallbackWithCode(
'some-url4', 'some-url4',
config, config,
@ -201,7 +201,7 @@ describe('CodeFlowCallbackService ', () => {
}; };
try { try {
await lastValueFrom( await firstValueFrom(
codeFlowCallbackService.authenticatedCallbackWithCode( codeFlowCallbackService.authenticatedCallbackWithCode(
'some-url5', 'some-url5',
config, config,

View File

@ -1,6 +1,6 @@
import { TestBed, mockRouterProvider } from '@/testing'; import { TestBed, mockRouterProvider } from '@/testing';
import { AbstractRouter } from 'oidc-client-rx'; import { AbstractRouter } from 'oidc-client-rx';
import { lastValueFrom, of, throwError } from 'rxjs'; import { firstValueFrom, of, throwError } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import type { CallbackContext } from '../flows/callback-context'; import type { CallbackContext } from '../flows/callback-context';
import { FlowsDataService } from '../flows/flows-data.service'; import { FlowsDataService } from '../flows/flows-data.service';
@ -20,6 +20,7 @@ describe('ImplicitFlowCallbackService ', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [],
providers: [ providers: [
ImplicitFlowCallbackService,
mockRouterProvider(), mockRouterProvider(),
mockProvider(FlowsService), mockProvider(FlowsService),
mockProvider(FlowsDataService), mockProvider(FlowsDataService),
@ -81,7 +82,7 @@ describe('ImplicitFlowCallbackService ', () => {
triggerAuthorizationResultEvent: true, triggerAuthorizationResultEvent: true,
}; };
await lastValueFrom( await firstValueFrom(
implicitFlowCallbackService.authenticatedImplicitFlowCallback( implicitFlowCallbackService.authenticatedImplicitFlowCallback(
config, config,
[config], [config],
@ -118,7 +119,7 @@ describe('ImplicitFlowCallbackService ', () => {
postLoginRoute: 'postLoginRoute', postLoginRoute: 'postLoginRoute',
}; };
await lastValueFrom( await firstValueFrom(
implicitFlowCallbackService.authenticatedImplicitFlowCallback( implicitFlowCallbackService.authenticatedImplicitFlowCallback(
config, config,
[config], [config],
@ -152,7 +153,7 @@ describe('ImplicitFlowCallbackService ', () => {
}; };
try { try {
await lastValueFrom( await firstValueFrom(
implicitFlowCallbackService.authenticatedImplicitFlowCallback( implicitFlowCallbackService.authenticatedImplicitFlowCallback(
config, config,
[config], [config],
@ -188,7 +189,7 @@ describe('ImplicitFlowCallbackService ', () => {
}; };
try { try {
await lastValueFrom( await firstValueFrom(
implicitFlowCallbackService.authenticatedImplicitFlowCallback( implicitFlowCallbackService.authenticatedImplicitFlowCallback(
config, config,
[config], [config],

View File

@ -7,8 +7,10 @@ describe('IntervalService', () => {
let intervalService: IntervalService; let intervalService: IntervalService;
beforeEach(() => { beforeEach(() => {
vi.useFakeTimers();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
IntervalService,
{ {
provide: Document, provide: Document,
useValue: { useValue: {
@ -22,6 +24,11 @@ describe('IntervalService', () => {
intervalService = TestBed.inject(IntervalService); intervalService = TestBed.inject(IntervalService);
}); });
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => {
vi.useRealTimers();
});
it('should create', () => { it('should create', () => {
expect(intervalService).toBeTruthy(); expect(intervalService).toBeTruthy();
}); });

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { lastValueFrom, of, throwError } from 'rxjs'; import { ReplaySubject, firstValueFrom, of, share, throwError } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { AuthStateService } from '../auth-state/auth-state.service'; import { AuthStateService } from '../auth-state/auth-state.service';
import { ConfigurationService } from '../config/config.service'; import { ConfigurationService } from '../config/config.service';
@ -33,9 +33,11 @@ describe('PeriodicallyTokenCheckService', () => {
let publicEventsService: PublicEventsService; let publicEventsService: PublicEventsService;
beforeEach(() => { beforeEach(() => {
vi.useFakeTimers();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [],
providers: [ providers: [
PeriodicallyTokenCheckService,
mockProvider(ResetAuthDataService), mockProvider(ResetAuthDataService),
FlowHelper, FlowHelper,
mockProvider(FlowsDataService), mockProvider(FlowsDataService),
@ -73,10 +75,11 @@ describe('PeriodicallyTokenCheckService', () => {
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation> // biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => { afterEach(() => {
if (intervalService.runTokenValidationRunning?.unsubscribe) { if (intervalService?.runTokenValidationRunning?.unsubscribe) {
intervalService.runTokenValidationRunning.unsubscribe(); intervalService.runTokenValidationRunning.unsubscribe();
intervalService.runTokenValidationRunning = null; intervalService.runTokenValidationRunning = null;
} }
vi.useRealTimers();
}); });
it('should create', () => { it('should create', () => {
@ -84,13 +87,22 @@ describe('PeriodicallyTokenCheckService', () => {
}); });
describe('startTokenValidationPeriodically', () => { 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', async () => {
const configs = [ const configs = [
{ silentRenew: false, configId: 'configId1' }, { silentRenew: false, configId: 'configId1' },
{ silentRenew: false, configId: 'configId2' }, { silentRenew: false, configId: 'configId2' },
]; ];
const result = await lastValueFrom( const result = await firstValueFrom(
periodicallyTokenCheckService.startTokenValidationPeriodically( periodicallyTokenCheckService.startTokenValidationPeriodically(
configs, configs,
configs[0]! configs[0]!
@ -107,7 +119,7 @@ describe('PeriodicallyTokenCheckService', () => {
true true
); );
const result = await lastValueFrom( const result = await firstValueFrom(
periodicallyTokenCheckService.startTokenValidationPeriodically( periodicallyTokenCheckService.startTokenValidationPeriodically(
configs, configs,
configs[0]! configs[0]!
@ -181,19 +193,29 @@ describe('PeriodicallyTokenCheckService', () => {
of(configs[0]!) of(configs[0]!)
); );
periodicallyTokenCheckService.startTokenValidationPeriodically( try {
configs, const test$ = periodicallyTokenCheckService
configs[0]! .startTokenValidationPeriodically(configs, configs[0]!)
); .pipe(
share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: true,
})
);
await vi.advanceTimersByTimeAsync(1000); test$.subscribe();
expect( await vi.advanceTimersByTimeAsync(1000);
periodicallyTokenCheckService.startTokenValidationPeriodically
).toThrowError(); await firstValueFrom(test$);
expect(resetSilentRenewRunning).toHaveBeenCalledExactlyOnceWith( expect.fail('should throw errror');
configs[0] } catch {
); expect(resetSilentRenewRunning).toHaveBeenCalledExactlyOnceWith(
configs[0]
);
}
}); });
it('interval throws silent renew failed event with data in case of an error', async () => { it('interval throws silent renew failed event with data in case of an error', async () => {
@ -220,20 +242,29 @@ describe('PeriodicallyTokenCheckService', () => {
of(configs[0]!) of(configs[0]!)
); );
periodicallyTokenCheckService.startTokenValidationPeriodically( try {
configs, const test$ = periodicallyTokenCheckService
configs[0]! .startTokenValidationPeriodically(configs, configs[0]!)
); .pipe(
share({
connector: () => new ReplaySubject(1),
resetOnComplete: false,
resetOnError: false,
resetOnRefCountZero: false,
})
);
await vi.advanceTimersByTimeAsync(1000); test$.subscribe();
expect( await vi.advanceTimersByTimeAsync(1000);
periodicallyTokenCheckService.startTokenValidationPeriodically
).toThrowError(); await firstValueFrom(test$);
expect(publicEventsServiceSpy).toBeCalledWith([ } catch {
[EventTypes.SilentRenewStarted], expect(publicEventsServiceSpy.mock.calls).toEqual([
[EventTypes.SilentRenewFailed, new Error('error')], [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', async () => {

View File

@ -1,6 +1,6 @@
import { Injectable, inject } from 'injection-js'; import { Injectable, inject } from 'injection-js';
import { type Observable, forkJoin, of, throwError } from 'rxjs'; import { type Observable, ReplaySubject, forkJoin, of, throwError } from 'rxjs';
import { catchError, map, shareReplay, switchMap } from 'rxjs/operators'; import { catchError, map, share, switchMap } from 'rxjs/operators';
import { AuthStateService } from '../auth-state/auth-state.service'; import { AuthStateService } from '../auth-state/auth-state.service';
import { ConfigurationService } from '../config/config.service'; import { ConfigurationService } from '../config/config.service';
import type { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
@ -52,16 +52,16 @@ export class PeriodicallyTokenCheckService {
startTokenValidationPeriodically( startTokenValidationPeriodically(
allConfigs: OpenIdConfiguration[], allConfigs: OpenIdConfiguration[],
currentConfig: OpenIdConfiguration currentConfig: OpenIdConfiguration
): Observable<void> { ): Observable<undefined> {
const configsWithSilentRenewEnabled = const configsWithSilentRenewEnabled =
this.getConfigsWithSilentRenewEnabled(allConfigs); this.getConfigsWithSilentRenewEnabled(allConfigs);
if (configsWithSilentRenewEnabled.length <= 0) { if (configsWithSilentRenewEnabled.length <= 0) {
return; return of(undefined);
} }
if (this.intervalService.isTokenValidationRunning()) { if (this.intervalService.isTokenValidationRunning()) {
return; return of(undefined);
} }
const refreshTimeInSeconds = this.getSmallestRefreshTimeFromConfigs( const refreshTimeInSeconds = this.getSmallestRefreshTimeFromConfigs(
@ -87,7 +87,14 @@ export class PeriodicallyTokenCheckService {
); );
const o$ = periodicallyCheck$.pipe( const o$ = periodicallyCheck$.pipe(
catchError((error) => throwError(() => new Error(error))), catchError((error) => {
this.loggerService.logError(
currentConfig,
'silent renew failed!',
error
);
return throwError(() => error);
}),
map((objectWithConfigIds) => { map((objectWithConfigIds) => {
for (const [configId, _] of Object.entries(objectWithConfigIds)) { for (const [configId, _] of Object.entries(objectWithConfigIds)) {
this.configurationService this.configurationService
@ -104,20 +111,18 @@ export class PeriodicallyTokenCheckService {
this.flowsDataService.resetSilentRenewRunning(config); this.flowsDataService.resetSilentRenewRunning(config);
} }
}); });
return undefined;
} }
}), }),
catchError((error) => { share({
this.loggerService.logError( connector: () => new ReplaySubject(1),
currentConfig, resetOnError: false,
'silent renew failed!', resetOnComplete: false,
error resetOnRefCountZero: false,
); })
return throwError(() => error);
}),
shareReplay(1)
); );
this.intervalService.runTokenValidationRunning = o$.subscribe(); this.intervalService.runTokenValidationRunning = o$.subscribe({});
return o$; return o$;
} }

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { lastValueFrom, of, throwError } from 'rxjs'; import { firstValueFrom, of, throwError } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import type { CallbackContext } from '../flows/callback-context'; import type { CallbackContext } from '../flows/callback-context';
import { FlowsService } from '../flows/flows.service'; import { FlowsService } from '../flows/flows.service';
@ -16,6 +16,7 @@ describe('RefreshSessionRefreshTokenService', () => {
let flowsService: FlowsService; let flowsService: FlowsService;
beforeEach(() => { beforeEach(() => {
vi.useFakeTimers();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [],
providers: [ providers: [
@ -34,6 +35,11 @@ describe('RefreshSessionRefreshTokenService', () => {
resetAuthDataService = TestBed.inject(ResetAuthDataService); resetAuthDataService = TestBed.inject(ResetAuthDataService);
}); });
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => {
vi.useRealTimers();
});
it('should create', () => { it('should create', () => {
expect(refreshSessionRefreshTokenService).toBeTruthy(); expect(refreshSessionRefreshTokenService).toBeTruthy();
}); });
@ -44,7 +50,7 @@ describe('RefreshSessionRefreshTokenService', () => {
.spyOn(flowsService, 'processRefreshToken') .spyOn(flowsService, 'processRefreshToken')
.mockReturnValue(of({} as CallbackContext)); .mockReturnValue(of({} as CallbackContext));
await lastValueFrom( await firstValueFrom(
refreshSessionRefreshTokenService.refreshSessionWithRefreshTokens( refreshSessionRefreshTokenService.refreshSessionWithRefreshTokens(
{ configId: 'configId1' }, { configId: 'configId1' },
[{ configId: 'configId1' }] [{ configId: 'configId1' }]
@ -63,7 +69,7 @@ describe('RefreshSessionRefreshTokenService', () => {
); );
try { try {
await lastValueFrom( await firstValueFrom(
refreshSessionRefreshTokenService.refreshSessionWithRefreshTokens( refreshSessionRefreshTokenService.refreshSessionWithRefreshTokens(
{ configId: 'configId1' }, { configId: 'configId1' },
[{ configId: 'configId1' }] [{ configId: 'configId1' }]
@ -85,7 +91,7 @@ describe('RefreshSessionRefreshTokenService', () => {
); );
try { try {
await lastValueFrom( await firstValueFrom(
refreshSessionRefreshTokenService.refreshSessionWithRefreshTokens( refreshSessionRefreshTokenService.refreshSessionWithRefreshTokens(
{ configId: 'configId1' }, { configId: 'configId1' },
[{ configId: 'configId1' }] [{ configId: 'configId1' }]

View File

@ -1,6 +1,12 @@
import { TestBed, spyOnProperty } from '@/testing'; import { TestBed, spyOnProperty } from '@/testing';
import { EmptyError, lastValueFrom, of, throwError } from 'rxjs'; import {
import { delay } from 'rxjs/operators'; EmptyError,
ReplaySubject,
firstValueFrom,
of,
throwError,
} from 'rxjs';
import { delay, share } from 'rxjs/operators';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { AuthStateService } from '../auth-state/auth-state.service'; import { AuthStateService } from '../auth-state/auth-state.service';
import { AuthWellKnownService } from '../config/auth-well-known/auth-well-known.service'; import { AuthWellKnownService } from '../config/auth-well-known/auth-well-known.service';
@ -22,6 +28,7 @@ import {
} from './refresh-session.service'; } from './refresh-session.service';
describe('RefreshSessionService ', () => { describe('RefreshSessionService ', () => {
vi.useFakeTimers();
let refreshSessionService: RefreshSessionService; let refreshSessionService: RefreshSessionService;
let flowHelper: FlowHelper; let flowHelper: FlowHelper;
let authStateService: AuthStateService; let authStateService: AuthStateService;
@ -63,6 +70,11 @@ describe('RefreshSessionService ', () => {
storagePersistenceService = TestBed.inject(StoragePersistenceService); storagePersistenceService = TestBed.inject(StoragePersistenceService);
}); });
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => {
vi.useRealTimers();
});
it('should create', () => { it('should create', () => {
expect(refreshSessionService).toBeTruthy(); expect(refreshSessionService).toBeTruthy();
}); });
@ -91,7 +103,7 @@ describe('RefreshSessionService ', () => {
const extraCustomParams = { extra: 'custom' }; const extraCustomParams = { extra: 'custom' };
await lastValueFrom( await firstValueFrom(
refreshSessionService.userForceRefreshSession( refreshSessionService.userForceRefreshSession(
allConfigs[0]!, allConfigs[0]!,
allConfigs, allConfigs,
@ -128,7 +140,7 @@ describe('RefreshSessionService ', () => {
const extraCustomParams = { extra: 'custom' }; const extraCustomParams = { extra: 'custom' };
await lastValueFrom( await firstValueFrom(
refreshSessionService.userForceRefreshSession( refreshSessionService.userForceRefreshSession(
allConfigs[0]!, allConfigs[0]!,
allConfigs, allConfigs,
@ -163,7 +175,7 @@ describe('RefreshSessionService ', () => {
]; ];
const writeSpy = vi.spyOn(storagePersistenceService, 'write'); const writeSpy = vi.spyOn(storagePersistenceService, 'write');
await lastValueFrom( await firstValueFrom(
refreshSessionService.userForceRefreshSession( refreshSessionService.userForceRefreshSession(
allConfigs[0]!, allConfigs[0]!,
allConfigs allConfigs
@ -186,7 +198,7 @@ describe('RefreshSessionService ', () => {
]; ];
try { try {
const result = await lastValueFrom( const result = await firstValueFrom(
refreshSessionService.userForceRefreshSession( refreshSessionService.userForceRefreshSession(
allConfigs[0]!, allConfigs[0]!,
allConfigs allConfigs
@ -217,7 +229,7 @@ describe('RefreshSessionService ', () => {
]; ];
try { try {
await lastValueFrom( await firstValueFrom(
refreshSessionService.userForceRefreshSession( refreshSessionService.userForceRefreshSession(
allConfigs[0]!, allConfigs[0]!,
allConfigs allConfigs
@ -259,7 +271,7 @@ describe('RefreshSessionService ', () => {
}, },
]; ];
const result = await lastValueFrom( const result = await firstValueFrom(
refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs) refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
); );
expect(result.idToken).toEqual('id-token'); expect(result.idToken).toEqual('id-token');
@ -285,7 +297,7 @@ describe('RefreshSessionService ', () => {
}, },
]; ];
const result = await lastValueFrom( const result = await firstValueFrom(
refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs) refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
); );
expect(result).toEqual({ expect(result).toEqual({
@ -328,7 +340,7 @@ describe('RefreshSessionService ', () => {
}, },
]; ];
const result = await lastValueFrom( const result = await firstValueFrom(
refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs) refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
); );
expect(result.idToken).toBeDefined(); expect(result.idToken).toBeDefined();
@ -358,7 +370,7 @@ describe('RefreshSessionService ', () => {
}, },
]; ];
const result = await lastValueFrom( const result = await firstValueFrom(
refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs) refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
); );
expect(result).toEqual({ expect(result).toEqual({
@ -372,6 +384,8 @@ describe('RefreshSessionService ', () => {
}); });
it('occurs timeout error and retry mechanism exhausted max retry count throws error', async () => { it('occurs timeout error and retry mechanism exhausted max retry count throws error', async () => {
vi.useRealTimers();
vi.useFakeTimers();
vi.spyOn( vi.spyOn(
flowHelper, flowHelper,
'isCurrentFlowCodeFlowWithRefreshTokens' 'isCurrentFlowCodeFlowWithRefreshTokens'
@ -402,10 +416,25 @@ describe('RefreshSessionService ', () => {
const expectedInvokeCount = MAX_RETRY_ATTEMPTS; const expectedInvokeCount = MAX_RETRY_ATTEMPTS;
try { try {
const result = await lastValueFrom( const o$ = refreshSessionService
refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs) .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) { if (result) {
expect.fail('It should not return any result.'); expect.fail('It should not return any result.');
} }
@ -415,10 +444,6 @@ describe('RefreshSessionService ', () => {
expectedInvokeCount expectedInvokeCount
); );
} }
await vi.advanceTimersByTimeAsync(
allConfigs[0]!.silentRenewTimeoutInSeconds * 10000
);
}); });
it('occurs unknown error throws it to subscriber', async () => { it('occurs unknown error throws it to subscriber', async () => {
@ -453,7 +478,7 @@ describe('RefreshSessionService ', () => {
); );
try { try {
await lastValueFrom( await firstValueFrom(
refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs) refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
); );
expect.fail('It should not return any result.'); expect.fail('It should not return any result.');
@ -489,7 +514,7 @@ describe('RefreshSessionService ', () => {
'refreshSessionWithIFrameCompleted$' 'refreshSessionWithIFrameCompleted$'
).mockReturnValue(of(null)); ).mockReturnValue(of(null));
const result = await lastValueFrom( const result = await firstValueFrom(
refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs) refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
); );
expect(result).toEqual({ expect(result).toEqual({
@ -533,7 +558,7 @@ describe('RefreshSessionService ', () => {
.spyOn(authStateService, 'areAuthStorageTokensValid') .spyOn(authStateService, 'areAuthStorageTokensValid')
.mockReturnValue(true); .mockReturnValue(true);
const result = await lastValueFrom( const result = await firstValueFrom(
refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs) refreshSessionService.forceRefreshSession(allConfigs[0]!, allConfigs)
); );
expect(result).toEqual({ expect(result).toEqual({
@ -552,7 +577,7 @@ describe('RefreshSessionService ', () => {
it('returns null if no auth well known endpoint defined', async () => { it('returns null if no auth well known endpoint defined', async () => {
vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(true); vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(true);
const result = await lastValueFrom( const result = await firstValueFrom(
(refreshSessionService as any).startRefreshSession() (refreshSessionService as any).startRefreshSession()
); );
expect(result).toBe(null); expect(result).toBe(null);
@ -561,7 +586,7 @@ describe('RefreshSessionService ', () => {
it('returns null if silent renew Is running', async () => { it('returns null if silent renew Is running', async () => {
vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(true); vi.spyOn(flowsDataService, 'isSilentRenewRunning').mockReturnValue(true);
const result = await lastValueFrom( const result = await firstValueFrom(
(refreshSessionService as any).startRefreshSession() (refreshSessionService as any).startRefreshSession()
); );
expect(result).toBe(null); expect(result).toBe(null);
@ -594,7 +619,7 @@ describe('RefreshSessionService ', () => {
'refreshSessionWithRefreshTokens' 'refreshSessionWithRefreshTokens'
).mockReturnValue(of({} as CallbackContext)); ).mockReturnValue(of({} as CallbackContext));
await lastValueFrom( await firstValueFrom(
(refreshSessionService as any).startRefreshSession( (refreshSessionService as any).startRefreshSession(
allConfigs[0]!, allConfigs[0]!,
allConfigs allConfigs
@ -629,7 +654,7 @@ describe('RefreshSessionService ', () => {
) )
.mockReturnValue(of({} as CallbackContext)); .mockReturnValue(of({} as CallbackContext));
await lastValueFrom( await firstValueFrom(
(refreshSessionService as any).startRefreshSession( (refreshSessionService as any).startRefreshSession(
allConfigs[0]!, allConfigs[0]!,
allConfigs allConfigs
@ -668,7 +693,7 @@ describe('RefreshSessionService ', () => {
.spyOn(refreshSessionIframeService, 'refreshSessionWithIframe') .spyOn(refreshSessionIframeService, 'refreshSessionWithIframe')
.mockReturnValue(of(false)); .mockReturnValue(of(false));
await lastValueFrom( await firstValueFrom(
(refreshSessionService as any).startRefreshSession( (refreshSessionService as any).startRefreshSession(
allConfigs[0]!, allConfigs[0]!,
allConfigs allConfigs

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { lastValueFrom, of, throwError } from 'rxjs'; import { firstValueFrom, of, throwError } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { DataService } from '../../api/data.service'; import { DataService } from '../../api/data.service';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
@ -56,7 +56,7 @@ describe('AuthWellKnownDataService', () => {
const urlWithoutSuffix = 'myUrl'; const urlWithoutSuffix = 'myUrl';
const urlWithSuffix = `${urlWithoutSuffix}/.well-known/openid-configuration`; const urlWithSuffix = `${urlWithoutSuffix}/.well-known/openid-configuration`;
await lastValueFrom( await firstValueFrom(
(service as any).getWellKnownDocument(urlWithoutSuffix, { (service as any).getWellKnownDocument(urlWithoutSuffix, {
configId: 'configId1', configId: 'configId1',
}) })
@ -72,7 +72,7 @@ describe('AuthWellKnownDataService', () => {
.mockReturnValue(of(null)); .mockReturnValue(of(null));
const urlWithSuffix = 'myUrl/.well-known/openid-configuration'; const urlWithSuffix = 'myUrl/.well-known/openid-configuration';
await lastValueFrom( await firstValueFrom(
(service as any).getWellKnownDocument(urlWithSuffix, { (service as any).getWellKnownDocument(urlWithSuffix, {
configId: 'configId1', configId: 'configId1',
}) })
@ -89,7 +89,7 @@ describe('AuthWellKnownDataService', () => {
const urlWithSuffix = const urlWithSuffix =
'myUrl/.well-known/openid-configuration/and/some/more/stuff'; 'myUrl/.well-known/openid-configuration/and/some/more/stuff';
await lastValueFrom( await firstValueFrom(
(service as any).getWellKnownDocument(urlWithSuffix, { (service as any).getWellKnownDocument(urlWithSuffix, {
configId: 'configId1', configId: 'configId1',
}) })
@ -106,7 +106,7 @@ describe('AuthWellKnownDataService', () => {
const urlWithoutSuffix = 'myUrl'; const urlWithoutSuffix = 'myUrl';
const urlWithSuffix = `${urlWithoutSuffix}/.well-known/test-openid-configuration`; const urlWithSuffix = `${urlWithoutSuffix}/.well-known/test-openid-configuration`;
await lastValueFrom( await firstValueFrom(
(service as any).getWellKnownDocument(urlWithoutSuffix, { (service as any).getWellKnownDocument(urlWithoutSuffix, {
configId: 'configId1', configId: 'configId1',
authWellknownUrlSuffix: '/.well-known/test-openid-configuration', authWellknownUrlSuffix: '/.well-known/test-openid-configuration',
@ -126,7 +126,7 @@ describe('AuthWellKnownDataService', () => {
) )
); );
const res: unknown = await lastValueFrom( const res: unknown = await firstValueFrom(
(service as any).getWellKnownDocument('anyurl', { (service as any).getWellKnownDocument('anyurl', {
configId: 'configId1', configId: 'configId1',
}) })
@ -144,7 +144,7 @@ describe('AuthWellKnownDataService', () => {
) )
); );
const res: any = await lastValueFrom( const res: any = await firstValueFrom(
(service as any).getWellKnownDocument('anyurl', { (service as any).getWellKnownDocument('anyurl', {
configId: 'configId1', configId: 'configId1',
}) })
@ -164,7 +164,7 @@ describe('AuthWellKnownDataService', () => {
); );
try { try {
await lastValueFrom( await firstValueFrom(
(service as any).getWellKnownDocument('anyurl', 'configId') (service as any).getWellKnownDocument('anyurl', 'configId')
); );
} catch (err: unknown) { } catch (err: unknown) {
@ -181,7 +181,7 @@ describe('AuthWellKnownDataService', () => {
const spy = vi.spyOn(service as any, 'getWellKnownDocument'); const spy = vi.spyOn(service as any, 'getWellKnownDocument');
const result = await lastValueFrom( const result = await firstValueFrom(
service.getWellKnownEndPointsForConfig({ service.getWellKnownEndPointsForConfig({
configId: 'configId1', configId: 'configId1',
authWellknownEndpointUrl: 'any-url', authWellknownEndpointUrl: 'any-url',
@ -200,7 +200,7 @@ describe('AuthWellKnownDataService', () => {
}; };
try { try {
await lastValueFrom(service.getWellKnownEndPointsForConfig(config)); await firstValueFrom(service.getWellKnownEndPointsForConfig(config));
} catch (error: any) { } catch (error: any) {
expect(loggerSpy).toHaveBeenCalledExactlyOnceWith( expect(loggerSpy).toHaveBeenCalledExactlyOnceWith(
config, config,
@ -221,7 +221,7 @@ describe('AuthWellKnownDataService', () => {
jwksUri: DUMMY_WELL_KNOWN_DOCUMENT.jwks_uri, jwksUri: DUMMY_WELL_KNOWN_DOCUMENT.jwks_uri,
}; };
const result = await lastValueFrom( const result = await firstValueFrom(
service.getWellKnownEndPointsForConfig({ service.getWellKnownEndPointsForConfig({
configId: 'configId1', configId: 'configId1',
authWellknownEndpointUrl: 'any-url', authWellknownEndpointUrl: 'any-url',

View File

@ -1,5 +1,5 @@
import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { TestBed, mockImplementationWhenArgsEqual } from '@/testing';
import { lastValueFrom, of, throwError } from 'rxjs'; import { firstValueFrom, of, throwError } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { EventTypes } from '../../public-events/event-types'; import { EventTypes } from '../../public-events/event-types';
import { PublicEventsService } from '../../public-events/public-events.service'; import { PublicEventsService } from '../../public-events/public-events.service';
@ -36,7 +36,7 @@ describe('AuthWellKnownService', () => {
describe('getAuthWellKnownEndPoints', () => { describe('getAuthWellKnownEndPoints', () => {
it('getAuthWellKnownEndPoints throws an error if not config provided', async () => { it('getAuthWellKnownEndPoints throws an error if not config provided', async () => {
try { try {
await lastValueFrom(service.queryAndStoreAuthWellKnownEndPoints(null)); await firstValueFrom(service.queryAndStoreAuthWellKnownEndPoints(null));
} catch (error) { } catch (error) {
expect(error).toEqual( expect(error).toEqual(
new Error( new Error(
@ -57,7 +57,7 @@ describe('AuthWellKnownService', () => {
() => ({ issuer: 'anything' }) () => ({ issuer: 'anything' })
); );
const result = await lastValueFrom( const result = await firstValueFrom(
service.queryAndStoreAuthWellKnownEndPoints({ configId: 'configId1' }) service.queryAndStoreAuthWellKnownEndPoints({ configId: 'configId1' })
); );
expect(storagePersistenceService.read).not.toHaveBeenCalled(); expect(storagePersistenceService.read).not.toHaveBeenCalled();
@ -77,7 +77,7 @@ describe('AuthWellKnownService', () => {
); );
const storeSpy = vi.spyOn(service, 'storeWellKnownEndpoints'); const storeSpy = vi.spyOn(service, 'storeWellKnownEndpoints');
const result = await lastValueFrom( const result = await firstValueFrom(
service.queryAndStoreAuthWellKnownEndPoints({ configId: 'configId1' }) service.queryAndStoreAuthWellKnownEndPoints({ configId: 'configId1' })
); );
expect(dataServiceSpy).toHaveBeenCalled(); expect(dataServiceSpy).toHaveBeenCalled();
@ -92,7 +92,7 @@ describe('AuthWellKnownService', () => {
const publicEventsServiceSpy = vi.spyOn(publicEventsService, 'fireEvent'); const publicEventsServiceSpy = vi.spyOn(publicEventsService, 'fireEvent');
try { try {
await lastValueFrom( await firstValueFrom(
service.queryAndStoreAuthWellKnownEndPoints({ configId: 'configId1' }) service.queryAndStoreAuthWellKnownEndPoints({ configId: 'configId1' })
); );
} catch (err: any) { } catch (err: any) {

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { lastValueFrom, of } from 'rxjs'; import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
import { EventTypes } from '../public-events/event-types'; import { EventTypes } from '../public-events/event-types';
@ -93,7 +93,7 @@ describe('Configuration Service', () => {
}; };
const spy = vi.spyOn(configService as any, 'loadConfigs'); const spy = vi.spyOn(configService as any, 'loadConfigs');
const config = await lastValueFrom( const config = await firstValueFrom(
configService.getOpenIDConfiguration('configId1') configService.getOpenIDConfiguration('configId1')
); );
expect(config).toBeTruthy(); expect(config).toBeTruthy();
@ -108,7 +108,7 @@ describe('Configuration Service', () => {
vi.spyOn(configValidationService, 'validateConfig').mockReturnValue(true); vi.spyOn(configValidationService, 'validateConfig').mockReturnValue(true);
const config = await lastValueFrom( const config = await firstValueFrom(
configService.getOpenIDConfiguration('configId1') configService.getOpenIDConfiguration('configId1')
); );
expect(config).toBeTruthy(); expect(config).toBeTruthy();
@ -126,7 +126,7 @@ describe('Configuration Service', () => {
); );
const consoleSpy = vi.spyOn(console, 'warn'); const consoleSpy = vi.spyOn(console, 'warn');
const config = await lastValueFrom( const config = await firstValueFrom(
configService.getOpenIDConfiguration('configId1') configService.getOpenIDConfiguration('configId1')
); );
expect(config).toBeNull(); expect(config).toBeNull();
@ -141,7 +141,7 @@ describe('Configuration Service', () => {
configId2: { configId: 'configId2' }, configId2: { configId: 'configId2' },
}; };
const config = await lastValueFrom( const config = await firstValueFrom(
configService.getOpenIDConfiguration('notExisting') configService.getOpenIDConfiguration('notExisting')
); );
expect(config).toBeNull(); expect(config).toBeNull();
@ -160,7 +160,7 @@ describe('Configuration Service', () => {
issuer: 'auth-well-known', issuer: 'auth-well-known',
}); });
const config = await lastValueFrom( const config = await firstValueFrom(
configService.getOpenIDConfiguration('configId1') configService.getOpenIDConfiguration('configId1')
); );
expect(config?.authWellknownEndpoints).toEqual({ expect(config?.authWellknownEndpoints).toEqual({
@ -182,7 +182,7 @@ describe('Configuration Service', () => {
const spy = vi.spyOn(publicEventsService, 'fireEvent'); const spy = vi.spyOn(publicEventsService, 'fireEvent');
await lastValueFrom(configService.getOpenIDConfiguration('configId1')); await firstValueFrom(configService.getOpenIDConfiguration('configId1'));
expect(spy).toHaveBeenCalledExactlyOnceWith( expect(spy).toHaveBeenCalledExactlyOnceWith(
EventTypes.ConfigLoaded, EventTypes.ConfigLoaded,
expect.anything() expect.anything()
@ -209,7 +209,7 @@ describe('Configuration Service', () => {
'storeWellKnownEndpoints' 'storeWellKnownEndpoints'
); );
const config = await lastValueFrom( const config = await firstValueFrom(
configService.getOpenIDConfiguration('configId1') configService.getOpenIDConfiguration('configId1')
); );
expect(config).toBeTruthy(); expect(config).toBeTruthy();
@ -237,7 +237,7 @@ describe('Configuration Service', () => {
vi.spyOn(configValidationService, 'validateConfig').mockReturnValue(true); vi.spyOn(configValidationService, 'validateConfig').mockReturnValue(true);
const result = await lastValueFrom( const result = await firstValueFrom(
configService.getOpenIDConfigurations('configId1') configService.getOpenIDConfigurations('configId1')
); );
expect(result.allConfigs.length).toEqual(2); expect(result.allConfigs.length).toEqual(2);
@ -254,7 +254,7 @@ describe('Configuration Service', () => {
vi.spyOn(configValidationService, 'validateConfig').mockReturnValue(true); vi.spyOn(configValidationService, 'validateConfig').mockReturnValue(true);
const result = await lastValueFrom( const result = await firstValueFrom(
configService.getOpenIDConfigurations() configService.getOpenIDConfigurations()
); );
expect(result.allConfigs.length).toEqual(2); expect(result.allConfigs.length).toEqual(2);
@ -276,7 +276,7 @@ describe('Configuration Service', () => {
false false
); );
const { allConfigs, currentConfig } = await lastValueFrom( const { allConfigs, currentConfig } = await firstValueFrom(
configService.getOpenIDConfigurations() configService.getOpenIDConfigurations()
); );
expect(allConfigs).toEqual([]); expect(allConfigs).toEqual([]);

View File

@ -1,4 +1,4 @@
import { lastValueFrom, of } from 'rxjs'; import { firstValueFrom, of } from 'rxjs';
import type { OpenIdConfiguration } from '../openid-configuration'; import type { OpenIdConfiguration } from '../openid-configuration';
import { StsConfigHttpLoader, StsConfigStaticLoader } from './config-loader'; import { StsConfigHttpLoader, StsConfigStaticLoader } from './config-loader';
@ -15,7 +15,7 @@ describe('ConfigLoader', () => {
const result$ = loader.loadConfigs(); const result$ = loader.loadConfigs();
const result = await lastValueFrom(result$); const result = await firstValueFrom(result$);
expect(Array.isArray(result)).toBeTruthy(); expect(Array.isArray(result)).toBeTruthy();
}); });
@ -26,7 +26,7 @@ describe('ConfigLoader', () => {
const result$ = loader.loadConfigs(); const result$ = loader.loadConfigs();
const result = await lastValueFrom(result$); const result = await firstValueFrom(result$);
expect(Array.isArray(result)).toBeTruthy(); expect(Array.isArray(result)).toBeTruthy();
}); });
}); });
@ -43,7 +43,7 @@ describe('ConfigLoader', () => {
const result$ = loader.loadConfigs(); const result$ = loader.loadConfigs();
const result = await lastValueFrom(result$); const result = await firstValueFrom(result$);
expect(Array.isArray(result)).toBeTruthy(); expect(Array.isArray(result)).toBeTruthy();
expect(result[0]!.configId).toBe('configId1'); expect(result[0]!.configId).toBe('configId1');
expect(result[1]!.configId).toBe('configId2'); expect(result[1]!.configId).toBe('configId2');
@ -58,7 +58,7 @@ describe('ConfigLoader', () => {
const result$ = loader.loadConfigs(); const result$ = loader.loadConfigs();
const result = await lastValueFrom(result$); const result = await firstValueFrom(result$);
expect(Array.isArray(result)).toBeTruthy(); expect(Array.isArray(result)).toBeTruthy();
expect(result[0]!.configId).toBe('configId1'); expect(result[0]!.configId).toBe('configId1');
expect(result[1]!.configId).toBe('configId2'); expect(result[1]!.configId).toBe('configId2');
@ -71,7 +71,7 @@ describe('ConfigLoader', () => {
const result$ = loader.loadConfigs(); const result$ = loader.loadConfigs();
const result = await lastValueFrom(result$); const result = await firstValueFrom(result$);
expect(Array.isArray(result)).toBeTruthy(); expect(Array.isArray(result)).toBeTruthy();
expect(result[0]!.configId).toBe('configId1'); expect(result[0]!.configId).toBe('configId1');
}); });

View File

@ -13,7 +13,6 @@ export abstract class StsConfigLoader {
export class StsConfigStaticLoader implements StsConfigLoader { export class StsConfigStaticLoader implements StsConfigLoader {
constructor( constructor(
// biome-ignore lint/style/noParameterProperties: <explanation>
private readonly passedConfigs: OpenIdConfiguration | OpenIdConfiguration[] private readonly passedConfigs: OpenIdConfiguration | OpenIdConfiguration[]
) {} ) {}

View File

@ -1,4 +1,5 @@
import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { TestBed, mockImplementationWhenArgsEqual } from '@/testing';
import { mockImplementationWhenArgs, spyOnWithOrigin } from '@/testing/spy';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { LogLevel } from '../../logging/log-level'; import { LogLevel } from '../../logging/log-level';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
@ -51,12 +52,12 @@ describe('Config Validation Service', () => {
it('calls `logWarning` if one rule has warning level', () => { it('calls `logWarning` if one rule has warning level', () => {
const loggerWarningSpy = vi.spyOn(loggerService, 'logWarning'); const loggerWarningSpy = vi.spyOn(loggerService, 'logWarning');
const messageTypeSpy = vi.spyOn( const messageTypeSpy = spyOnWithOrigin(
configValidationService as any, configValidationService,
'getAllMessagesOfType' 'getAllMessagesOfType' as any
); );
mockImplementationWhenArgsEqual( mockImplementationWhenArgs(
messageTypeSpy, messageTypeSpy,
(arg1: any, arg2: any) => arg1 === 'warning' && Array.isArray(arg2), (arg1: any, arg2: any) => arg1 === 'warning' && Array.isArray(arg2),
() => ['A warning message'] () => ['A warning message']

View File

@ -85,7 +85,7 @@ export class ConfigValidationService {
return allErrorMessages.length; return allErrorMessages.length;
} }
private getAllMessagesOfType( protected getAllMessagesOfType(
type: Level, type: Level,
results: RuleValidationResult[] results: RuleValidationResult[]
): string[] { ): string[] {

View File

@ -102,21 +102,30 @@ describe('JwkExtractor', () => {
describe('extractJwk', () => { describe('extractJwk', () => {
it('throws error if no keys are present in array', () => { it('throws error if no keys are present in array', () => {
expect(() => { try {
service.extractJwk([]); service.extractJwk([]);
}).toThrow(JwkExtractorInvalidArgumentError); expect.fail('should error');
} catch (error: any) {
expect(error).toBe(JwkExtractorInvalidArgumentError);
}
}); });
it('throws error if spec.kid is present, but no key was matching', () => { it('throws error if spec.kid is present, but no key was matching', () => {
expect(() => { try {
service.extractJwk(keys, { kid: 'doot' }); service.extractJwk(keys, { kid: 'doot' });
}).toThrow(JwkExtractorNoMatchingKeysError); expect.fail('should error');
} catch (error: any) {
expect(error).toBe(JwkExtractorNoMatchingKeysError);
}
}); });
it('throws error if spec.use is present, but no key was matching', () => { it('throws error if spec.use is present, but no key was matching', () => {
expect(() => { try {
service.extractJwk(keys, { use: 'blorp' }); service.extractJwk(keys, { use: 'blorp' });
}).toThrow(JwkExtractorNoMatchingKeysError); expect.fail('should error');
} catch (error: any) {
expect(error).toBe(JwkExtractorNoMatchingKeysError);
}
}); });
it('does not throw error if no key is matching when throwOnEmpty is false', () => { it('does not throw error if no key is matching when throwOnEmpty is false', () => {
@ -126,9 +135,12 @@ describe('JwkExtractor', () => {
}); });
it('throws error if multiple keys are present, and spec is not present', () => { it('throws error if multiple keys are present, and spec is not present', () => {
expect(() => { try {
service.extractJwk(keys); service.extractJwk(keys);
}).toThrow(JwkExtractorSeveralMatchingKeysError); expect.fail('should error');
} catch (error: any) {
expect(error).toBe(JwkExtractorSeveralMatchingKeysError);
}
}); });
it('returns array of keys matching spec.kid', () => { it('returns array of keys matching spec.kid', () => {

View File

@ -1,6 +1,6 @@
import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { TestBed, mockImplementationWhenArgsEqual } from '@/testing';
import { HttpErrorResponse, HttpHeaders } from '@ngify/http'; import { HttpErrorResponse, HttpHeaders } from '@ngify/http';
import { lastValueFrom, of, throwError } from 'rxjs'; import { firstValueFrom, of, throwError } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { DataService } from '../../api/data.service'; import { DataService } from '../../api/data.service';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
@ -56,7 +56,7 @@ describe('CodeFlowCallbackHandlerService', () => {
); );
try { try {
await lastValueFrom( await firstValueFrom(
service.codeFlowCallback('test-url', { configId: 'configId1' }) service.codeFlowCallback('test-url', { configId: 'configId1' })
); );
} catch (err: any) { } catch (err: any) {
@ -76,7 +76,7 @@ describe('CodeFlowCallbackHandlerService', () => {
); );
try { try {
await lastValueFrom( await firstValueFrom(
service.codeFlowCallback('test-url', { configId: 'configId1' }) service.codeFlowCallback('test-url', { configId: 'configId1' })
); );
} catch (err: any) { } catch (err: any) {
@ -99,7 +99,7 @@ describe('CodeFlowCallbackHandlerService', () => {
existingIdToken: null, existingIdToken: null,
} as CallbackContext; } as CallbackContext;
const callbackContext = await lastValueFrom( const callbackContext = await firstValueFrom(
service.codeFlowCallback('test-url', { configId: 'configId1' }) service.codeFlowCallback('test-url', { configId: 'configId1' })
); );
expect(callbackContext).toEqual(expectedCallbackContext); expect(callbackContext).toEqual(expectedCallbackContext);
@ -122,7 +122,7 @@ describe('CodeFlowCallbackHandlerService', () => {
).mockReturnValue(false); ).mockReturnValue(false);
try { try {
await lastValueFrom( await firstValueFrom(
service.codeFlowCodeRequest({} as CallbackContext, { service.codeFlowCodeRequest({} as CallbackContext, {
configId: 'configId1', configId: 'configId1',
}) })
@ -144,7 +144,7 @@ describe('CodeFlowCallbackHandlerService', () => {
); );
try { try {
await lastValueFrom( await firstValueFrom(
service.codeFlowCodeRequest({} as CallbackContext, { service.codeFlowCodeRequest({} as CallbackContext, {
configId: 'configId1', configId: 'configId1',
}) })
@ -166,7 +166,7 @@ describe('CodeFlowCallbackHandlerService', () => {
); );
try { try {
await lastValueFrom( await firstValueFrom(
service.codeFlowCodeRequest({} as CallbackContext, { service.codeFlowCodeRequest({} as CallbackContext, {
configId: 'configId1', configId: 'configId1',
}) })
@ -190,7 +190,7 @@ describe('CodeFlowCallbackHandlerService', () => {
'validateStateFromHashCallback' 'validateStateFromHashCallback'
).mockReturnValue(true); ).mockReturnValue(true);
await lastValueFrom( await firstValueFrom(
service.codeFlowCodeRequest({} as CallbackContext, { service.codeFlowCodeRequest({} as CallbackContext, {
configId: 'configId1', configId: 'configId1',
}) })
@ -226,7 +226,7 @@ describe('CodeFlowCallbackHandlerService', () => {
const postSpy = vi.spyOn(dataService, 'post').mockReturnValue(of({})); const postSpy = vi.spyOn(dataService, 'post').mockReturnValue(of({}));
await lastValueFrom( await firstValueFrom(
service.codeFlowCodeRequest({ code: 'foo' } as CallbackContext, config) service.codeFlowCodeRequest({ code: 'foo' } as CallbackContext, config)
); );
expect(urlServiceSpy).toHaveBeenCalledExactlyOnceWith('foo', config, { expect(urlServiceSpy).toHaveBeenCalledExactlyOnceWith('foo', config, {
@ -253,7 +253,7 @@ describe('CodeFlowCallbackHandlerService', () => {
'validateStateFromHashCallback' 'validateStateFromHashCallback'
).mockReturnValue(true); ).mockReturnValue(true);
await lastValueFrom( await firstValueFrom(
service.codeFlowCodeRequest({} as CallbackContext, config) service.codeFlowCodeRequest({} as CallbackContext, config)
); );
const httpHeaders = postSpy.mock.calls.at(-1)?.[3] as HttpHeaders; const httpHeaders = postSpy.mock.calls.at(-1)?.[3] as HttpHeaders;
@ -280,7 +280,7 @@ describe('CodeFlowCallbackHandlerService', () => {
); );
try { try {
await lastValueFrom( await firstValueFrom(
service.codeFlowCodeRequest({} as CallbackContext, config) service.codeFlowCodeRequest({} as CallbackContext, config)
); );
} catch (err: any) { } catch (err: any) {
@ -313,7 +313,7 @@ describe('CodeFlowCallbackHandlerService', () => {
).mockReturnValue(true); ).mockReturnValue(true);
try { try {
const res = await lastValueFrom( const res = await firstValueFrom(
service.codeFlowCodeRequest({} as CallbackContext, config) service.codeFlowCodeRequest({} as CallbackContext, config)
); );
expect(res).toBeTruthy(); expect(res).toBeTruthy();
@ -348,7 +348,7 @@ describe('CodeFlowCallbackHandlerService', () => {
).mockReturnValue(true); ).mockReturnValue(true);
try { try {
const res = await lastValueFrom( const res = await firstValueFrom(
service.codeFlowCodeRequest({} as CallbackContext, config) service.codeFlowCodeRequest({} as CallbackContext, config)
); );
expect(res).toBeFalsy(); expect(res).toBeFalsy();

View File

@ -1,5 +1,5 @@
import { HttpHeaders } from '@ngify/http'; import { HttpHeaders } from '@ngify/http';
import { inject, Injectable } from 'injection-js'; import { Injectable, inject } from 'injection-js';
import { type Observable, of, throwError, timer } from 'rxjs'; import { type Observable, of, throwError, timer } from 'rxjs';
import { catchError, mergeMap, retryWhen, switchMap } from 'rxjs/operators'; import { catchError, mergeMap, retryWhen, switchMap } from 'rxjs/operators';
import { DataService } from '../../api/data.service'; import { DataService } from '../../api/data.service';
@ -116,7 +116,7 @@ export class CodeFlowCallbackHandlerService {
switchMap((response) => { switchMap((response) => {
if (response) { if (response) {
const authResult: AuthResult = { const authResult: AuthResult = {
...response, ...(response as any),
state: callbackContext.state, state: callbackContext.state,
session_state: callbackContext.sessionState, session_state: callbackContext.sessionState,
}; };

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { lastValueFrom, of, throwError } from 'rxjs'; import { firstValueFrom, of, throwError } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { AuthStateService } from '../../auth-state/auth-state.service'; import { AuthStateService } from '../../auth-state/auth-state.service';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
@ -83,14 +83,14 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue(
of({ keys: [] } as JwtKeys) of({ keys: [] } as JwtKeys)
); );
await lastValueFrom( await firstValueFrom(
service.callbackHistoryAndResetJwtKeys( service.callbackHistoryAndResetJwtKeys(
callbackContext, callbackContext,
allConfigs[0]!, allConfigs[0]!,
allConfigs allConfigs
) )
); );
expect(storagePersistenceServiceSpy).toBeCalledWith([ expect(storagePersistenceServiceSpy.mock.calls).toEqual([
['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]], ['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]],
['jwtKeys', { keys: [] }, allConfigs[0]], ['jwtKeys', { keys: [] }, allConfigs[0]],
]); ]);
@ -121,14 +121,14 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
of({ keys: [] } as JwtKeys) of({ keys: [] } as JwtKeys)
); );
await lastValueFrom( await firstValueFrom(
service.callbackHistoryAndResetJwtKeys( service.callbackHistoryAndResetJwtKeys(
callbackContext, callbackContext,
allConfigs[0]!, allConfigs[0]!,
allConfigs allConfigs
) )
); );
expect(storagePersistenceServiceSpy).toBeCalledWith([ expect(storagePersistenceServiceSpy.mock.calls).toEqual([
['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]], ['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]],
['jwtKeys', { keys: [] }, allConfigs[0]], ['jwtKeys', { keys: [] }, allConfigs[0]],
]); ]);
@ -159,14 +159,14 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue(
of({ keys: [] } as JwtKeys) of({ keys: [] } as JwtKeys)
); );
await lastValueFrom( await firstValueFrom(
service.callbackHistoryAndResetJwtKeys( service.callbackHistoryAndResetJwtKeys(
callbackContext, callbackContext,
allConfigs[0]!, allConfigs[0]!,
allConfigs allConfigs
) )
); );
expect(storagePersistenceServiceSpy).toBeCalledWith([ expect(storagePersistenceServiceSpy.mock.calls).toEqual([
['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]], ['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]],
['reusable_refresh_token', 'dummy_refresh_token', allConfigs[0]], ['reusable_refresh_token', 'dummy_refresh_token', allConfigs[0]],
['jwtKeys', { keys: [] }, allConfigs[0]], ['jwtKeys', { keys: [] }, allConfigs[0]],
@ -194,7 +194,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue(
of({ keys: [] } as JwtKeys) of({ keys: [] } as JwtKeys)
); );
await lastValueFrom( await firstValueFrom(
service.callbackHistoryAndResetJwtKeys( service.callbackHistoryAndResetJwtKeys(
callbackContext, callbackContext,
allConfigs[0]!, allConfigs[0]!,
@ -223,7 +223,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue(
of({ keys: [{ kty: 'henlo' } as JwtKey] } as JwtKeys) of({ keys: [{ kty: 'henlo' } as JwtKey] } as JwtKeys)
); );
const result = await lastValueFrom( const result = await firstValueFrom(
service.callbackHistoryAndResetJwtKeys( service.callbackHistoryAndResetJwtKeys(
callbackContext, callbackContext,
allConfigs[0]!, allConfigs[0]!,
@ -257,7 +257,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
of({} as JwtKeys) of({} as JwtKeys)
); );
try { try {
await lastValueFrom( await firstValueFrom(
service.callbackHistoryAndResetJwtKeys( service.callbackHistoryAndResetJwtKeys(
callbackContext, callbackContext,
allConfigs[0]!, allConfigs[0]!,
@ -290,7 +290,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
throwError(() => new Error('error')) throwError(() => new Error('error'))
); );
try { try {
await lastValueFrom( await firstValueFrom(
service.callbackHistoryAndResetJwtKeys( service.callbackHistoryAndResetJwtKeys(
callbackContext, callbackContext,
allConfigs[0]!, allConfigs[0]!,
@ -316,7 +316,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
]; ];
try { try {
await lastValueFrom( await firstValueFrom(
service.callbackHistoryAndResetJwtKeys( service.callbackHistoryAndResetJwtKeys(
callbackContext, callbackContext,
allConfigs[0]!, allConfigs[0]!,
@ -353,7 +353,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
); );
try { try {
await lastValueFrom( await firstValueFrom(
service.callbackHistoryAndResetJwtKeys( service.callbackHistoryAndResetJwtKeys(
callbackContext, callbackContext,
allConfigs[0]!, allConfigs[0]!,
@ -394,7 +394,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
); );
try { try {
await lastValueFrom( await firstValueFrom(
service.callbackHistoryAndResetJwtKeys( service.callbackHistoryAndResetJwtKeys(
callbackContext, callbackContext,
allConfigs[0]!, allConfigs[0]!,
@ -436,7 +436,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
); );
try { try {
const callbackContext: CallbackContext = await lastValueFrom( const callbackContext: CallbackContext = await firstValueFrom(
service.callbackHistoryAndResetJwtKeys( service.callbackHistoryAndResetJwtKeys(
initialCallbackContext, initialCallbackContext,
allConfigs[0]!, allConfigs[0]!,
@ -444,7 +444,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
) )
); );
expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(2); expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(2);
expect(storagePersistenceServiceSpy).toBeCalledWith([ expect(storagePersistenceServiceSpy.mock.calls).toEqual([
['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]], ['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]],
['jwtKeys', DUMMY_JWT_KEYS, allConfigs[0]], ['jwtKeys', DUMMY_JWT_KEYS, allConfigs[0]],
]); ]);
@ -479,7 +479,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
); );
try { try {
const callbackContext: CallbackContext = await lastValueFrom( const callbackContext: CallbackContext = await firstValueFrom(
service.callbackHistoryAndResetJwtKeys( service.callbackHistoryAndResetJwtKeys(
initialCallbackContext, initialCallbackContext,
allConfigs[0]!, allConfigs[0]!,
@ -523,7 +523,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
); );
try { try {
const callbackContext: CallbackContext = await lastValueFrom( const callbackContext: CallbackContext = await firstValueFrom(
service.callbackHistoryAndResetJwtKeys( service.callbackHistoryAndResetJwtKeys(
initialCallbackContext, initialCallbackContext,
allConfigs[0]!, allConfigs[0]!,
@ -560,7 +560,7 @@ describe('HistoryJwtKeysCallbackHandlerService', () => {
); );
try { try {
const callbackContext: CallbackContext = await lastValueFrom( const callbackContext: CallbackContext = await firstValueFrom(
service.callbackHistoryAndResetJwtKeys( service.callbackHistoryAndResetJwtKeys(
initialCallbackContext, initialCallbackContext,
allConfigs[0]!, allConfigs[0]!,

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { lastValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { DOCUMENT } from '../../dom'; import { DOCUMENT } from '../../dom';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
@ -58,7 +58,7 @@ describe('ImplicitFlowCallbackHandlerService', () => {
}, },
]; ];
await lastValueFrom( await firstValueFrom(
service.implicitFlowCallback(allConfigs[0]!, allConfigs, 'any-hash') service.implicitFlowCallback(allConfigs[0]!, allConfigs, 'any-hash')
); );
expect(resetAuthorizationDataSpy).toHaveBeenCalled(); expect(resetAuthorizationDataSpy).toHaveBeenCalled();
@ -76,7 +76,7 @@ describe('ImplicitFlowCallbackHandlerService', () => {
}, },
]; ];
await lastValueFrom( await firstValueFrom(
service.implicitFlowCallback(allConfigs[0]!, allConfigs, 'any-hash') service.implicitFlowCallback(allConfigs[0]!, allConfigs, 'any-hash')
); );
expect(resetAuthorizationDataSpy).not.toHaveBeenCalled(); expect(resetAuthorizationDataSpy).not.toHaveBeenCalled();
@ -102,7 +102,7 @@ describe('ImplicitFlowCallbackHandlerService', () => {
}, },
]; ];
const callbackContext = await lastValueFrom( const callbackContext = await firstValueFrom(
service.implicitFlowCallback(allConfigs[0]!, allConfigs, 'anyHash') service.implicitFlowCallback(allConfigs[0]!, allConfigs, 'anyHash')
); );
expect(callbackContext).toEqual(expectedCallbackContext); expect(callbackContext).toEqual(expectedCallbackContext);
@ -128,7 +128,7 @@ describe('ImplicitFlowCallbackHandlerService', () => {
}, },
]; ];
const callbackContext = await lastValueFrom( const callbackContext = await firstValueFrom(
service.implicitFlowCallback(allConfigs[0]!, allConfigs) service.implicitFlowCallback(allConfigs[0]!, allConfigs)
); );
expect(callbackContext).toEqual(expectedCallbackContext); expect(callbackContext).toEqual(expectedCallbackContext);

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { lastValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { AuthStateService } from '../../auth-state/auth-state.service'; import { AuthStateService } from '../../auth-state/auth-state.service';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
@ -54,7 +54,7 @@ describe('RefreshSessionCallbackHandlerService', () => {
existingIdToken: 'henlo-legger', existingIdToken: 'henlo-legger',
} as CallbackContext; } as CallbackContext;
const callbackContext = await lastValueFrom( const callbackContext = await firstValueFrom(
service.refreshSessionWithRefreshTokens({ configId: 'configId1' }) service.refreshSessionWithRefreshTokens({ configId: 'configId1' })
); );
expect(callbackContext).toEqual(expectedCallbackContext); expect(callbackContext).toEqual(expectedCallbackContext);
@ -69,7 +69,7 @@ describe('RefreshSessionCallbackHandlerService', () => {
vi.spyOn(authStateService, 'getIdToken').mockReturnValue('henlo-legger'); vi.spyOn(authStateService, 'getIdToken').mockReturnValue('henlo-legger');
try { try {
await lastValueFrom( await firstValueFrom(
service.refreshSessionWithRefreshTokens({ configId: 'configId1' }) service.refreshSessionWithRefreshTokens({ configId: 'configId1' })
); );
} catch (err: any) { } catch (err: any) {

View File

@ -1,6 +1,6 @@
import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { TestBed, mockImplementationWhenArgsEqual } from '@/testing';
import { HttpErrorResponse, HttpHeaders } from '@ngify/http'; import { HttpErrorResponse, HttpHeaders } from '@ngify/http';
import { lastValueFrom, of, throwError } from 'rxjs'; import { firstValueFrom, of, throwError } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { DataService } from '../../api/data.service'; import { DataService } from '../../api/data.service';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
@ -46,7 +46,7 @@ describe('RefreshTokenCallbackHandlerService', () => {
it('throws error if no tokenEndpoint is given', async () => { it('throws error if no tokenEndpoint is given', async () => {
try { try {
await lastValueFrom( await firstValueFrom(
(service as any).refreshTokensRequestTokens({} as CallbackContext) (service as any).refreshTokensRequestTokens({} as CallbackContext)
); );
} catch (err: unknown) { } catch (err: unknown) {
@ -63,7 +63,7 @@ describe('RefreshTokenCallbackHandlerService', () => {
() => ({ tokenEndpoint: 'tokenEndpoint' }) () => ({ tokenEndpoint: 'tokenEndpoint' })
); );
await lastValueFrom( await firstValueFrom(
service.refreshTokensRequestTokens({} as CallbackContext, { service.refreshTokensRequestTokens({} as CallbackContext, {
configId: 'configId1', configId: 'configId1',
}) })
@ -90,7 +90,7 @@ describe('RefreshTokenCallbackHandlerService', () => {
() => ({ tokenEndpoint: 'tokenEndpoint' }) () => ({ tokenEndpoint: 'tokenEndpoint' })
); );
await lastValueFrom( await firstValueFrom(
service.refreshTokensRequestTokens({} as CallbackContext, { service.refreshTokensRequestTokens({} as CallbackContext, {
configId: 'configId1', configId: 'configId1',
}) })
@ -115,7 +115,7 @@ describe('RefreshTokenCallbackHandlerService', () => {
); );
try { try {
await lastValueFrom( await firstValueFrom(
service.refreshTokensRequestTokens({} as CallbackContext, config) service.refreshTokensRequestTokens({} as CallbackContext, config)
); );
} catch (err: any) { } catch (err: any) {
@ -139,7 +139,7 @@ describe('RefreshTokenCallbackHandlerService', () => {
); );
try { try {
const res = await lastValueFrom( const res = await firstValueFrom(
service.refreshTokensRequestTokens({} as CallbackContext, config) service.refreshTokensRequestTokens({} as CallbackContext, config)
); );
expect(res).toBeTruthy(); expect(res).toBeTruthy();
@ -165,7 +165,7 @@ describe('RefreshTokenCallbackHandlerService', () => {
); );
try { try {
const res = await lastValueFrom( const res = await firstValueFrom(
service.refreshTokensRequestTokens({} as CallbackContext, config) service.refreshTokensRequestTokens({} as CallbackContext, config)
); );
expect(res).toBeFalsy(); expect(res).toBeFalsy();

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { lastValueFrom, of } from 'rxjs'; import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { AuthStateService } from '../../auth-state/auth-state.service'; import { AuthStateService } from '../../auth-state/auth-state.service';
import { DOCUMENT } from '../../dom'; import { DOCUMENT } from '../../dom';
@ -66,7 +66,7 @@ describe('StateValidationCallbackHandlerService', () => {
); );
const allConfigs = [{ configId: 'configId1' }]; const allConfigs = [{ configId: 'configId1' }];
const newCallbackContext = await lastValueFrom( const newCallbackContext = await firstValueFrom(
service.callbackStateValidation( service.callbackStateValidation(
{} as CallbackContext, {} as CallbackContext,
allConfigs[0]!, allConfigs[0]!,
@ -95,7 +95,7 @@ describe('StateValidationCallbackHandlerService', () => {
const allConfigs = [{ configId: 'configId1' }]; const allConfigs = [{ configId: 'configId1' }];
try { try {
await lastValueFrom( await firstValueFrom(
service.callbackStateValidation( service.callbackStateValidation(
{} as CallbackContext, {} as CallbackContext,
allConfigs[0]!, allConfigs[0]!,
@ -132,7 +132,7 @@ describe('StateValidationCallbackHandlerService', () => {
const allConfigs = [{ configId: 'configId1' }]; const allConfigs = [{ configId: 'configId1' }];
try { try {
await lastValueFrom( await firstValueFrom(
service.callbackStateValidation( service.callbackStateValidation(
{ isRenewProcess: true } as CallbackContext, { isRenewProcess: true } as CallbackContext,
allConfigs[0]!, allConfigs[0]!,

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { lastValueFrom, of } from 'rxjs'; import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { AuthStateService } from '../../auth-state/auth-state.service'; import { AuthStateService } from '../../auth-state/auth-state.service';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
@ -70,7 +70,7 @@ describe('UserCallbackHandlerService', () => {
const spy = vi.spyOn(flowsDataService, 'setSessionState'); const spy = vi.spyOn(flowsDataService, 'setSessionState');
const resultCallbackContext = await lastValueFrom( const resultCallbackContext = await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
); );
expect(spy).toHaveBeenCalledExactlyOnceWith('mystate', allConfigs[0]); expect(spy).toHaveBeenCalledExactlyOnceWith('mystate', allConfigs[0]);
@ -103,7 +103,7 @@ describe('UserCallbackHandlerService', () => {
]; ];
const spy = vi.spyOn(flowsDataService, 'setSessionState'); const spy = vi.spyOn(flowsDataService, 'setSessionState');
const resultCallbackContext = await lastValueFrom( const resultCallbackContext = await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
); );
expect(spy).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled();
@ -136,7 +136,7 @@ describe('UserCallbackHandlerService', () => {
]; ];
const spy = vi.spyOn(flowsDataService, 'setSessionState'); const spy = vi.spyOn(flowsDataService, 'setSessionState');
const resultCallbackContext = await lastValueFrom( const resultCallbackContext = await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
); );
expect(spy).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled();
@ -165,7 +165,7 @@ describe('UserCallbackHandlerService', () => {
const spy = vi.spyOn(flowsDataService, 'setSessionState'); const spy = vi.spyOn(flowsDataService, 'setSessionState');
const resultCallbackContext = await lastValueFrom( const resultCallbackContext = await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
); );
expect(spy).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled();
@ -203,7 +203,7 @@ describe('UserCallbackHandlerService', () => {
'updateAndPublishAuthState' 'updateAndPublishAuthState'
); );
const resultCallbackContext = await lastValueFrom( const resultCallbackContext = await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
); );
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({ expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({
@ -244,7 +244,7 @@ describe('UserCallbackHandlerService', () => {
.spyOn(userService, 'getAndPersistUserDataInStore') .spyOn(userService, 'getAndPersistUserDataInStore')
.mockReturnValue(of({ user: 'some_data' })); .mockReturnValue(of({ user: 'some_data' }));
const resultCallbackContext = await lastValueFrom( const resultCallbackContext = await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
); );
expect(getAndPersistUserDataInStoreSpy).toHaveBeenCalledExactlyOnceWith( expect(getAndPersistUserDataInStoreSpy).toHaveBeenCalledExactlyOnceWith(
@ -292,7 +292,7 @@ describe('UserCallbackHandlerService', () => {
'updateAndPublishAuthState' 'updateAndPublishAuthState'
); );
const resultCallbackContext = await lastValueFrom( const resultCallbackContext = await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
); );
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({ expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({
@ -335,7 +335,7 @@ describe('UserCallbackHandlerService', () => {
); );
const setSessionStateSpy = vi.spyOn(flowsDataService, 'setSessionState'); const setSessionStateSpy = vi.spyOn(flowsDataService, 'setSessionState');
const resultCallbackContext = await lastValueFrom( const resultCallbackContext = await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
); );
expect(setSessionStateSpy).toHaveBeenCalledExactlyOnceWith( expect(setSessionStateSpy).toHaveBeenCalledExactlyOnceWith(
@ -381,7 +381,7 @@ describe('UserCallbackHandlerService', () => {
); );
try { try {
await lastValueFrom( await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
); );
} catch (err: any) { } catch (err: any) {
@ -432,7 +432,7 @@ describe('UserCallbackHandlerService', () => {
); );
try { try {
await lastValueFrom( await firstValueFrom(
service.callbackUser(callbackContext, allConfigs[0]!, allConfigs) service.callbackUser(callbackContext, allConfigs[0]!, allConfigs)
); );
} catch (err: any) { } catch (err: any) {

View File

@ -247,15 +247,15 @@ describe('Flows Data Service', () => {
}); });
describe('isSilentRenewRunning', () => { describe('isSilentRenewRunning', () => {
it('silent renew process timeout exceeded reset state object and returns false result', () => { it('silent renew process timeout exceeded reset state object and returns false result', async () => {
const config = { const config = {
silentRenewTimeoutInSeconds: 10, silentRenewTimeoutInSeconds: 10,
configId: 'configId1', configId: 'configId1',
}; };
vi.useRealTimers(); vi.useRealTimers();
vi.useFakeTimers();
const baseTime = new Date(); const baseTime = new Date();
vi.useFakeTimers();
vi.setSystemTime(baseTime); vi.setSystemTime(baseTime);
@ -271,7 +271,7 @@ describe('Flows Data Service', () => {
); );
const spyWrite = vi.spyOn(storagePersistenceService, 'write'); const spyWrite = vi.spyOn(storagePersistenceService, 'write');
vi.advanceTimersByTimeAsync( await vi.advanceTimersByTimeAsync(
(config.silentRenewTimeoutInSeconds + 1) * 1000 (config.silentRenewTimeoutInSeconds + 1) * 1000
); );

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { lastValueFrom, of } from 'rxjs'; import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { mockProvider } from '../testing/mock'; import { mockProvider } from '../testing/mock';
import type { CallbackContext } from './callback-context'; import type { CallbackContext } from './callback-context';
@ -87,7 +87,7 @@ describe('Flows Service', () => {
}, },
]; ];
const value = await lastValueFrom( const value = await firstValueFrom(
service.processCodeFlowCallback( service.processCodeFlowCallback(
'some-url1234', 'some-url1234',
allConfigs[0]!, allConfigs[0]!,
@ -129,7 +129,7 @@ describe('Flows Service', () => {
}, },
]; ];
const value = await lastValueFrom( const value = await firstValueFrom(
service.processSilentRenewCodeFlowCallback( service.processSilentRenewCodeFlowCallback(
{} as CallbackContext, {} as CallbackContext,
allConfigs[0]!, allConfigs[0]!,
@ -167,7 +167,7 @@ describe('Flows Service', () => {
}, },
]; ];
const value = await lastValueFrom( const value = await firstValueFrom(
service.processImplicitFlowCallback( service.processImplicitFlowCallback(
allConfigs[0]!, allConfigs[0]!,
allConfigs, allConfigs,
@ -211,7 +211,7 @@ describe('Flows Service', () => {
}, },
]; ];
const value = await lastValueFrom( const value = await firstValueFrom(
service.processRefreshToken(allConfigs[0]!, allConfigs) service.processRefreshToken(allConfigs[0]!, allConfigs)
); );
expect(value).toEqual({} as CallbackContext); expect(value).toEqual({} as CallbackContext);

View File

@ -1,6 +1,6 @@
import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { TestBed, mockImplementationWhenArgsEqual } from '@/testing';
import { HttpResponse } from '@ngify/http'; import { HttpResponse } from '@ngify/http';
import { EmptyError, isObservable, lastValueFrom, of, throwError } from 'rxjs'; import { EmptyError, firstValueFrom, isObservable, of, throwError } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { DataService } from '../api/data.service'; import { DataService } from '../api/data.service';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
@ -60,7 +60,7 @@ describe('Signin Key Data Service', () => {
const result = service.getSigningKeys({ configId: 'configId1' }); const result = service.getSigningKeys({ configId: 'configId1' });
try { try {
await lastValueFrom(result); await firstValueFrom(result);
} catch (err: any) { } catch (err: any) {
expect(err).toBeTruthy(); expect(err).toBeTruthy();
} }
@ -75,7 +75,7 @@ describe('Signin Key Data Service', () => {
const result = service.getSigningKeys({ configId: 'configId1' }); const result = service.getSigningKeys({ configId: 'configId1' });
try { try {
await lastValueFrom(result); await firstValueFrom(result);
} catch (err: any) { } catch (err: any) {
expect(err).toBeTruthy(); expect(err).toBeTruthy();
} }
@ -92,7 +92,7 @@ describe('Signin Key Data Service', () => {
const result = service.getSigningKeys({ configId: 'configId1' }); const result = service.getSigningKeys({ configId: 'configId1' });
try { try {
await lastValueFrom(result); await firstValueFrom(result);
} catch (err: any) { } catch (err: any) {
if (err instanceof EmptyError) { if (err instanceof EmptyError) {
expect(spy).toHaveBeenCalledExactlyOnceWith('someUrl', { expect(spy).toHaveBeenCalledExactlyOnceWith('someUrl', {
@ -115,7 +115,7 @@ describe('Signin Key Data Service', () => {
) )
); );
const res = await lastValueFrom( const res = await firstValueFrom(
service.getSigningKeys({ configId: 'configId1' }) service.getSigningKeys({ configId: 'configId1' })
); );
expect(res).toBeTruthy(); expect(res).toBeTruthy();
@ -136,7 +136,7 @@ describe('Signin Key Data Service', () => {
) )
); );
const res = await lastValueFrom( const res = await firstValueFrom(
service.getSigningKeys({ configId: 'configId1' }) service.getSigningKeys({ configId: 'configId1' })
); );
expect(res).toBeTruthy(); expect(res).toBeTruthy();
@ -159,7 +159,7 @@ describe('Signin Key Data Service', () => {
); );
try { try {
await lastValueFrom(service.getSigningKeys({ configId: 'configId1' })); await firstValueFrom(service.getSigningKeys({ configId: 'configId1' }));
} catch (err: any) { } catch (err: any) {
expect(err).toBeTruthy(); expect(err).toBeTruthy();
} }
@ -180,7 +180,7 @@ describe('Signin Key Data Service', () => {
const logSpy = vi.spyOn(loggerService, 'logError'); const logSpy = vi.spyOn(loggerService, 'logError');
try { try {
await lastValueFrom( await firstValueFrom(
(service as any).handleErrorGetSigningKeys( (service as any).handleErrorGetSigningKeys(
new HttpResponse({ status: 400, statusText: 'nono' }), new HttpResponse({ status: 400, statusText: 'nono' }),
{ configId: 'configId1' } { configId: 'configId1' }
@ -198,7 +198,7 @@ describe('Signin Key Data Service', () => {
const logSpy = vi.spyOn(loggerService, 'logError'); const logSpy = vi.spyOn(loggerService, 'logError');
try { try {
await lastValueFrom( await firstValueFrom(
(service as any).handleErrorGetSigningKeys('Just some Error', { (service as any).handleErrorGetSigningKeys('Just some Error', {
configId: 'configId1', configId: 'configId1',
}) })
@ -215,7 +215,7 @@ describe('Signin Key Data Service', () => {
const logSpy = vi.spyOn(loggerService, 'logError'); const logSpy = vi.spyOn(loggerService, 'logError');
try { try {
await lastValueFrom( await firstValueFrom(
(service as any).handleErrorGetSigningKeys( (service as any).handleErrorGetSigningKeys(
{ message: 'Just some Error' }, { message: 'Just some Error' },
{ configId: 'configId1' } { configId: 'configId1' }

View File

@ -1,5 +1,6 @@
import type { HttpFeature, HttpInterceptor } from '@ngify/http'; import type { HttpFeature, HttpInterceptor } from '@ngify/http';
import { InjectionToken } from 'injection-js'; import { InjectionToken } from 'injection-js';
export { HttpParams, HttpParamsOptions } from './params';
export const HTTP_INTERCEPTORS = new InjectionToken<readonly HttpInterceptor[]>( export const HTTP_INTERCEPTORS = new InjectionToken<readonly HttpInterceptor[]>(
'HTTP_INTERCEPTORS' 'HTTP_INTERCEPTORS'

355
src/http/params.ts Normal file
View File

@ -0,0 +1,355 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import type { HttpParameterCodec } from '@ngify/http';
/**
* Provides encoding and decoding of URL parameter and query-string values.
*
* Serializes and parses URL parameter keys and values to encode and decode them.
* If you pass URL query parameters without encoding,
* the query parameters can be misinterpreted at the receiving end.
*
*/
export class HttpUrlEncodingCodec implements HttpParameterCodec {
/**
* Encodes a key name for a URL parameter or query-string.
* @param key The key name.
* @returns The encoded key name.
*/
encodeKey(key: string): string {
return standardEncoding(key);
}
/**
* Encodes the value of a URL parameter or query-string.
* @param value The value.
* @returns The encoded value.
*/
encodeValue(value: string): string {
return standardEncoding(value);
}
/**
* Decodes an encoded URL parameter or query-string key.
* @param key The encoded key name.
* @returns The decoded key name.
*/
decodeKey(key: string): string {
return decodeURIComponent(key);
}
/**
* Decodes an encoded URL parameter or query-string value.
* @param value The encoded value.
* @returns The decoded value.
*/
decodeValue(value: string) {
return decodeURIComponent(value);
}
}
/**
* Encode input string with standard encodeURIComponent and then un-encode specific characters.
*/
const STANDARD_ENCODING_REGEX = /%(\d[a-f0-9])/gi;
const STANDARD_ENCODING_REPLACEMENTS: { [x: string]: string } = {
'40': '@',
'3A': ':',
'24': '$',
'2C': ',',
'3B': ';',
'3D': '=',
'3F': '?',
'2F': '/',
};
function standardEncoding(v: string): string {
return encodeURIComponent(v).replace(
STANDARD_ENCODING_REGEX,
(s, t) => STANDARD_ENCODING_REPLACEMENTS[t] ?? s
);
}
function paramParser(
rawParams: string,
codec: HttpParameterCodec
): Map<string, string[]> {
const map = new Map<string, string[]>();
if (rawParams.length > 0) {
// The `window.location.search` can be used while creating an instance of the `HttpParams` class
// (e.g. `new HttpParams({ fromString: window.location.search })`). The `window.location.search`
// may start with the `?` char, so we strip it if it's present.
const params: string[] = rawParams.replace(/^\?/, '').split('&');
params.forEach((param: string) => {
const eqIdx = param.indexOf('=');
const [key, val]: string[] =
eqIdx === -1
? [codec.decodeKey(param), '']
: [
codec.decodeKey(param.slice(0, eqIdx)),
codec.decodeValue(param.slice(eqIdx + 1)),
];
const list = map.get(key) || [];
list.push(val);
map.set(key, list);
});
}
return map;
}
interface Update {
param: string;
value?: string | number | boolean;
op: 'a' | 'd' | 's';
}
/**
* Options used to construct an `HttpParams` instance.
*
*/
export interface HttpParamsOptions {
/**
* String representation of the HTTP parameters in URL-query-string format.
* Mutually exclusive with `fromObject`.
*/
fromString?: string;
/** Object map of the HTTP parameters. Mutually exclusive with `fromString`. */
fromObject?: {
[param: string]:
| string
| number
| boolean
| ReadonlyArray<string | number | boolean>;
};
/** Encoding codec used to parse and serialize the parameters. */
encoder?: HttpParameterCodec;
}
/**
*
* @ngify/http has slighty different implementation than Angular's HttpParams.
* So this file to keep implement to angular
* An HTTP request/response body that represents serialized parameters,
* per the MIME type `application/x-www-form-urlencoded`.
*
* This class is immutable; all mutation operations return a new instance.
*/
export class HttpParams {
private map: Map<string, string[]> | null;
private encoder: HttpParameterCodec;
private updates: Update[] | null = null;
private cloneFrom: HttpParams | null = null;
constructor(options: HttpParamsOptions = {} as HttpParamsOptions) {
this.encoder = options.encoder || new HttpUrlEncodingCodec();
if (options.fromString) {
if (options.fromObject) {
throw new Error('Cannot specify both fromString and fromObject.');
}
this.map = paramParser(options.fromString, this.encoder);
} else if (options.fromObject) {
this.map = new Map<string, string[]>();
Object.keys(options.fromObject).forEach((key) => {
const value = (options.fromObject as any)[key];
// convert the values to strings
const values = Array.isArray(value)
? value.map((value) => `${value}`)
: [`${value}`];
this.map!.set(key, values);
});
} else {
this.map = null;
}
}
/**
* Reports whether the body includes one or more values for a given parameter.
* @param param The parameter name.
* @returns True if the parameter has one or more values,
* false if it has no value or is not present.
*/
has(param: string): boolean {
this.init();
return this.map!.has(param);
}
/**
* Retrieves the first value for a parameter.
* @param param The parameter name.
* @returns The first value of the given parameter,
* or `null` if the parameter is not present.
*/
get(param: string): string | null {
this.init();
const res = this.map!.get(param);
return res ? res[0] : null;
}
/**
* Retrieves all values for a parameter.
* @param param The parameter name.
* @returns All values in a string array,
* or `null` if the parameter not present.
*/
getAll(param: string): string[] | null {
this.init();
return this.map!.get(param) || null;
}
/**
* Retrieves all the parameters for this body.
* @returns The parameter names in a string array.
*/
keys(): string[] {
this.init();
return Array.from(this.map!.keys());
}
/**
* Appends a new value to existing values for a parameter.
* @param param The parameter name.
* @param value The new value to add.
* @return A new body with the appended value.
*/
append(param: string, value: string | number | boolean): HttpParams {
return this.clone({ param, value, op: 'a' });
}
/**
* Constructs a new body with appended values for the given parameter name.
* @param params parameters and values
* @return A new body with the new value.
*/
appendAll(params: {
[param: string]:
| string
| number
| boolean
| ReadonlyArray<string | number | boolean>;
}): HttpParams {
const updates: Update[] = [];
Object.keys(params).forEach((param) => {
const value = params[param];
if (Array.isArray(value)) {
value.forEach((_value) => {
updates.push({ param, value: _value, op: 'a' });
});
} else {
updates.push({
param,
value: value as string | number | boolean,
op: 'a',
});
}
});
return this.clone(updates);
}
/**
* Replaces the value for a parameter.
* @param param The parameter name.
* @param value The new value.
* @return A new body with the new value.
*/
set(param: string, value: string | number | boolean): HttpParams {
return this.clone({ param, value, op: 's' });
}
/**
* Removes a given value or all values from a parameter.
* @param param The parameter name.
* @param value The value to remove, if provided.
* @return A new body with the given value removed, or with all values
* removed if no value is specified.
*/
delete(param: string, value?: string | number | boolean): HttpParams {
return this.clone({ param, value, op: 'd' });
}
/**
* Serializes the body to an encoded string, where key-value pairs (separated by `=`) are
* separated by `&`s.
*/
toString(): string {
this.init();
return (
this.keys()
.map((key) => {
const eKey = this.encoder.encodeKey(key);
// `a: ['1']` produces `'a=1'`
// `b: []` produces `''`
// `c: ['1', '2']` produces `'c=1&c=2'`
return this.map!.get(key)!
.map((value) => `${eKey}=${this.encoder.encodeValue(value)}`)
.join('&');
})
// filter out empty values because `b: []` produces `''`
// which results in `a=1&&c=1&c=2` instead of `a=1&c=1&c=2` if we don't
.filter((param) => param !== '')
.join('&')
);
}
private clone(update: Update | Update[]): HttpParams {
const clone = new HttpParams({
encoder: this.encoder,
} as HttpParamsOptions);
clone.cloneFrom = this.cloneFrom || this;
clone.updates = (this.updates || []).concat(update);
return clone;
}
private init() {
if (this.map === null) {
this.map = new Map<string, string[]>();
}
if (this.cloneFrom !== null) {
this.cloneFrom.init();
this.cloneFrom
.keys()
.forEach((key) => this.map!.set(key, this.cloneFrom!.map!.get(key)!));
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
this.updates!.forEach((update) => {
switch (update.op) {
case 'a':
case 's': {
const base =
(update.op === 'a' ? this.map!.get(update.param) : undefined) ||
[];
base.push(`${update.value!}`);
this.map!.set(update.param, base);
break;
}
case 'd': {
if (update.value !== undefined) {
const base = this.map!.get(update.param) || [];
const idx = base.indexOf(`${update.value}`);
if (idx !== -1) {
base.splice(idx, 1);
}
if (base.length > 0) {
this.map!.set(update.param, base);
} else {
this.map!.delete(update.param);
}
} else {
this.map!.delete(update.param);
break;
}
break;
}
default:
}
});
this.cloneFrom = this.updates = null;
}
}
}

View File

@ -1,6 +1,6 @@
import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { TestBed, mockImplementationWhenArgsEqual } from '@/testing';
import { lastValueFrom, of } from 'rxjs'; import { ReplaySubject, firstValueFrom, of } from 'rxjs';
import { skip } from 'rxjs/operators'; import { share, skip } from 'rxjs/operators';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
import { OidcSecurityService } from '../oidc.security.service'; import { OidcSecurityService } from '../oidc.security.service';
@ -333,7 +333,7 @@ describe('CheckSessionService', () => {
}); });
describe('init', () => { describe('init', () => {
it('returns falsy observable when lastIframerefresh and iframeRefreshInterval are bigger than now', async () => { it('angular oidc client', async () => {
const serviceAsAny = checkSessionService as any; const serviceAsAny = checkSessionService as any;
const dateNow = new Date(); const dateNow = new Date();
const lastRefresh = dateNow.setMinutes(dateNow.getMinutes() + 30); const lastRefresh = dateNow.setMinutes(dateNow.getMinutes() + 30);
@ -341,7 +341,7 @@ describe('CheckSessionService', () => {
serviceAsAny.lastIFrameRefresh = lastRefresh; serviceAsAny.lastIFrameRefresh = lastRefresh;
serviceAsAny.iframeRefreshInterval = lastRefresh; serviceAsAny.iframeRefreshInterval = lastRefresh;
const result = await lastValueFrom(serviceAsAny.init()); const result = await firstValueFrom(serviceAsAny.init());
expect(result).toBeUndefined(); expect(result).toBeUndefined();
}); });
}); });
@ -366,18 +366,27 @@ describe('CheckSessionService', () => {
describe('checkSessionChanged$', () => { describe('checkSessionChanged$', () => {
it('emits when internal event is thrown', async () => { it('emits when internal event is thrown', async () => {
const result = await lastValueFrom( const test$ = checkSessionService.checkSessionChanged$.pipe(
checkSessionService.checkSessionChanged$.pipe(skip(1)) skip(1),
share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: true,
})
); );
expect(result).toBe(true);
test$.subscribe();
const serviceAsAny = checkSessionService as any; const serviceAsAny = checkSessionService as any;
serviceAsAny.checkSessionChangedInternal$.next(true); serviceAsAny.checkSessionChangedInternal$.next(true);
const result = await firstValueFrom(test$);
expect(result).toBe(true);
}); });
it('emits false initially', async () => { it('emits false initially', async () => {
const result = await lastValueFrom( const result = await firstValueFrom(
checkSessionService.checkSessionChanged$ checkSessionService.checkSessionChanged$
); );
expect(result).toBe(false); expect(result).toBe(false);
@ -387,7 +396,7 @@ describe('CheckSessionService', () => {
const expectedResultsInOrder = [false, true]; const expectedResultsInOrder = [false, true];
let counter = 0; let counter = 0;
const result = await lastValueFrom( const result = await firstValueFrom(
checkSessionService.checkSessionChanged$ checkSessionService.checkSessionChanged$
); );
expect(result).toBe(expectedResultsInOrder[counter]); expect(result).toBe(expectedResultsInOrder[counter]);

View File

@ -1,4 +1,4 @@
import { Injectable, NgZone, type OnDestroy, inject } from 'injection-js'; import { Injectable, inject } from 'injection-js';
import { BehaviorSubject, Observable, of } from 'rxjs'; import { BehaviorSubject, Observable, of } from 'rxjs';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import type { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
@ -14,7 +14,7 @@ const IFRAME_FOR_CHECK_SESSION_IDENTIFIER = 'myiFrameForCheckSession';
// http://openid.net/specs/openid-connect-session-1_0-ID4.html // http://openid.net/specs/openid-connect-session-1_0-ID4.html
@Injectable() @Injectable()
export class CheckSessionService implements OnDestroy { export class CheckSessionService {
private readonly loggerService = inject(LoggerService); private readonly loggerService = inject(LoggerService);
private readonly storagePersistenceService = inject( private readonly storagePersistenceService = inject(
@ -25,8 +25,6 @@ export class CheckSessionService implements OnDestroy {
private readonly eventService = inject(PublicEventsService); private readonly eventService = inject(PublicEventsService);
private readonly zone = inject(NgZone);
private readonly document = inject(DOCUMENT); private readonly document = inject(DOCUMENT);
private checkSessionReceived = false; private checkSessionReceived = false;
@ -54,7 +52,7 @@ export class CheckSessionService implements OnDestroy {
return this.checkSessionChangedInternal$.asObservable(); return this.checkSessionChangedInternal$.asObservable();
} }
ngOnDestroy(): void { [Symbol.dispose]() {
this.stop(); this.stop();
const windowAsDefaultView = this.document.defaultView; const windowAsDefaultView = this.document.defaultView;
@ -104,9 +102,9 @@ export class CheckSessionService implements OnDestroy {
); );
} }
private init(configuration: OpenIdConfiguration): Observable<void> { private init(configuration: OpenIdConfiguration): Observable<undefined> {
if (this.lastIFrameRefresh + this.iframeRefreshInterval > Date.now()) { if (this.lastIFrameRefresh + this.iframeRefreshInterval > Date.now()) {
return of(); return of(undefined);
} }
const authWellKnownEndPoints = this.storagePersistenceService.read( const authWellKnownEndPoints = this.storagePersistenceService.read(
@ -120,7 +118,7 @@ export class CheckSessionService implements OnDestroy {
'CheckSession - init check session: authWellKnownEndpoints is undefined. Returning.' 'CheckSession - init check session: authWellKnownEndpoints is undefined. Returning.'
); );
return of(); return of(undefined);
} }
const existingIframe = this.getOrCreateIframe(configuration); const existingIframe = this.getOrCreateIframe(configuration);
@ -138,7 +136,7 @@ export class CheckSessionService implements OnDestroy {
'CheckSession - init check session: checkSessionIframe is not configured to run' 'CheckSession - init check session: checkSessionIframe is not configured to run'
); );
return of(); return of(undefined);
} }
if (contentWindow) { if (contentWindow) {
@ -228,13 +226,11 @@ export class CheckSessionService implements OnDestroy {
); );
} }
this.zone.runOutsideAngular(() => { this.scheduledHeartBeatRunning =
this.scheduledHeartBeatRunning = this.document?.defaultView?.setTimeout(
this.document?.defaultView?.setTimeout( pollServerSessionRecur,
() => this.zone.run(pollServerSessionRecur), this.heartBeatInterval
this.heartBeatInterval ) ?? null;
) ?? null;
});
}); });
}; };

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { lastValueFrom, of } from 'rxjs'; import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
import { mockProvider } from '../testing/mock'; import { mockProvider } from '../testing/mock';
@ -41,26 +41,27 @@ describe('RefreshSessionIframeService ', () => {
.mockReturnValue(of(null)); .mockReturnValue(of(null));
const allConfigs = [{ configId: 'configId1' }]; const allConfigs = [{ configId: 'configId1' }];
await lastValueFrom(refreshSessionIframeService await firstValueFrom(
.refreshSessionWithIframe(allConfigs[0]!, allConfigs)); refreshSessionIframeService.refreshSessionWithIframe(
expect( allConfigs[0]!,
sendAuthorizeRequestUsingSilentRenewSpy allConfigs
).toHaveBeenCalledExactlyOnceWith( )
'a-url', );
allConfigs[0]!, expect(
allConfigs sendAuthorizeRequestUsingSilentRenewSpy
); ).toHaveBeenCalledExactlyOnceWith('a-url', allConfigs[0]!, allConfigs);
}); });
}); });
describe('initSilentRenewRequest', () => { describe('initSilentRenewRequest', () => {
it('dispatches customevent to window object', async () => { it('dispatches customevent to window object', () => {
const dispatchEventSpy = vi.spyOn(window, 'dispatchEvent'); const dispatchEventSpy = vi.spyOn(
document.defaultView?.window!,
await lastValueFrom( 'dispatchEvent'
(refreshSessionIframeService as any).initSilentRenewRequest()
); );
(refreshSessionIframeService as any).initSilentRenewRequest();
expect(dispatchEventSpy).toHaveBeenCalledExactlyOnceWith( expect(dispatchEventSpy).toHaveBeenCalledExactlyOnceWith(
new CustomEvent('oidc-silent-renew-init', { new CustomEvent('oidc-silent-renew-init', {
detail: expect.any(Number), detail: expect.any(Number),

View File

@ -1,6 +1,11 @@
import { Injectable, RendererFactory2, inject } from 'injection-js'; import { Injectable, inject } from 'injection-js';
import { Observable } from 'rxjs'; import {
import { switchMap } from 'rxjs/operators'; Observable,
ReplaySubject,
type Subscription,
fromEventPattern,
} from 'rxjs';
import { filter, share, switchMap, takeUntil } from 'rxjs/operators';
import type { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
import { DOCUMENT } from '../dom'; import { DOCUMENT } from '../dom';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
@ -9,11 +14,6 @@ import { SilentRenewService } from './silent-renew.service';
@Injectable() @Injectable()
export class RefreshSessionIframeService { export class RefreshSessionIframeService {
private readonly renderer = inject(RendererFactory2).createRenderer(
null,
null
);
private readonly loggerService = inject(LoggerService); private readonly loggerService = inject(LoggerService);
private readonly urlService = inject(UrlService); private readonly urlService = inject(UrlService);
@ -22,6 +22,8 @@ export class RefreshSessionIframeService {
private readonly document = inject(DOCUMENT); private readonly document = inject(DOCUMENT);
private silentRenewEventHandlerSubscription?: Subscription;
refreshSessionWithIframe( refreshSessionWithIframe(
config: OpenIdConfiguration, config: OpenIdConfiguration,
allConfigs: OpenIdConfiguration[], allConfigs: OpenIdConfiguration[],
@ -80,24 +82,53 @@ export class RefreshSessionIframeService {
): void { ): void {
const instanceId = Math.random(); const instanceId = Math.random();
const initDestroyHandler = this.renderer.listen( const oidcSilentRenewInit$ = fromEventPattern(
'window', (handler) =>
'oidc-silent-renew-init', this.document.defaultView.window.addEventListener(
(e: CustomEvent) => { 'oidc-silent-renew-init',
if (e.detail !== instanceId) { handler
initDestroyHandler(); ),
renewDestroyHandler(); (handler) =>
} this.document.defaultView.window.removeEventListener(
} 'oidc-silent-renew-init',
); handler
const renewDestroyHandler = this.renderer.listen( )
'window',
'oidc-silent-renew-message',
(e) =>
this.silentRenewService.silentRenewEventHandler(e, config, allConfigs)
); );
this.document.defaultView?.dispatchEvent( const oidcSilentRenewInitNotSelf$ = oidcSilentRenewInit$.pipe(
filter((e: CustomEvent) => e.detail !== instanceId)
);
if (this.silentRenewEventHandlerSubscription) {
this.silentRenewEventHandlerSubscription.unsubscribe();
}
this.silentRenewEventHandlerSubscription = fromEventPattern<CustomEvent>(
(handler) =>
this.document.defaultView.window.addEventListener(
'oidc-silent-renew-message',
handler
),
(handler) =>
this.document.defaultView.window.removeEventListener(
'oidc-silent-renew-message',
handler
)
)
.pipe(
takeUntil(oidcSilentRenewInitNotSelf$),
switchMap((e) =>
this.silentRenewService.silentRenewEventHandler(e, config, allConfigs)
),
share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: true,
})
)
.subscribe();
this.document.defaultView?.window.dispatchEvent(
new CustomEvent('oidc-silent-renew-init', { new CustomEvent('oidc-silent-renew-init', {
detail: instanceId, detail: instanceId,
}) })

View File

@ -1,5 +1,12 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { Observable, lastValueFrom, of, throwError } from 'rxjs'; import {
Observable,
ReplaySubject,
firstValueFrom,
of,
share,
throwError,
} from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { AuthStateService } from '../auth-state/auth-state.service'; import { AuthStateService } from '../auth-state/auth-state.service';
import { ImplicitFlowCallbackService } from '../callback/implicit-flow-callback.service'; import { ImplicitFlowCallbackService } from '../callback/implicit-flow-callback.service';
@ -28,6 +35,7 @@ describe('SilentRenewService ', () => {
let intervalService: IntervalService; let intervalService: IntervalService;
beforeEach(() => { beforeEach(() => {
vi.useFakeTimers();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
SilentRenewService, SilentRenewService,
@ -54,6 +62,11 @@ describe('SilentRenewService ', () => {
intervalService = TestBed.inject(IntervalService); intervalService = TestBed.inject(IntervalService);
}); });
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => {
vi.useRealTimers();
});
it('should create', () => { it('should create', () => {
expect(silentRenewService).toBeTruthy(); expect(silentRenewService).toBeTruthy();
}); });
@ -149,7 +162,7 @@ describe('SilentRenewService ', () => {
const urlParts = const urlParts =
'code=some-code&state=some-state&session_state=some-session-state'; 'code=some-code&state=some-state&session_state=some-session-state';
await lastValueFrom( await firstValueFrom(
silentRenewService.codeFlowCallbackSilentRenewIframe( silentRenewService.codeFlowCallbackSilentRenewIframe(
[url, urlParts], [url, urlParts],
config, config,
@ -188,7 +201,7 @@ describe('SilentRenewService ', () => {
const urlParts = 'error=some_error'; const urlParts = 'error=some_error';
try { try {
await lastValueFrom( await firstValueFrom(
silentRenewService.codeFlowCallbackSilentRenewIframe( silentRenewService.codeFlowCallbackSilentRenewIframe(
[url, urlParts], [url, urlParts],
config, config,
@ -312,19 +325,31 @@ describe('SilentRenewService ', () => {
const eventData = { detail: 'detail?detail2' } as CustomEvent; const eventData = { detail: 'detail?detail2' } as CustomEvent;
const allConfigs = [{ configId: 'configId1' }]; const allConfigs = [{ configId: 'configId1' }];
const result = await lastValueFrom( const test$ = silentRenewService.refreshSessionWithIFrameCompleted$.pipe(
silentRenewService.refreshSessionWithIFrameCompleted$ share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: true,
})
); );
test$.subscribe();
await firstValueFrom(
silentRenewService.silentRenewEventHandler(
eventData,
allConfigs[0]!,
allConfigs
)
);
await vi.advanceTimersByTimeAsync(1000);
const result = await firstValueFrom(test$);
expect(result).toEqual({ expect(result).toEqual({
refreshToken: 'callbackContext', refreshToken: 'callbackContext',
} as CallbackContext); } as CallbackContext);
silentRenewService.silentRenewEventHandler(
eventData,
allConfigs[0]!,
allConfigs
);
await vi.advanceTimersByTimeAsync(1000);
}); });
it('loggs and calls flowsDataService.resetSilentRenewRunning in case of an error', async () => { it('loggs and calls flowsDataService.resetSilentRenewRunning in case of an error', async () => {
@ -341,10 +366,12 @@ describe('SilentRenewService ', () => {
const allConfigs = [{ configId: 'configId1' }]; const allConfigs = [{ configId: 'configId1' }];
const eventData = { detail: 'detail?detail2' } as CustomEvent; const eventData = { detail: 'detail?detail2' } as CustomEvent;
silentRenewService.silentRenewEventHandler( await firstValueFrom(
eventData, silentRenewService.silentRenewEventHandler(
allConfigs[0]!, eventData,
allConfigs allConfigs[0]!,
allConfigs
)
); );
await vi.advanceTimersByTimeAsync(1000); await vi.advanceTimersByTimeAsync(1000);
expect(resetSilentRenewRunningSpy).toHaveBeenCalledTimes(1); expect(resetSilentRenewRunningSpy).toHaveBeenCalledTimes(1);
@ -360,17 +387,28 @@ describe('SilentRenewService ', () => {
const eventData = { detail: 'detail?detail2' } as CustomEvent; const eventData = { detail: 'detail?detail2' } as CustomEvent;
const allConfigs = [{ configId: 'configId1' }]; const allConfigs = [{ configId: 'configId1' }];
const result = await lastValueFrom( const test$ = silentRenewService.refreshSessionWithIFrameCompleted$.pipe(
silentRenewService.refreshSessionWithIFrameCompleted$ share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: true,
})
); );
expect(result).toBeNull();
silentRenewService.silentRenewEventHandler( test$.subscribe();
eventData,
allConfigs[0]!, await firstValueFrom(
allConfigs silentRenewService.silentRenewEventHandler(
eventData,
allConfigs[0]!,
allConfigs
)
); );
await vi.advanceTimersByTimeAsync(1000); await vi.advanceTimersByTimeAsync(1000);
const result = await firstValueFrom(test$);
expect(result).toBeNull();
}); });
}); });
}); });

View File

@ -1,7 +1,6 @@
import { HttpParams } from '@ngify/http'; import { Injectable, inject } from 'injection-js';
import { Injectable, inject } from 'injection-js'; import { type Observable, Subject, of, throwError } from 'rxjs';
import { type Observable, Subject, throwError } from 'rxjs'; import { catchError, map } from 'rxjs/operators';
import { catchError } from 'rxjs/operators';
import { AuthStateService } from '../auth-state/auth-state.service'; import { AuthStateService } from '../auth-state/auth-state.service';
import { ImplicitFlowCallbackService } from '../callback/implicit-flow-callback.service'; import { ImplicitFlowCallbackService } from '../callback/implicit-flow-callback.service';
import { IntervalService } from '../callback/interval.service'; import { IntervalService } from '../callback/interval.service';
@ -10,6 +9,7 @@ import type { CallbackContext } from '../flows/callback-context';
import { FlowsDataService } from '../flows/flows-data.service'; import { FlowsDataService } from '../flows/flows-data.service';
import { FlowsService } from '../flows/flows.service'; import { FlowsService } from '../flows/flows.service';
import { ResetAuthDataService } from '../flows/reset-auth-data.service'; import { ResetAuthDataService } from '../flows/reset-auth-data.service';
import { HttpParams } from '../http';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
import { FlowHelper } from '../utils/flowHelper/flow-helper.service'; import { FlowHelper } from '../utils/flowHelper/flow-helper.service';
import { ValidationResult } from '../validation/validation-result'; import { ValidationResult } from '../validation/validation-result';
@ -70,8 +70,9 @@ export class SilentRenewService {
config: OpenIdConfiguration, config: OpenIdConfiguration,
allConfigs: OpenIdConfiguration[] allConfigs: OpenIdConfiguration[]
): Observable<CallbackContext> { ): Observable<CallbackContext> {
// TODO: fix @ngify/http const params = new HttpParams({
const params = new HttpParams(urlParts[1] || undefined); fromString: urlParts[1],
});
const errorParam = params.get('error'); const errorParam = params.get('error');
@ -120,10 +121,10 @@ export class SilentRenewService {
e: CustomEvent, e: CustomEvent,
config: OpenIdConfiguration, config: OpenIdConfiguration,
allConfigs: OpenIdConfiguration[] allConfigs: OpenIdConfiguration[]
): void { ): Observable<undefined> {
this.loggerService.logDebug(config, 'silentRenewEventHandler'); this.loggerService.logDebug(config, 'silentRenewEventHandler');
if (!e.detail) { if (!e.detail) {
return; return of(undefined);
} }
let callback$: Observable<CallbackContext>; let callback$: Observable<CallbackContext>;
@ -146,17 +147,19 @@ export class SilentRenewService {
); );
} }
callback$.subscribe({ return callback$.pipe(
next: (callbackContext) => { map((callbackContext) => {
this.refreshSessionWithIFrameCompletedInternal$.next(callbackContext); this.refreshSessionWithIFrameCompletedInternal$.next(callbackContext);
this.flowsDataService.resetSilentRenewRunning(config); this.flowsDataService.resetSilentRenewRunning(config);
}, return undefined;
error: (err: unknown) => { }),
catchError((err: unknown) => {
this.loggerService.logError(config, `Error: ${err}`); this.loggerService.logError(config, `Error: ${err}`);
this.refreshSessionWithIFrameCompletedInternal$.next(null); this.refreshSessionWithIFrameCompletedInternal$.next(null);
this.flowsDataService.resetSilentRenewRunning(config); this.flowsDataService.resetSilentRenewRunning(config);
}, return of(undefined);
}); })
);
} }
private getExistingIframe(): HTMLIFrameElement | null { private getExistingIframe(): HTMLIFrameElement | null {

View File

@ -1,6 +1,6 @@
// Public classes. // Public classes.
export { PassedInitialConfig } from './auth-config'; export type { PassedInitialConfig } from './auth-config';
export * from './auth-options'; export * from './auth-options';
export * from './auth-state/auth-result'; export * from './auth-state/auth-result';
export * from './auth-state/auth-state'; export * from './auth-state/auth-state';

View File

@ -1,3 +1,3 @@
export { Module } from './module'; export type { Module } from './module';
export { APP_INITIALIZER } from './convention'; export { APP_INITIALIZER } from './convention';
export { injectAbstractType } from './inject'; export { injectAbstractType } from './inject';

View File

@ -1,4 +1,3 @@
import 'reflect-metadata';
import type { Injector } from 'injection-js'; import type { Injector } from 'injection-js';
export type Module = (parentInjector: Injector) => Injector; export type Module = (parentInjector: Injector) => Injector;

View File

@ -10,7 +10,7 @@ import {
HttpTestingController, HttpTestingController,
provideHttpClientTesting, provideHttpClientTesting,
} from '@ngify/http/testing'; } from '@ngify/http/testing';
import { lastValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { AuthStateService } from '../auth-state/auth-state.service'; import { AuthStateService } from '../auth-state/auth-state.service';
import { ConfigurationService } from '../config/config.service'; import { ConfigurationService } from '../config/config.service';
@ -106,7 +106,7 @@ describe('AuthHttpInterceptor', () => {
true true
); );
const response = await lastValueFrom(httpClient.get(actionUrl)); const response = await firstValueFrom(httpClient.get(actionUrl));
expect(response).toBeTruthy(); expect(response).toBeTruthy();
const httpRequest = httpTestingController.expectOne(actionUrl); const httpRequest = httpTestingController.expectOne(actionUrl);
@ -132,7 +132,7 @@ describe('AuthHttpInterceptor', () => {
true true
); );
const response = await lastValueFrom(httpClient.get(actionUrl)); const response = await firstValueFrom(httpClient.get(actionUrl));
expect(response).toBeTruthy(); expect(response).toBeTruthy();
const httpRequest = httpTestingController.expectOne(actionUrl); const httpRequest = httpTestingController.expectOne(actionUrl);
@ -160,7 +160,7 @@ describe('AuthHttpInterceptor', () => {
'thisIsAToken' 'thisIsAToken'
); );
const response = await lastValueFrom(httpClient.get(actionUrl)); const response = await firstValueFrom(httpClient.get(actionUrl));
expect(response).toBeTruthy(); expect(response).toBeTruthy();
const httpRequest = httpTestingController.expectOne(actionUrl); const httpRequest = httpTestingController.expectOne(actionUrl);
@ -185,7 +185,7 @@ describe('AuthHttpInterceptor', () => {
true true
); );
const response = await lastValueFrom(httpClient.get(actionUrl)); const response = await firstValueFrom(httpClient.get(actionUrl));
expect(response).toBeTruthy(); expect(response).toBeTruthy();
const httpRequest = httpTestingController.expectOne(actionUrl); const httpRequest = httpTestingController.expectOne(actionUrl);
@ -211,7 +211,7 @@ describe('AuthHttpInterceptor', () => {
); );
vi.spyOn(authStateService, 'getAccessToken').mockReturnValue(''); vi.spyOn(authStateService, 'getAccessToken').mockReturnValue('');
const response = await lastValueFrom(httpClient.get(actionUrl)); const response = await firstValueFrom(httpClient.get(actionUrl));
expect(response).toBeTruthy(); expect(response).toBeTruthy();
const httpRequest = httpTestingController.expectOne(actionUrl); const httpRequest = httpTestingController.expectOne(actionUrl);
@ -229,7 +229,7 @@ describe('AuthHttpInterceptor', () => {
false false
); );
const response = await lastValueFrom(httpClient.get(actionUrl)); const response = await firstValueFrom(httpClient.get(actionUrl));
expect(response).toBeTruthy(); expect(response).toBeTruthy();
const httpRequest = httpTestingController.expectOne(actionUrl); const httpRequest = httpTestingController.expectOne(actionUrl);
@ -260,7 +260,7 @@ describe('AuthHttpInterceptor', () => {
matchingConfig: null, matchingConfig: null,
}); });
const response = await lastValueFrom(httpClient.get(actionUrl)); const response = await firstValueFrom(httpClient.get(actionUrl));
expect(response).toBeTruthy(); expect(response).toBeTruthy();
const httpRequest = httpTestingController.expectOne(actionUrl); const httpRequest = httpTestingController.expectOne(actionUrl);
@ -286,10 +286,10 @@ describe('AuthHttpInterceptor', () => {
true true
); );
let response = await lastValueFrom(httpClient.get(actionUrl)); let response = await firstValueFrom(httpClient.get(actionUrl));
expect(response).toBeTruthy(); expect(response).toBeTruthy();
response = await lastValueFrom(httpClient.get(actionUrl2)); response = await firstValueFrom(httpClient.get(actionUrl2));
expect(response).toBeTruthy(); expect(response).toBeTruthy();
const httpRequest = httpTestingController.expectOne(actionUrl); const httpRequest = httpTestingController.expectOne(actionUrl);

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { lastValueFrom, of } from 'rxjs'; import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { StoragePersistenceService } from '../storage/storage-persistence.service'; import { StoragePersistenceService } from '../storage/storage-persistence.service';
import { mockProvider } from '../testing/mock'; import { mockProvider } from '../testing/mock';
@ -83,7 +83,7 @@ describe('LoginService', () => {
); );
}); });
it("should throw error if configuration is null and doesn't call loginPar or loginStandard", () => { it("should throw error if configuration is null and doesn't call loginPar or loginStandard", async () => {
// arrange // arrange
// biome-ignore lint/suspicious/noEvolvingTypes: <explanation> // biome-ignore lint/suspicious/noEvolvingTypes: <explanation>
const config = null; const config = null;
@ -91,13 +91,16 @@ describe('LoginService', () => {
const standardLoginSpy = vi.spyOn(standardLoginService, 'loginStandard'); const standardLoginSpy = vi.spyOn(standardLoginService, 'loginStandard');
const authOptions = { customParams: { custom: 'params' } }; const authOptions = { customParams: { custom: 'params' } };
// act try {
const fn = (): void => service.login(config, authOptions); await firstValueFrom(service.login(config, authOptions));
expect.fail('should be error');
// assert } catch (error: unknown) {
expect(fn).toThrow( expect(error).toEqual(
new Error('Please provide a configuration before setting up the module') new Error(
); 'Please provide a configuration before setting up the module'
)
);
}
expect(loginParSpy).not.toHaveBeenCalled(); expect(loginParSpy).not.toHaveBeenCalled();
expect(standardLoginSpy).not.toHaveBeenCalled(); expect(standardLoginSpy).not.toHaveBeenCalled();
}); });
@ -115,7 +118,7 @@ describe('LoginService', () => {
.mockReturnValue(of({} as LoginResponse)); .mockReturnValue(of({} as LoginResponse));
// act // act
await lastValueFrom(service.loginWithPopUp(config, [config])); await firstValueFrom(service.loginWithPopUp(config, [config]));
expect(loginWithPopUpPar).toHaveBeenCalledTimes(1); expect(loginWithPopUpPar).toHaveBeenCalledTimes(1);
expect(loginWithPopUpStandardSpy).not.toHaveBeenCalled(); expect(loginWithPopUpStandardSpy).not.toHaveBeenCalled();
}); });
@ -131,7 +134,7 @@ describe('LoginService', () => {
.mockReturnValue(of({} as LoginResponse)); .mockReturnValue(of({} as LoginResponse));
// act // act
await lastValueFrom(service.loginWithPopUp(config, [config])); await firstValueFrom(service.loginWithPopUp(config, [config]));
expect(loginWithPopUpPar).not.toHaveBeenCalled(); expect(loginWithPopUpPar).not.toHaveBeenCalled();
expect(loginWithPopUpStandardSpy).toHaveBeenCalledTimes(1); expect(loginWithPopUpStandardSpy).toHaveBeenCalledTimes(1);
}); });
@ -150,7 +153,7 @@ describe('LoginService', () => {
); );
// act // act
await lastValueFrom( await firstValueFrom(
service.loginWithPopUp(config, [config], authOptions) service.loginWithPopUp(config, [config], authOptions)
); );
expect(storagePersistenceServiceSpy).toHaveBeenCalledExactlyOnceWith( expect(storagePersistenceServiceSpy).toHaveBeenCalledExactlyOnceWith(
@ -174,7 +177,7 @@ describe('LoginService', () => {
vi.spyOn(popUpService, 'isCurrentlyInPopup').mockReturnValue(true); vi.spyOn(popUpService, 'isCurrentlyInPopup').mockReturnValue(true);
// act // act
const result = await lastValueFrom( const result = await firstValueFrom(
service.loginWithPopUp(config, [config], authOptions) service.loginWithPopUp(config, [config], authOptions)
); );
expect(result).toEqual({ expect(result).toEqual({

View File

@ -1,5 +1,5 @@
import { Injectable, inject } from 'injection-js'; import { Injectable, inject } from 'injection-js';
import { type Observable, of } from 'rxjs'; import { type Observable, of, throwError } from 'rxjs';
import type { AuthOptions } from '../auth-options'; import type { AuthOptions } from '../auth-options';
import type { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
import { StoragePersistenceService } from '../storage/storage-persistence.service'; import { StoragePersistenceService } from '../storage/storage-persistence.service';
@ -27,10 +27,13 @@ export class LoginService {
login( login(
configuration: OpenIdConfiguration | null, configuration: OpenIdConfiguration | null,
authOptions?: AuthOptions authOptions?: AuthOptions
): void { ): Observable<void> {
if (!configuration) { if (!configuration) {
throw new Error( return throwError(
'Please provide a configuration before setting up the module' () =>
new Error(
'Please provide a configuration before setting up the module'
)
); );
} }
@ -45,10 +48,9 @@ export class LoginService {
} }
if (usePushedAuthorisationRequests) { if (usePushedAuthorisationRequests) {
this.parLoginService.loginPar(configuration, authOptions); return this.parLoginService.loginPar(configuration, authOptions);
} else {
this.standardLoginService.loginStandard(configuration, authOptions);
} }
return this.standardLoginService.loginStandard(configuration, authOptions);
} }
loginWithPopUp( loginWithPopUp(

View File

@ -1,5 +1,5 @@
import { TestBed, spyOnProperty } from '@/testing'; import { TestBed, spyOnProperty } from '@/testing';
import { lastValueFrom, of } from 'rxjs'; import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { CheckAuthService } from '../../auth-state/check-auth.service'; import { CheckAuthService } from '../../auth-state/check-auth.service';
import { AuthWellKnownService } from '../../config/auth-well-known/auth-well-known.service'; import { AuthWellKnownService } from '../../config/auth-well-known/auth-well-known.service';
@ -65,7 +65,7 @@ describe('ParLoginService', () => {
).mockReturnValue(false); ).mockReturnValue(false);
const loggerSpy = vi.spyOn(loggerService, 'logError'); const loggerSpy = vi.spyOn(loggerService, 'logError');
const result = await lastValueFrom(service.loginPar({})); const result = await firstValueFrom(service.loginPar({}));
expect(result).toBeUndefined(); expect(result).toBeUndefined();
expect(loggerSpy).toHaveBeenCalled(); expect(loggerSpy).toHaveBeenCalled();
@ -86,7 +86,7 @@ describe('ParLoginService', () => {
.spyOn(parService, 'postParRequest') .spyOn(parService, 'postParRequest')
.mockReturnValue(of({ requestUri: 'requestUri' } as ParResponse)); .mockReturnValue(of({ requestUri: 'requestUri' } as ParResponse));
const result = await lastValueFrom( const result = await firstValueFrom(
service.loginPar({ service.loginPar({
authWellknownEndpointUrl: 'authWellknownEndpoint', authWellknownEndpointUrl: 'authWellknownEndpoint',
responseType: 'stubValue', responseType: 'stubValue',
@ -116,7 +116,7 @@ describe('ParLoginService', () => {
.spyOn(parService, 'postParRequest') .spyOn(parService, 'postParRequest')
.mockReturnValue(of({ requestUri: 'requestUri' } as ParResponse)); .mockReturnValue(of({ requestUri: 'requestUri' } as ParResponse));
const result = await lastValueFrom( const result = await firstValueFrom(
service.loginPar(config, { service.loginPar(config, {
customParams: { some: 'thing' }, customParams: { some: 'thing' },
}) })
@ -149,7 +149,7 @@ describe('ParLoginService', () => {
vi.spyOn(urlService, 'getAuthorizeParUrl').mockReturnValue(''); vi.spyOn(urlService, 'getAuthorizeParUrl').mockReturnValue('');
const spy = vi.spyOn(loggerService, 'logError'); const spy = vi.spyOn(loggerService, 'logError');
const result = await lastValueFrom(service.loginPar(config)); const result = await firstValueFrom(service.loginPar(config));
expect(result).toBeUndefined(); expect(result).toBeUndefined();
expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(1);
@ -180,7 +180,7 @@ describe('ParLoginService', () => {
); );
const spy = vi.spyOn(redirectService, 'redirectTo'); const spy = vi.spyOn(redirectService, 'redirectTo');
await lastValueFrom(service.loginPar(config, authOptions)); await firstValueFrom(service.loginPar(config, authOptions));
expect(spy).toHaveBeenCalledExactlyOnceWith('some-par-url'); expect(spy).toHaveBeenCalledExactlyOnceWith('some-par-url');
}); });
@ -212,7 +212,7 @@ describe('ParLoginService', () => {
spy(url); spy(url);
}; };
service.loginPar(config, { urlHandler }); await firstValueFrom(service.loginPar(config, { urlHandler }));
expect(spy).toHaveBeenCalledExactlyOnceWith('some-par-url'); expect(spy).toHaveBeenCalledExactlyOnceWith('some-par-url');
expect(redirectToSpy).not.toHaveBeenCalled(); expect(redirectToSpy).not.toHaveBeenCalled();
@ -230,7 +230,7 @@ describe('ParLoginService', () => {
const allConfigs = [config]; const allConfigs = [config];
try { try {
await lastValueFrom(service.loginWithPopUpPar(config, allConfigs)); await firstValueFrom(service.loginWithPopUpPar(config, allConfigs));
} catch (err: any) { } catch (err: any) {
expect(loggerSpy).toHaveBeenCalled(); expect(loggerSpy).toHaveBeenCalled();
expect(err.message).toBe('Invalid response type!'); expect(err.message).toBe('Invalid response type!');
@ -258,7 +258,7 @@ describe('ParLoginService', () => {
.mockReturnValue(of({ requestUri: 'requestUri' } as ParResponse)); .mockReturnValue(of({ requestUri: 'requestUri' } as ParResponse));
try { try {
await lastValueFrom(service.loginWithPopUpPar(config, allConfigs)); await firstValueFrom(service.loginWithPopUpPar(config, allConfigs));
} catch (err: any) { } catch (err: any) {
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
expect(err.message).toBe( expect(err.message).toBe(
@ -288,7 +288,7 @@ describe('ParLoginService', () => {
.mockReturnValue(of({ requestUri: 'requestUri' } as ParResponse)); .mockReturnValue(of({ requestUri: 'requestUri' } as ParResponse));
try { try {
await lastValueFrom( await firstValueFrom(
service.loginWithPopUpPar(config, allConfigs, { service.loginWithPopUpPar(config, allConfigs, {
customParams: { some: 'thing' }, customParams: { some: 'thing' },
}) })
@ -326,7 +326,7 @@ describe('ParLoginService', () => {
const spy = vi.spyOn(loggerService, 'logError'); const spy = vi.spyOn(loggerService, 'logError');
try { try {
await lastValueFrom( await firstValueFrom(
service.loginWithPopUpPar(config, allConfigs, { service.loginWithPopUpPar(config, allConfigs, {
customParams: { some: 'thing' }, customParams: { some: 'thing' },
}) })
@ -369,7 +369,7 @@ describe('ParLoginService', () => {
); );
const spy = vi.spyOn(popupService, 'openPopUp'); const spy = vi.spyOn(popupService, 'openPopUp');
await lastValueFrom(service.loginWithPopUpPar(config, allConfigs)); await firstValueFrom(service.loginWithPopUpPar(config, allConfigs));
expect(spy).toHaveBeenCalledExactlyOnceWith( expect(spy).toHaveBeenCalledExactlyOnceWith(
'some-par-url', 'some-par-url',
undefined, undefined,
@ -419,7 +419,7 @@ describe('ParLoginService', () => {
spyOnProperty(popupService, 'result$').mockReturnValue(of(popupResult)); spyOnProperty(popupService, 'result$').mockReturnValue(of(popupResult));
const result = await lastValueFrom( const result = await firstValueFrom(
service.loginWithPopUpPar(config, allConfigs) service.loginWithPopUpPar(config, allConfigs)
); );
expect(checkAuthSpy).toHaveBeenCalledExactlyOnceWith( expect(checkAuthSpy).toHaveBeenCalledExactlyOnceWith(
@ -465,7 +465,7 @@ describe('ParLoginService', () => {
spyOnProperty(popupService, 'result$').mockReturnValue(of(popupResult)); spyOnProperty(popupService, 'result$').mockReturnValue(of(popupResult));
const result = await lastValueFrom( const result = await firstValueFrom(
service.loginWithPopUpPar(config, allConfigs) service.loginWithPopUpPar(config, allConfigs)
); );
expect(checkAuthSpy).not.toHaveBeenCalled(); expect(checkAuthSpy).not.toHaveBeenCalled();

View File

@ -1,6 +1,6 @@
import { Injectable, inject } from 'injection-js'; import { Injectable, inject } from 'injection-js';
import { type Observable, of, throwError } from 'rxjs'; import { type Observable, of, throwError } from 'rxjs';
import { map, shareReplay, switchMap, take } from 'rxjs/operators'; import { map, switchMap, take } from 'rxjs/operators';
import type { AuthOptions } from '../../auth-options'; import type { AuthOptions } from '../../auth-options';
import { CheckAuthService } from '../../auth-state/check-auth.service'; import { CheckAuthService } from '../../auth-state/check-auth.service';
import { AuthWellKnownService } from '../../config/auth-well-known/auth-well-known.service'; import { AuthWellKnownService } from '../../config/auth-well-known/auth-well-known.service';
@ -47,7 +47,7 @@ export class ParLoginService {
) { ) {
this.loggerService.logError(configuration, 'Invalid response type!'); this.loggerService.logError(configuration, 'Invalid response type!');
return; return of(undefined);
} }
this.loggerService.logDebug( this.loggerService.logDebug(
@ -55,53 +55,42 @@ export class ParLoginService {
'BEGIN Authorize OIDC Flow, no auth data' 'BEGIN Authorize OIDC Flow, no auth data'
); );
const result$ = this.authWellKnownService return this.authWellKnownService
.queryAndStoreAuthWellKnownEndPoints(configuration) .queryAndStoreAuthWellKnownEndPoints(configuration)
.pipe( .pipe(
switchMap(() => switchMap(() =>
this.parService.postParRequest(configuration, authOptions) this.parService.postParRequest(configuration, authOptions)
), ),
map(() => { map((response) => {
(response) => { this.loggerService.logDebug(
this.loggerService.logDebug( configuration,
'par response: ',
response
);
const url = this.urlService.getAuthorizeParUrl(
response.requestUri,
configuration
);
this.loggerService.logDebug(configuration, 'par request url: ', url);
if (!url) {
this.loggerService.logError(
configuration, configuration,
'par response: ', `Could not create URL with param ${response.requestUri}: '${url}'`
response
); );
const url = this.urlService.getAuthorizeParUrl( return;
response.requestUri, }
configuration
);
this.loggerService.logDebug( if (authOptions?.urlHandler) {
configuration, authOptions.urlHandler(url);
'par request url: ', } else {
url this.redirectService.redirectTo(url);
); }
})
if (!url) {
this.loggerService.logError(
configuration,
`Could not create URL with param ${response.requestUri}: '${url}'`
);
return;
}
if (authOptions?.urlHandler) {
authOptions.urlHandler(url);
} else {
this.redirectService.redirectTo(url);
}
};
}),
shareReplay(1)
); );
result$.subscribe();
return result$;
} }
loginWithPopUpPar( loginWithPopUpPar(

View File

@ -1,6 +1,6 @@
import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { TestBed, mockImplementationWhenArgsEqual } from '@/testing';
import { HttpHeaders } from '@ngify/http'; import { HttpHeaders } from '@ngify/http';
import { lastValueFrom, of, throwError } from 'rxjs'; import { firstValueFrom, of, throwError } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { DataService } from '../../api/data.service'; import { DataService } from '../../api/data.service';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
@ -20,6 +20,7 @@ describe('ParService', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
ParService,
mockProvider(LoggerService), mockProvider(LoggerService),
mockProvider(UrlService), mockProvider(UrlService),
mockProvider(DataService), mockProvider(DataService),
@ -48,7 +49,7 @@ describe('ParService', () => {
() => null () => null
); );
try { try {
await lastValueFrom(service.postParRequest({ configId: 'configId1' })); await firstValueFrom(service.postParRequest({ configId: 'configId1' }));
} catch (err: any) { } catch (err: any) {
expect(err.message).toBe( expect(err.message).toBe(
'Could not read PAR endpoint because authWellKnownEndPoints are not given' 'Could not read PAR endpoint because authWellKnownEndPoints are not given'
@ -66,7 +67,7 @@ describe('ParService', () => {
() => ({ some: 'thing' }) () => ({ some: 'thing' })
); );
try { try {
await lastValueFrom(service.postParRequest({ configId: 'configId1' })); await firstValueFrom(service.postParRequest({ configId: 'configId1' }));
} catch (err: any) { } catch (err: any) {
expect(err.message).toBe( expect(err.message).toBe(
'Could not read PAR endpoint from authWellKnownEndpoints' 'Could not read PAR endpoint from authWellKnownEndpoints'
@ -88,7 +89,7 @@ describe('ParService', () => {
.spyOn(dataService, 'post') .spyOn(dataService, 'post')
.mockReturnValue(of({})); .mockReturnValue(of({}));
await lastValueFrom(service.postParRequest({ configId: 'configId1' })); await firstValueFrom(service.postParRequest({ configId: 'configId1' }));
expect(dataServiceSpy).toHaveBeenCalledExactlyOnceWith( expect(dataServiceSpy).toHaveBeenCalledExactlyOnceWith(
'parEndpoint', 'parEndpoint',
'some-url123', 'some-url123',
@ -109,7 +110,7 @@ describe('ParService', () => {
vi.spyOn(dataService, 'post').mockReturnValue( vi.spyOn(dataService, 'post').mockReturnValue(
of({ expires_in: 123, request_uri: 'request_uri' }) of({ expires_in: 123, request_uri: 'request_uri' })
); );
const result = await lastValueFrom( const result = await firstValueFrom(
service.postParRequest({ configId: 'configId1' }) service.postParRequest({ configId: 'configId1' })
); );
expect(result).toEqual({ expiresIn: 123, requestUri: 'request_uri' }); expect(result).toEqual({ expiresIn: 123, requestUri: 'request_uri' });
@ -130,7 +131,7 @@ describe('ParService', () => {
const loggerSpy = vi.spyOn(loggerService, 'logError'); const loggerSpy = vi.spyOn(loggerService, 'logError');
try { try {
await lastValueFrom(service.postParRequest({ configId: 'configId1' })); await firstValueFrom(service.postParRequest({ configId: 'configId1' }));
} catch (err: any) { } catch (err: any) {
expect(err.message).toBe( expect(err.message).toBe(
'There was an error on ParService postParRequest' 'There was an error on ParService postParRequest'
@ -159,7 +160,7 @@ describe('ParService', () => {
) )
); );
const res = await lastValueFrom( const res = await firstValueFrom(
service.postParRequest({ configId: 'configId1' }) service.postParRequest({ configId: 'configId1' })
); );
expect(res).toBeTruthy(); expect(res).toBeTruthy();
@ -183,7 +184,7 @@ describe('ParService', () => {
) )
); );
const res = await lastValueFrom( const res = await firstValueFrom(
service.postParRequest({ configId: 'configId1' }) service.postParRequest({ configId: 'configId1' })
); );
expect(res).toBeTruthy(); expect(res).toBeTruthy();
@ -209,7 +210,7 @@ describe('ParService', () => {
); );
try { try {
await lastValueFrom(service.postParRequest({ configId: 'configId1' })); await firstValueFrom(service.postParRequest({ configId: 'configId1' }));
} catch (err: any) { } catch (err: any) {
expect(err).toBeTruthy(); expect(err).toBeTruthy();
} }

View File

@ -1,5 +1,5 @@
import { TestBed, spyOnProperty } from '@/testing'; import { TestBed, spyOnProperty } from '@/testing';
import { lastValueFrom, of } from 'rxjs'; import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { CheckAuthService } from '../../auth-state/check-auth.service'; import { CheckAuthService } from '../../auth-state/check-auth.service';
import { AuthWellKnownService } from '../../config/auth-well-known/auth-well-known.service'; import { AuthWellKnownService } from '../../config/auth-well-known/auth-well-known.service';
@ -60,7 +60,7 @@ describe('PopUpLoginService', () => {
const loggerSpy = vi.spyOn(loggerService, 'logError'); const loggerSpy = vi.spyOn(loggerService, 'logError');
try { try {
await lastValueFrom( await firstValueFrom(
popUpLoginService.loginWithPopUpStandard(config, [config]) popUpLoginService.loginWithPopUpStandard(config, [config])
); );
} catch (err: any) { } catch (err: any) {
@ -91,7 +91,7 @@ describe('PopUpLoginService', () => {
of({} as LoginResponse) of({} as LoginResponse)
); );
await lastValueFrom( await firstValueFrom(
popUpLoginService.loginWithPopUpStandard(config, [config]) popUpLoginService.loginWithPopUpStandard(config, [config])
); );
expect(urlService.getAuthorizeUrl).toHaveBeenCalled(); expect(urlService.getAuthorizeUrl).toHaveBeenCalled();
@ -120,7 +120,7 @@ describe('PopUpLoginService', () => {
); );
const popupSpy = vi.spyOn(popupService, 'openPopUp'); const popupSpy = vi.spyOn(popupService, 'openPopUp');
await lastValueFrom( await firstValueFrom(
popUpLoginService.loginWithPopUpStandard(config, [config]) popUpLoginService.loginWithPopUpStandard(config, [config])
); );
expect(popupSpy).toHaveBeenCalled(); expect(popupSpy).toHaveBeenCalled();
@ -160,7 +160,7 @@ describe('PopUpLoginService', () => {
spyOnProperty(popupService, 'result$').mockReturnValue(of(popupResult)); spyOnProperty(popupService, 'result$').mockReturnValue(of(popupResult));
const result = await lastValueFrom( const result = await firstValueFrom(
popUpLoginService.loginWithPopUpStandard(config, [config]) popUpLoginService.loginWithPopUpStandard(config, [config])
); );
expect(checkAuthSpy).toHaveBeenCalledExactlyOnceWith( expect(checkAuthSpy).toHaveBeenCalledExactlyOnceWith(
@ -201,7 +201,7 @@ describe('PopUpLoginService', () => {
spyOnProperty(popupService, 'result$').mockReturnValue(of(popupResult)); spyOnProperty(popupService, 'result$').mockReturnValue(of(popupResult));
const result = await lastValueFrom( const result = await firstValueFrom(
popUpLoginService.loginWithPopUpStandard(config, [config]) popUpLoginService.loginWithPopUpStandard(config, [config])
); );
expect(checkAuthSpy).not.toHaveBeenCalled(); expect(checkAuthSpy).not.toHaveBeenCalled();

View File

@ -1,5 +1,5 @@
import { TestBed, spyOnProperty } from '@/testing'; import { TestBed, spyOnProperty } from '@/testing';
import { lastValueFrom } from 'rxjs'; import { ReplaySubject, firstValueFrom, map, share } from 'rxjs';
import { type MockInstance, vi } from 'vitest'; import { type MockInstance, vi } from 'vitest';
import type { OpenIdConfiguration } from '../../config/openid-configuration'; import type { OpenIdConfiguration } from '../../config/openid-configuration';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
@ -18,6 +18,7 @@ describe('PopUpService', () => {
providers: [ providers: [
mockProvider(StoragePersistenceService), mockProvider(StoragePersistenceService),
mockProvider(LoggerService), mockProvider(LoggerService),
PopUpService,
], ],
}); });
storagePersistenceService = TestBed.inject(StoragePersistenceService); storagePersistenceService = TestBed.inject(StoragePersistenceService);
@ -53,7 +54,11 @@ describe('PopUpService', () => {
vi.spyOn(popUpService as any, 'canAccessSessionStorage').mockReturnValue( vi.spyOn(popUpService as any, 'canAccessSessionStorage').mockReturnValue(
false false
); );
spyOnProperty(popUpService as any, 'windowInternal').mockReturnValue({ spyOnProperty(
popUpService as any,
'windowInternal',
'get'
).mockReturnValue({
opener: {} as Window, opener: {} as Window,
}); });
vi.spyOn(storagePersistenceService, 'read').mockReturnValue({ vi.spyOn(storagePersistenceService, 'read').mockReturnValue({
@ -113,10 +118,23 @@ describe('PopUpService', () => {
receivedUrl: 'some-url1111', receivedUrl: 'some-url1111',
}; };
const result = await lastValueFrom(popUpService.result$); const test$ = popUpService.result$.pipe(
expect(result).toBe(popupResult); map((result) => {
expect(result).toBe(popupResult);
}),
share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: true,
})
);
test$.subscribe();
(popUpService as any).resultInternal$.next(popupResult); (popUpService as any).resultInternal$.next(popupResult);
await firstValueFrom(test$);
}); });
}); });
@ -183,7 +201,8 @@ describe('PopUpService', () => {
let popupResult: PopupResult; let popupResult: PopupResult;
let cleanUpSpy: MockInstance; let cleanUpSpy: MockInstance;
beforeEach(async () => { beforeEach(() => {
vi.useFakeTimers();
popup = { popup = {
closed: false, closed: false,
close: () => undefined, close: () => undefined,
@ -195,8 +214,14 @@ describe('PopUpService', () => {
popupResult = {} as PopupResult; popupResult = {} as PopupResult;
const result = await lastValueFrom(popUpService.result$); popUpService.result$.subscribe((result) => {
popupResult = result; popupResult = result;
});
});
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => {
vi.useRealTimers();
}); });
it('message received with data', async () => { it('message received with data', async () => {
@ -273,9 +298,18 @@ describe('PopUpService', () => {
}); });
describe('sendMessageToMainWindow', () => { describe('sendMessageToMainWindow', () => {
beforeEach(() => {
vi.useFakeTimers({});
});
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => {
vi.useRealTimers();
});
it('does nothing if window.opener is null', async () => { it('does nothing if window.opener is null', async () => {
// arrange // arrange
spyOnProperty(window, 'opener').mockReturnValue(null); spyOnProperty(window, 'opener', 'get', () => null);
const sendMessageSpy = vi.spyOn(popUpService as any, 'sendMessage'); const sendMessageSpy = vi.spyOn(popUpService as any, 'sendMessage');

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { lastValueFrom, of } from 'rxjs'; import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { AuthWellKnownService } from '../../config/auth-well-known/auth-well-known.service'; import { AuthWellKnownService } from '../../config/auth-well-known/auth-well-known.service';
import { FlowsDataService } from '../../flows/flows-data.service'; import { FlowsDataService } from '../../flows/flows-data.service';
@ -20,6 +20,7 @@ describe('StandardLoginService', () => {
let flowsDataService: FlowsDataService; let flowsDataService: FlowsDataService;
beforeEach(() => { beforeEach(() => {
vi.useFakeTimers();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [],
providers: [ providers: [
@ -44,6 +45,11 @@ describe('StandardLoginService', () => {
flowsDataService = TestBed.inject(FlowsDataService); flowsDataService = TestBed.inject(FlowsDataService);
}); });
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => {
vi.useRealTimers();
});
it('should create', () => { it('should create', () => {
expect(standardLoginService).toBeTruthy(); expect(standardLoginService).toBeTruthy();
}); });
@ -56,7 +62,7 @@ describe('StandardLoginService', () => {
).mockReturnValue(false); ).mockReturnValue(false);
const loggerSpy = vi.spyOn(loggerService, 'logError'); const loggerSpy = vi.spyOn(loggerService, 'logError');
const result = await lastValueFrom( const result = await firstValueFrom(
standardLoginService.loginStandard({ standardLoginService.loginStandard({
configId: 'configId1', configId: 'configId1',
}) })
@ -83,7 +89,7 @@ describe('StandardLoginService', () => {
vi.spyOn(urlService, 'getAuthorizeUrl').mockReturnValue(of('someUrl')); vi.spyOn(urlService, 'getAuthorizeUrl').mockReturnValue(of('someUrl'));
const flowsDataSpy = vi.spyOn(flowsDataService, 'setCodeFlowInProgress'); const flowsDataSpy = vi.spyOn(flowsDataService, 'setCodeFlowInProgress');
const result = await lastValueFrom( const result = await firstValueFrom(
standardLoginService.loginStandard(config) standardLoginService.loginStandard(config)
); );
@ -107,7 +113,7 @@ describe('StandardLoginService', () => {
).mockReturnValue(of({})); ).mockReturnValue(of({}));
vi.spyOn(urlService, 'getAuthorizeUrl').mockReturnValue(of('someUrl')); vi.spyOn(urlService, 'getAuthorizeUrl').mockReturnValue(of('someUrl'));
const result = await lastValueFrom( const result = await firstValueFrom(
standardLoginService.loginStandard(config) standardLoginService.loginStandard(config)
); );
@ -131,7 +137,7 @@ describe('StandardLoginService', () => {
vi.spyOn(urlService, 'getAuthorizeUrl').mockReturnValue(of('someUrl')); vi.spyOn(urlService, 'getAuthorizeUrl').mockReturnValue(of('someUrl'));
const redirectSpy = vi.spyOn(redirectService, 'redirectTo'); const redirectSpy = vi.spyOn(redirectService, 'redirectTo');
standardLoginService.loginStandard(config); await firstValueFrom(standardLoginService.loginStandard(config));
await vi.advanceTimersByTimeAsync(0); await vi.advanceTimersByTimeAsync(0);
expect(redirectSpy).toHaveBeenCalledExactlyOnceWith('someUrl'); expect(redirectSpy).toHaveBeenCalledExactlyOnceWith('someUrl');
}); });
@ -159,7 +165,9 @@ describe('StandardLoginService', () => {
spy(url); spy(url);
}; };
standardLoginService.loginStandard(config, { urlHandler }); await firstValueFrom(
standardLoginService.loginStandard(config, { urlHandler })
);
await vi.advanceTimersByTimeAsync(0); await vi.advanceTimersByTimeAsync(0);
expect(spy).toHaveBeenCalledExactlyOnceWith('someUrl'); expect(spy).toHaveBeenCalledExactlyOnceWith('someUrl');
expect(redirectSpy).not.toHaveBeenCalled(); expect(redirectSpy).not.toHaveBeenCalled();
@ -185,7 +193,7 @@ describe('StandardLoginService', () => {
'resetSilentRenewRunning' 'resetSilentRenewRunning'
); );
standardLoginService.loginStandard(config, {}); await firstValueFrom(standardLoginService.loginStandard(config, {}));
await vi.advanceTimersByTimeAsync(0); await vi.advanceTimersByTimeAsync(0);
expect(flowsDataSpy).toHaveBeenCalled(); expect(flowsDataSpy).toHaveBeenCalled();
@ -212,9 +220,11 @@ describe('StandardLoginService', () => {
.spyOn(redirectService, 'redirectTo') .spyOn(redirectService, 'redirectTo')
.mockImplementation(() => undefined); .mockImplementation(() => undefined);
standardLoginService.loginStandard(config, { await firstValueFrom(
customParams: { to: 'add', as: 'well' }, standardLoginService.loginStandard(config, {
}); customParams: { to: 'add', as: 'well' },
})
);
await vi.advanceTimersByTimeAsync(0); await vi.advanceTimersByTimeAsync(0);
expect(redirectSpy).toHaveBeenCalledExactlyOnceWith('someUrl'); expect(redirectSpy).toHaveBeenCalledExactlyOnceWith('someUrl');
expect(getAuthorizeUrlSpy).toHaveBeenCalledExactlyOnceWith(config, { expect(getAuthorizeUrlSpy).toHaveBeenCalledExactlyOnceWith(config, {
@ -243,7 +253,7 @@ describe('StandardLoginService', () => {
.spyOn(redirectService, 'redirectTo') .spyOn(redirectService, 'redirectTo')
.mockImplementation(() => undefined); .mockImplementation(() => undefined);
standardLoginService.loginStandard(config); await firstValueFrom(standardLoginService.loginStandard(config));
await vi.advanceTimersByTimeAsync(0); await vi.advanceTimersByTimeAsync(0);
expect(loggerSpy).toHaveBeenCalledExactlyOnceWith( expect(loggerSpy).toHaveBeenCalledExactlyOnceWith(
config, config,

View File

@ -1,5 +1,5 @@
import { Injectable, inject } from 'injection-js'; import { Injectable, inject } from 'injection-js';
import { type Observable, map, shareReplay, switchMap } from 'rxjs'; import { type Observable, map, of, switchMap } from 'rxjs';
import type { AuthOptions } from '../../auth-options'; import type { AuthOptions } from '../../auth-options';
import { AuthWellKnownService } from '../../config/auth-well-known/auth-well-known.service'; import { AuthWellKnownService } from '../../config/auth-well-known/auth-well-known.service';
import type { OpenIdConfiguration } from '../../config/openid-configuration'; import type { OpenIdConfiguration } from '../../config/openid-configuration';
@ -28,7 +28,7 @@ export class StandardLoginService {
loginStandard( loginStandard(
configuration: OpenIdConfiguration, configuration: OpenIdConfiguration,
authOptions?: AuthOptions authOptions?: AuthOptions
): Observable<void> { ): Observable<undefined> {
if ( if (
!this.responseTypeValidationService.hasConfigValidResponseType( !this.responseTypeValidationService.hasConfigValidResponseType(
configuration configuration
@ -36,7 +36,7 @@ export class StandardLoginService {
) { ) {
this.loggerService.logError(configuration, 'Invalid response type!'); this.loggerService.logError(configuration, 'Invalid response type!');
return; return of(undefined);
} }
this.loggerService.logDebug( this.loggerService.logDebug(
@ -45,7 +45,7 @@ export class StandardLoginService {
); );
this.flowsDataService.setCodeFlowInProgress(configuration); this.flowsDataService.setCodeFlowInProgress(configuration);
const result$ = this.authWellKnownService return this.authWellKnownService
.queryAndStoreAuthWellKnownEndPoints(configuration) .queryAndStoreAuthWellKnownEndPoints(configuration)
.pipe( .pipe(
switchMap(() => { switchMap(() => {
@ -70,12 +70,8 @@ export class StandardLoginService {
} else { } else {
this.redirectService.redirectTo(url); this.redirectService.redirectTo(url);
} }
}), return undefined;
shareReplay(1) })
); );
result$.subscribe();
return result$;
} }
} }

View File

@ -1,6 +1,6 @@
import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { TestBed, mockImplementationWhenArgsEqual } from '@/testing';
import type { HttpHeaders } from '@ngify/http'; import type { HttpHeaders } from '@ngify/http';
import { Observable, lastValueFrom, of, throwError } from 'rxjs'; import { Observable, firstValueFrom, of, throwError } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { DataService } from '../api/data.service'; import { DataService } from '../api/data.service';
import { ResetAuthDataService } from '../flows/reset-auth-data.service'; import { ResetAuthDataService } from '../flows/reset-auth-data.service';
@ -26,6 +26,7 @@ describe('Logout and Revoke Service', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
LogoffRevocationService,
mockProvider(DataService), mockProvider(DataService),
mockProvider(LoggerService), mockProvider(LoggerService),
mockProvider(StoragePersistenceService), mockProvider(StoragePersistenceService),
@ -120,7 +121,7 @@ describe('Logout and Revoke Service', () => {
const config = { configId: 'configId1' }; const config = { configId: 'configId1' };
// Act // Act
const result = await lastValueFrom(service.revokeAccessToken(config)); const result = await firstValueFrom(service.revokeAccessToken(config));
expect(result).toEqual({ data: 'anything' }); expect(result).toEqual({ data: 'anything' });
expect(loggerSpy).toHaveBeenCalled(); expect(loggerSpy).toHaveBeenCalled();
}); });
@ -142,7 +143,7 @@ describe('Logout and Revoke Service', () => {
// Act // Act
try { try {
await lastValueFrom(service.revokeAccessToken(config)); await firstValueFrom(service.revokeAccessToken(config));
} catch (err: any) { } catch (err: any) {
expect(loggerSpy).toHaveBeenCalled(); expect(loggerSpy).toHaveBeenCalled();
expect(err).toBeTruthy(); expect(err).toBeTruthy();
@ -167,7 +168,7 @@ describe('Logout and Revoke Service', () => {
) )
); );
const res = await lastValueFrom(service.revokeAccessToken(config)); const res = await firstValueFrom(service.revokeAccessToken(config));
expect(res).toBeTruthy(); expect(res).toBeTruthy();
expect(res).toEqual({ data: 'anything' }); expect(res).toEqual({ data: 'anything' });
expect(loggerSpy).toHaveBeenCalled(); expect(loggerSpy).toHaveBeenCalled();
@ -192,7 +193,7 @@ describe('Logout and Revoke Service', () => {
) )
); );
const res = await lastValueFrom(service.revokeAccessToken(config)); const res = await firstValueFrom(service.revokeAccessToken(config));
expect(res).toBeTruthy(); expect(res).toBeTruthy();
expect(res).toEqual({ data: 'anything' }); expect(res).toEqual({ data: 'anything' });
expect(loggerSpy).toHaveBeenCalled(); expect(loggerSpy).toHaveBeenCalled();
@ -219,7 +220,7 @@ describe('Logout and Revoke Service', () => {
); );
try { try {
await lastValueFrom(service.revokeAccessToken(config)); await firstValueFrom(service.revokeAccessToken(config));
} catch (err: any) { } catch (err: any) {
expect(err).toBeTruthy(); expect(err).toBeTruthy();
expect(loggerSpy).toHaveBeenCalled(); expect(loggerSpy).toHaveBeenCalled();
@ -297,7 +298,7 @@ describe('Logout and Revoke Service', () => {
const config = { configId: 'configId1' }; const config = { configId: 'configId1' };
// Act // Act
const result = await lastValueFrom(service.revokeRefreshToken(config)); const result = await firstValueFrom(service.revokeRefreshToken(config));
expect(result).toEqual({ data: 'anything' }); expect(result).toEqual({ data: 'anything' });
expect(loggerSpy).toHaveBeenCalled(); expect(loggerSpy).toHaveBeenCalled();
}); });
@ -319,7 +320,7 @@ describe('Logout and Revoke Service', () => {
// Act // Act
try { try {
await lastValueFrom(service.revokeRefreshToken(config)); await firstValueFrom(service.revokeRefreshToken(config));
} catch (err: any) { } catch (err: any) {
expect(loggerSpy).toHaveBeenCalled(); expect(loggerSpy).toHaveBeenCalled();
expect(err).toBeTruthy(); expect(err).toBeTruthy();
@ -344,7 +345,7 @@ describe('Logout and Revoke Service', () => {
) )
); );
const res = await lastValueFrom(service.revokeRefreshToken(config)); const res = await firstValueFrom(service.revokeRefreshToken(config));
expect(res).toBeTruthy(); expect(res).toBeTruthy();
expect(res).toEqual({ data: 'anything' }); expect(res).toEqual({ data: 'anything' });
expect(loggerSpy).toHaveBeenCalled(); expect(loggerSpy).toHaveBeenCalled();
@ -369,7 +370,7 @@ describe('Logout and Revoke Service', () => {
) )
); );
const res = await lastValueFrom(service.revokeRefreshToken(config)); const res = await firstValueFrom(service.revokeRefreshToken(config));
expect(res).toBeTruthy(); expect(res).toBeTruthy();
expect(res).toEqual({ data: 'anything' }); expect(res).toEqual({ data: 'anything' });
expect(loggerSpy).toHaveBeenCalled(); expect(loggerSpy).toHaveBeenCalled();
@ -396,7 +397,7 @@ describe('Logout and Revoke Service', () => {
); );
try { try {
await lastValueFrom(service.revokeRefreshToken(config)); await firstValueFrom(service.revokeRefreshToken(config));
} catch (err: any) { } catch (err: any) {
expect(err).toBeTruthy(); expect(err).toBeTruthy();
expect(loggerSpy).toHaveBeenCalled(); expect(loggerSpy).toHaveBeenCalled();
@ -419,7 +420,7 @@ describe('Logout and Revoke Service', () => {
const result$ = service.logoff(config, [config]); const result$ = service.logoff(config, [config]);
// Assert // Assert
await lastValueFrom(result$); await firstValueFrom(result$);
expect(serverStateChangedSpy).not.toHaveBeenCalled(); expect(serverStateChangedSpy).not.toHaveBeenCalled();
}); });
@ -435,7 +436,7 @@ describe('Logout and Revoke Service', () => {
const result$ = service.logoff(config, [config]); const result$ = service.logoff(config, [config]);
// Assert // Assert
await lastValueFrom(result$); await firstValueFrom(result$);
expect(redirectSpy).not.toHaveBeenCalled(); expect(redirectSpy).not.toHaveBeenCalled();
}); });
@ -461,7 +462,7 @@ describe('Logout and Revoke Service', () => {
const result$ = service.logoff(config, [config], { urlHandler }); const result$ = service.logoff(config, [config], { urlHandler });
// Assert // Assert
await lastValueFrom(result$); await firstValueFrom(result$);
expect(redirectSpy).not.toHaveBeenCalled(); expect(redirectSpy).not.toHaveBeenCalled();
expect(spy).toHaveBeenCalledExactlyOnceWith('someValue'); expect(spy).toHaveBeenCalledExactlyOnceWith('someValue');
expect(resetAuthorizationDataSpy).toHaveBeenCalled(); expect(resetAuthorizationDataSpy).toHaveBeenCalled();
@ -482,7 +483,7 @@ describe('Logout and Revoke Service', () => {
const result$ = service.logoff(config, [config]); const result$ = service.logoff(config, [config]);
// Assert // Assert
await lastValueFrom(result$); await firstValueFrom(result$);
expect(redirectSpy).toHaveBeenCalledExactlyOnceWith('someValue'); expect(redirectSpy).toHaveBeenCalledExactlyOnceWith('someValue');
}); });
@ -501,7 +502,7 @@ describe('Logout and Revoke Service', () => {
const result$ = service.logoff(config, [config], { logoffMethod: 'GET' }); const result$ = service.logoff(config, [config], { logoffMethod: 'GET' });
// Assert // Assert
await lastValueFrom(result$); await firstValueFrom(result$);
expect(redirectSpy).toHaveBeenCalledExactlyOnceWith('someValue'); expect(redirectSpy).toHaveBeenCalledExactlyOnceWith('someValue');
}); });
@ -533,7 +534,7 @@ describe('Logout and Revoke Service', () => {
}); });
// Assert // Assert
await lastValueFrom(result$); await firstValueFrom(result$);
expect(redirectSpy).not.toHaveBeenCalled(); expect(redirectSpy).not.toHaveBeenCalled();
expect(postSpy).toHaveBeenCalledExactlyOnceWith( expect(postSpy).toHaveBeenCalledExactlyOnceWith(
'some-url', 'some-url',
@ -585,7 +586,7 @@ describe('Logout and Revoke Service', () => {
}); });
// Assert // Assert
await lastValueFrom(result$); await firstValueFrom(result$);
expect(redirectSpy).not.toHaveBeenCalled(); expect(redirectSpy).not.toHaveBeenCalled();
expect(postSpy).toHaveBeenCalledExactlyOnceWith( expect(postSpy).toHaveBeenCalledExactlyOnceWith(
'some-url', 'some-url',
@ -647,7 +648,7 @@ describe('Logout and Revoke Service', () => {
.mockReturnValue(of({ any: 'thing' })); .mockReturnValue(of({ any: 'thing' }));
// Act // Act
await lastValueFrom(service.logoffAndRevokeTokens(config, [config])); await firstValueFrom(service.logoffAndRevokeTokens(config, [config]));
expect(revokeRefreshTokenSpy).toHaveBeenCalled(); expect(revokeRefreshTokenSpy).toHaveBeenCalled();
expect(revokeAccessTokenSpy).toHaveBeenCalled(); expect(revokeAccessTokenSpy).toHaveBeenCalled();
}); });
@ -676,7 +677,7 @@ describe('Logout and Revoke Service', () => {
// Act // Act
try { try {
await lastValueFrom(service.logoffAndRevokeTokens(config, [config])); await firstValueFrom(service.logoffAndRevokeTokens(config, [config]));
} catch (err: any) { } catch (err: any) {
expect(loggerSpy).toHaveBeenCalled(); expect(loggerSpy).toHaveBeenCalled();
expect(err).toBeTruthy(); expect(err).toBeTruthy();
@ -700,7 +701,7 @@ describe('Logout and Revoke Service', () => {
const config = { configId: 'configId1' }; const config = { configId: 'configId1' };
// Act // Act
await lastValueFrom(service.logoffAndRevokeTokens(config, [config])); await firstValueFrom(service.logoffAndRevokeTokens(config, [config]));
expect(logoffSpy).toHaveBeenCalled(); expect(logoffSpy).toHaveBeenCalled();
}); });
@ -722,7 +723,7 @@ describe('Logout and Revoke Service', () => {
const config = { configId: 'configId1' }; const config = { configId: 'configId1' };
// Act // Act
await lastValueFrom( await firstValueFrom(
service.logoffAndRevokeTokens(config, [config], { urlHandler }) service.logoffAndRevokeTokens(config, [config], { urlHandler })
); );
expect(logoffSpy).toHaveBeenCalledExactlyOnceWith(config, [config], { expect(logoffSpy).toHaveBeenCalledExactlyOnceWith(config, [config], {
@ -749,7 +750,7 @@ describe('Logout and Revoke Service', () => {
.mockReturnValue(of({ any: 'thing' })); .mockReturnValue(of({ any: 'thing' }));
// Act // Act
await lastValueFrom(service.logoffAndRevokeTokens(config, [config])); await firstValueFrom(service.logoffAndRevokeTokens(config, [config]));
expect(revokeRefreshTokenSpy).not.toHaveBeenCalled(); expect(revokeRefreshTokenSpy).not.toHaveBeenCalled();
expect(revokeAccessTokenSpy).toHaveBeenCalled(); expect(revokeAccessTokenSpy).toHaveBeenCalled();
}); });
@ -774,7 +775,7 @@ describe('Logout and Revoke Service', () => {
// Act // Act
try { try {
await lastValueFrom(service.logoffAndRevokeTokens(config, [config])); await firstValueFrom(service.logoffAndRevokeTokens(config, [config]));
} catch (err: any) { } catch (err: any) {
expect(loggerSpy).toHaveBeenCalled(); expect(loggerSpy).toHaveBeenCalled();
expect(err).toBeTruthy(); expect(err).toBeTruthy();
@ -798,7 +799,7 @@ describe('Logout and Revoke Service', () => {
// Assert // Assert
expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(2); expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(2);
expect(checkSessionServiceSpy).toHaveBeenCalledTimes(2); expect(checkSessionServiceSpy).toHaveBeenCalledTimes(2);
expect(resetAuthorizationDataSpy).toBeCalledWith([ expect(resetAuthorizationDataSpy.mock.calls).toEqual([
[allConfigs[0]!, allConfigs], [allConfigs[0]!, allConfigs],
[allConfigs[1], allConfigs], [allConfigs[1], allConfigs],
]); ]);

View File

@ -1,5 +1,5 @@
import { TestBed, spyOnProperty } from '@/testing'; import { TestBed, spyOnProperty } from '@/testing';
import { Observable, lastValueFrom, of } from 'rxjs'; import { Observable, firstValueFrom, of } from 'rxjs';
import { type MockInstance, vi } from 'vitest'; import { type MockInstance, vi } from 'vitest';
import { AuthStateService } from './auth-state/auth-state.service'; import { AuthStateService } from './auth-state/auth-state.service';
import { CheckAuthService } from './auth-state/check-auth.service'; import { CheckAuthService } from './auth-state/check-auth.service';
@ -89,36 +89,38 @@ describe('OidcSecurityService', () => {
expect(oidcSecurityService).toBeTruthy(); expect(oidcSecurityService).toBeTruthy();
}); });
describe('userData$', () => { // without signal
it('calls userService.userData$', async () => { // describe('userData$', () => {
await lastValueFrom(oidcSecurityService.userData$); // it('calls userService.userData$', async () => {
// 1x from this subscribe // await firstValueFrom(oidcSecurityService.userData());
// 1x by the signal property // // 1x from this subscribe
expect(userDataSpy).toHaveBeenCalledTimes(2); // // 1x by the signal property
}); // expect(userDataSpy).toHaveBeenCalledTimes(2);
}); // });
// });
describe('userData', () => { describe('userData', () => {
it('calls userService.userData$', async () => { it('calls userService.userData$', async () => {
const _userdata = await lastValueFrom(oidcSecurityService.userData()); const _userdata = await firstValueFrom(oidcSecurityService.userData$);
expect(userDataSpy).toHaveBeenCalledTimes(1); expect(userDataSpy).toHaveBeenCalledTimes(1);
}); });
}); });
describe('isAuthenticated$', () => { // describe('isAuthenticated$', () => {
it('calls authStateService.isAuthenticated$', async () => { // it('calls authStateService.isAuthenticated$', async () => {
await lastValueFrom(oidcSecurityService.isAuthenticated$); // await firstValueFrom(oidcSecurityService.isAuthenticated());
// 1x from this subscribe // // 1x from this subscribe
// 1x by the signal property // // 1x by the signal property
expect(authenticatedSpy).toHaveBeenCalledTimes(2); // expect(authenticatedSpy).toHaveBeenCalledTimes(2);
}); // });
}); // });
// without signal
describe('authenticated', () => { describe('authenticated', () => {
it('calls authStateService.isAuthenticated$', async () => { it('calls authStateService.isAuthenticated$', async () => {
const _authenticated = await lastValueFrom( const _authenticated = await firstValueFrom(
oidcSecurityService.authenticated() oidcSecurityService.isAuthenticated$
); );
expect(authenticatedSpy).toHaveBeenCalledTimes(1); expect(authenticatedSpy).toHaveBeenCalledTimes(1);
@ -131,19 +133,20 @@ describe('OidcSecurityService', () => {
checkSessionService, checkSessionService,
'checkSessionChanged$' 'checkSessionChanged$'
).mockReturnValue(of(true)); ).mockReturnValue(of(true));
await lastValueFrom(oidcSecurityService.checkSessionChanged$); await firstValueFrom(oidcSecurityService.checkSessionChanged$);
expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(1);
}); });
}); });
describe('stsCallback$', () => { describe('stsCallback$', () => {
it('calls callbackService.stsCallback$', async () => { it('calls callbackService.stsCallback$', () => {
const spy = spyOnProperty( const spy = spyOnProperty(
callbackService, callbackService,
'stsCallback$' 'stsCallback$'
).mockReturnValue(of()); ).mockReturnValue(of());
await lastValueFrom(oidcSecurityService.stsCallback$); oidcSecurityService.stsCallback$.subscribe();
expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(1);
}); });
}); });
@ -159,7 +162,7 @@ describe('OidcSecurityService', () => {
.spyOn(authWellKnownService, 'queryAndStoreAuthWellKnownEndPoints') .spyOn(authWellKnownService, 'queryAndStoreAuthWellKnownEndPoints')
.mockReturnValue(of({})); .mockReturnValue(of({}));
await lastValueFrom(oidcSecurityService.preloadAuthWellKnownDocument()); await firstValueFrom(oidcSecurityService.preloadAuthWellKnownDocument());
expect(spy).toHaveBeenCalledExactlyOnceWith(config); expect(spy).toHaveBeenCalledExactlyOnceWith(config);
}); });
}); });
@ -210,7 +213,7 @@ describe('OidcSecurityService', () => {
some: 'thing', some: 'thing',
}); });
await lastValueFrom(oidcSecurityService.getUserData('configId')); await firstValueFrom(oidcSecurityService.getUserData('configId'));
expect(spy).toHaveBeenCalledExactlyOnceWith(config); expect(spy).toHaveBeenCalledExactlyOnceWith(config);
}); });
@ -225,8 +228,10 @@ describe('OidcSecurityService', () => {
some: 'thing', some: 'thing',
}); });
const result = await lastValueFrom(oidcSecurityService.getUserData('configId')); const result = await firstValueFrom(
expect(result).toEqual({ some: 'thing' }); oidcSecurityService.getUserData('configId')
);
expect(result).toEqual({ some: 'thing' });
}); });
}); });
@ -242,7 +247,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(checkAuthService, 'checkAuth') .spyOn(checkAuthService, 'checkAuth')
.mockReturnValue(of({} as LoginResponse)); .mockReturnValue(of({} as LoginResponse));
await lastValueFrom(oidcSecurityService.checkAuth()); await firstValueFrom(oidcSecurityService.checkAuth());
expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config], undefined); expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config], undefined);
}); });
@ -257,7 +262,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(checkAuthService, 'checkAuth') .spyOn(checkAuthService, 'checkAuth')
.mockReturnValue(of({} as LoginResponse)); .mockReturnValue(of({} as LoginResponse));
await lastValueFrom(oidcSecurityService.checkAuth('some-url')); await firstValueFrom(oidcSecurityService.checkAuth('some-url'));
expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config], 'some-url'); expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config], 'some-url');
}); });
}); });
@ -274,7 +279,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(checkAuthService, 'checkAuthMultiple') .spyOn(checkAuthService, 'checkAuthMultiple')
.mockReturnValue(of([{}] as LoginResponse[])); .mockReturnValue(of([{}] as LoginResponse[]));
await lastValueFrom(oidcSecurityService.checkAuthMultiple()); await firstValueFrom(oidcSecurityService.checkAuthMultiple());
expect(spy).toHaveBeenCalledExactlyOnceWith([config], undefined); expect(spy).toHaveBeenCalledExactlyOnceWith([config], undefined);
}); });
@ -289,8 +294,8 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(checkAuthService, 'checkAuthMultiple') .spyOn(checkAuthService, 'checkAuthMultiple')
.mockReturnValue(of([{}] as LoginResponse[])); .mockReturnValue(of([{}] as LoginResponse[]));
await lastValueFrom(oidcSecurityService.checkAuthMultiple('some-url')); await firstValueFrom(oidcSecurityService.checkAuthMultiple('some-url'));
expect(spy).toHaveBeenCalledExactlyOnceWith([config], 'some-u-+rl'); expect(spy).toHaveBeenCalledExactlyOnceWith([config], 'some-url');
}); });
}); });
@ -306,7 +311,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(authStateService, 'isAuthenticated') .spyOn(authStateService, 'isAuthenticated')
.mockReturnValue(true); .mockReturnValue(true);
await lastValueFrom(oidcSecurityService.isAuthenticated()); await firstValueFrom(oidcSecurityService.isAuthenticated());
expect(spy).toHaveBeenCalledExactlyOnceWith(config); expect(spy).toHaveBeenCalledExactlyOnceWith(config);
}); });
}); });
@ -323,7 +328,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(checkAuthService, 'checkAuthIncludingServer') .spyOn(checkAuthService, 'checkAuthIncludingServer')
.mockReturnValue(of({} as LoginResponse)); .mockReturnValue(of({} as LoginResponse));
await lastValueFrom(oidcSecurityService.checkAuthIncludingServer()); await firstValueFrom(oidcSecurityService.checkAuthIncludingServer());
expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config]); expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config]);
}); });
}); });
@ -340,7 +345,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(authStateService, 'getAccessToken') .spyOn(authStateService, 'getAccessToken')
.mockReturnValue(''); .mockReturnValue('');
await lastValueFrom(oidcSecurityService.getAccessToken()); await firstValueFrom(oidcSecurityService.getAccessToken());
expect(spy).toHaveBeenCalledExactlyOnceWith(config); expect(spy).toHaveBeenCalledExactlyOnceWith(config);
}); });
}); });
@ -355,7 +360,7 @@ expect(result).toEqual({ some: 'thing' });
const spy = vi.spyOn(authStateService, 'getIdToken').mockReturnValue(''); const spy = vi.spyOn(authStateService, 'getIdToken').mockReturnValue('');
await lastValueFrom(oidcSecurityService.getIdToken()); await firstValueFrom(oidcSecurityService.getIdToken());
expect(spy).toHaveBeenCalledExactlyOnceWith(config); expect(spy).toHaveBeenCalledExactlyOnceWith(config);
}); });
}); });
@ -371,7 +376,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(authStateService, 'getRefreshToken') .spyOn(authStateService, 'getRefreshToken')
.mockReturnValue(''); .mockReturnValue('');
await lastValueFrom(oidcSecurityService.getRefreshToken()); await firstValueFrom(oidcSecurityService.getRefreshToken());
expect(spy).toHaveBeenCalledExactlyOnceWith(config); expect(spy).toHaveBeenCalledExactlyOnceWith(config);
}); });
}); });
@ -388,7 +393,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(authStateService, 'getAuthenticationResult') .spyOn(authStateService, 'getAuthenticationResult')
.mockReturnValue(null); .mockReturnValue(null);
await lastValueFrom(oidcSecurityService.getAuthenticationResult()); await firstValueFrom(oidcSecurityService.getAuthenticationResult());
expect(spy).toHaveBeenCalledExactlyOnceWith(config); expect(spy).toHaveBeenCalledExactlyOnceWith(config);
}); });
}); });
@ -405,7 +410,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(tokenHelperService, 'getPayloadFromToken') .spyOn(tokenHelperService, 'getPayloadFromToken')
.mockReturnValue(null); .mockReturnValue(null);
await lastValueFrom(oidcSecurityService.getPayloadFromIdToken()); await firstValueFrom(oidcSecurityService.getPayloadFromIdToken());
expect(spy).toHaveBeenCalledExactlyOnceWith('some-token', false, config); expect(spy).toHaveBeenCalledExactlyOnceWith('some-token', false, config);
}); });
@ -420,7 +425,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(tokenHelperService, 'getPayloadFromToken') .spyOn(tokenHelperService, 'getPayloadFromToken')
.mockReturnValue(null); .mockReturnValue(null);
await lastValueFrom(oidcSecurityService.getPayloadFromIdToken(true)); await firstValueFrom(oidcSecurityService.getPayloadFromIdToken(true));
expect(spy).toHaveBeenCalledExactlyOnceWith('some-token', true, config); expect(spy).toHaveBeenCalledExactlyOnceWith('some-token', true, config);
}); });
}); });
@ -439,7 +444,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(tokenHelperService, 'getPayloadFromToken') .spyOn(tokenHelperService, 'getPayloadFromToken')
.mockReturnValue(null); .mockReturnValue(null);
await lastValueFrom(oidcSecurityService.getPayloadFromAccessToken()); await firstValueFrom(oidcSecurityService.getPayloadFromAccessToken());
expect(spy).toHaveBeenCalledExactlyOnceWith( expect(spy).toHaveBeenCalledExactlyOnceWith(
'some-access-token', 'some-access-token',
false, false,
@ -460,7 +465,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(tokenHelperService, 'getPayloadFromToken') .spyOn(tokenHelperService, 'getPayloadFromToken')
.mockReturnValue(null); .mockReturnValue(null);
await lastValueFrom(oidcSecurityService.getPayloadFromAccessToken(true)); await firstValueFrom(oidcSecurityService.getPayloadFromAccessToken(true));
expect(spy).toHaveBeenCalledExactlyOnceWith( expect(spy).toHaveBeenCalledExactlyOnceWith(
'some-access-token', 'some-access-token',
true, true,
@ -478,7 +483,7 @@ expect(result).toEqual({ some: 'thing' });
); );
const spy = vi.spyOn(flowsDataService, 'setAuthStateControl'); const spy = vi.spyOn(flowsDataService, 'setAuthStateControl');
await lastValueFrom(oidcSecurityService.setState('anyString')); await firstValueFrom(oidcSecurityService.setState('anyString'));
expect(spy).toHaveBeenCalledExactlyOnceWith('anyString', config); expect(spy).toHaveBeenCalledExactlyOnceWith('anyString', config);
}); });
}); });
@ -492,7 +497,7 @@ expect(result).toEqual({ some: 'thing' });
); );
const spy = vi.spyOn(flowsDataService, 'getAuthStateControl'); const spy = vi.spyOn(flowsDataService, 'getAuthStateControl');
await lastValueFrom(oidcSecurityService.getState()); await firstValueFrom(oidcSecurityService.getState());
expect(spy).toHaveBeenCalledExactlyOnceWith(config); expect(spy).toHaveBeenCalledExactlyOnceWith(config);
}); });
}); });
@ -504,9 +509,11 @@ expect(result).toEqual({ some: 'thing' });
vi.spyOn(configurationService, 'getOpenIDConfiguration').mockReturnValue( vi.spyOn(configurationService, 'getOpenIDConfiguration').mockReturnValue(
of(config) of(config)
); );
const spy = vi.spyOn(loginService, 'login'); const spy = vi
.spyOn(loginService, 'login')
.mockReturnValue(of(undefined));
await lastValueFrom(oidcSecurityService.authorize()); await firstValueFrom(oidcSecurityService.authorize());
expect(spy).toHaveBeenCalledExactlyOnceWith(config, undefined); expect(spy).toHaveBeenCalledExactlyOnceWith(config, undefined);
}); });
@ -517,9 +524,11 @@ expect(result).toEqual({ some: 'thing' });
vi.spyOn(configurationService, 'getOpenIDConfiguration').mockReturnValue( vi.spyOn(configurationService, 'getOpenIDConfiguration').mockReturnValue(
of(config) of(config)
); );
const spy = vi.spyOn(loginService, 'login'); const spy = vi
.spyOn(loginService, 'login')
.mockReturnValue(of(undefined));
await lastValueFrom( await firstValueFrom(
oidcSecurityService.authorize('configId', { oidcSecurityService.authorize('configId', {
customParams: { some: 'param' }, customParams: { some: 'param' },
}) })
@ -542,7 +551,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(loginService, 'loginWithPopUp') .spyOn(loginService, 'loginWithPopUp')
.mockImplementation(() => of({} as LoginResponse)); .mockImplementation(() => of({} as LoginResponse));
await lastValueFrom(oidcSecurityService.authorizeWithPopUp()); await firstValueFrom(oidcSecurityService.authorizeWithPopUp());
expect(spy).toHaveBeenCalledExactlyOnceWith( expect(spy).toHaveBeenCalledExactlyOnceWith(
config, config,
[config], [config],
@ -564,7 +573,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(refreshSessionService, 'userForceRefreshSession') .spyOn(refreshSessionService, 'userForceRefreshSession')
.mockReturnValue(of({} as LoginResponse)); .mockReturnValue(of({} as LoginResponse));
await lastValueFrom(oidcSecurityService.forceRefreshSession()); await firstValueFrom(oidcSecurityService.forceRefreshSession());
expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config], undefined); expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config], undefined);
}); });
}); });
@ -580,7 +589,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(logoffRevocationService, 'logoffAndRevokeTokens') .spyOn(logoffRevocationService, 'logoffAndRevokeTokens')
.mockReturnValue(of(null)); .mockReturnValue(of(null));
await lastValueFrom(oidcSecurityService.logoffAndRevokeTokens()); await firstValueFrom(oidcSecurityService.logoffAndRevokeTokens());
expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config], undefined); expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config], undefined);
}); });
}); });
@ -596,7 +605,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(logoffRevocationService, 'logoff') .spyOn(logoffRevocationService, 'logoff')
.mockReturnValue(of(null)); .mockReturnValue(of(null));
await lastValueFrom(oidcSecurityService.logoff()); await firstValueFrom(oidcSecurityService.logoff());
expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config], undefined); expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config], undefined);
}); });
}); });
@ -609,7 +618,7 @@ expect(result).toEqual({ some: 'thing' });
of({ allConfigs: [config], currentConfig: config }) of({ allConfigs: [config], currentConfig: config })
); );
const spy = vi.spyOn(logoffRevocationService, 'logoffLocal'); const spy = vi.spyOn(logoffRevocationService, 'logoffLocal');
await lastValueFrom(oidcSecurityService.logoffLocal()); await firstValueFrom(oidcSecurityService.logoffLocal());
expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config]); expect(spy).toHaveBeenCalledExactlyOnceWith(config, [config]);
}); });
}); });
@ -623,7 +632,7 @@ expect(result).toEqual({ some: 'thing' });
); );
const spy = vi.spyOn(logoffRevocationService, 'logoffLocalMultiple'); const spy = vi.spyOn(logoffRevocationService, 'logoffLocalMultiple');
await lastValueFrom(oidcSecurityService.logoffLocalMultiple()); await firstValueFrom(oidcSecurityService.logoffLocalMultiple());
expect(spy).toHaveBeenCalledExactlyOnceWith([config]); expect(spy).toHaveBeenCalledExactlyOnceWith([config]);
}); });
}); });
@ -639,7 +648,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(logoffRevocationService, 'revokeAccessToken') .spyOn(logoffRevocationService, 'revokeAccessToken')
.mockReturnValue(of(null)); .mockReturnValue(of(null));
await lastValueFrom(oidcSecurityService.revokeAccessToken()); await firstValueFrom(oidcSecurityService.revokeAccessToken());
expect(spy).toHaveBeenCalledExactlyOnceWith(config, undefined); expect(spy).toHaveBeenCalledExactlyOnceWith(config, undefined);
}); });
@ -653,7 +662,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(logoffRevocationService, 'revokeAccessToken') .spyOn(logoffRevocationService, 'revokeAccessToken')
.mockReturnValue(of(null)); .mockReturnValue(of(null));
await lastValueFrom( await firstValueFrom(
oidcSecurityService.revokeAccessToken('access_token') oidcSecurityService.revokeAccessToken('access_token')
); );
expect(spy).toHaveBeenCalledExactlyOnceWith(config, 'access_token'); expect(spy).toHaveBeenCalledExactlyOnceWith(config, 'access_token');
@ -671,7 +680,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(logoffRevocationService, 'revokeRefreshToken') .spyOn(logoffRevocationService, 'revokeRefreshToken')
.mockReturnValue(of(null)); .mockReturnValue(of(null));
await lastValueFrom(oidcSecurityService.revokeRefreshToken()); await firstValueFrom(oidcSecurityService.revokeRefreshToken());
expect(spy).toHaveBeenCalledExactlyOnceWith(config, undefined); expect(spy).toHaveBeenCalledExactlyOnceWith(config, undefined);
}); });
@ -685,7 +694,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(logoffRevocationService, 'revokeRefreshToken') .spyOn(logoffRevocationService, 'revokeRefreshToken')
.mockReturnValue(of(null)); .mockReturnValue(of(null));
await lastValueFrom( await firstValueFrom(
oidcSecurityService.revokeRefreshToken('refresh_token') oidcSecurityService.revokeRefreshToken('refresh_token')
); );
expect(spy).toHaveBeenCalledExactlyOnceWith(config, 'refresh_token'); expect(spy).toHaveBeenCalledExactlyOnceWith(config, 'refresh_token');
@ -704,7 +713,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(urlService, 'getEndSessionUrl') .spyOn(urlService, 'getEndSessionUrl')
.mockReturnValue(null); .mockReturnValue(null);
await lastValueFrom(oidcSecurityService.getEndSessionUrl()); await firstValueFrom(oidcSecurityService.getEndSessionUrl());
expect(spy).toHaveBeenCalledExactlyOnceWith(config, undefined); expect(spy).toHaveBeenCalledExactlyOnceWith(config, undefined);
}); });
@ -719,7 +728,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(urlService, 'getEndSessionUrl') .spyOn(urlService, 'getEndSessionUrl')
.mockReturnValue(null); .mockReturnValue(null);
await lastValueFrom( await firstValueFrom(
oidcSecurityService.getEndSessionUrl({ custom: 'params' }) oidcSecurityService.getEndSessionUrl({ custom: 'params' })
); );
expect(spy).toHaveBeenCalledExactlyOnceWith(config, { expect(spy).toHaveBeenCalledExactlyOnceWith(config, {
@ -740,7 +749,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(urlService, 'getAuthorizeUrl') .spyOn(urlService, 'getAuthorizeUrl')
.mockReturnValue(of(null)); .mockReturnValue(of(null));
await lastValueFrom(oidcSecurityService.getAuthorizeUrl()); await firstValueFrom(oidcSecurityService.getAuthorizeUrl());
expect(spy).toHaveBeenCalledExactlyOnceWith(config, undefined); expect(spy).toHaveBeenCalledExactlyOnceWith(config, undefined);
}); });
@ -755,7 +764,7 @@ expect(result).toEqual({ some: 'thing' });
.spyOn(urlService, 'getAuthorizeUrl') .spyOn(urlService, 'getAuthorizeUrl')
.mockReturnValue(of(null)); .mockReturnValue(of(null));
await lastValueFrom( await firstValueFrom(
oidcSecurityService.getAuthorizeUrl({ custom: 'params' }) oidcSecurityService.getAuthorizeUrl({ custom: 'params' })
); );
expect(spy).toHaveBeenCalledExactlyOnceWith(config, { expect(spy).toHaveBeenCalledExactlyOnceWith(config, {

View File

@ -1,6 +1,6 @@
import { Injectable, inject } from 'injection-js'; import { Injectable, inject } from 'injection-js';
import type { Observable } from 'rxjs'; import { BehaviorSubject, type Observable } from 'rxjs';
import { concatMap, map, shareReplay } from 'rxjs/operators'; import { concatMap, map, switchMap } from 'rxjs/operators';
import type { AuthOptions, LogoutAuthOptions } from './auth-options'; import type { AuthOptions, LogoutAuthOptions } from './auth-options';
import type { AuthenticatedResult } from './auth-state/auth-result'; import type { AuthenticatedResult } from './auth-state/auth-result';
import { AuthStateService } from './auth-state/auth-state.service'; import { AuthStateService } from './auth-state/auth-state.service';
@ -20,6 +20,7 @@ import type { PopupOptions } from './login/popup/popup-options';
import { LogoffRevocationService } from './logoff-revoke/logoff-revocation.service'; import { LogoffRevocationService } from './logoff-revoke/logoff-revocation.service';
import { UserService } from './user-data/user.service'; import { UserService } from './user-data/user.service';
import type { UserDataResult } from './user-data/userdata-result'; import type { UserDataResult } from './user-data/userdata-result';
import { MockUtil } from './utils/reflect/index';
import { TokenHelperService } from './utils/tokenHelper/token-helper.service'; import { TokenHelperService } from './utils/tokenHelper/token-helper.service';
import { UrlService } from './utils/url/url.service'; import { UrlService } from './utils/url/url.service';
@ -160,6 +161,8 @@ export class OidcSecurityService {
* *
* @returns An array of `LoginResponse` objects containing all information about the logins * @returns An array of `LoginResponse` objects containing all information about the logins
*/ */
@MockUtil({ implementation: () => new BehaviorSubject(undefined) })
checkAuthMultiple(url?: string): Observable<LoginResponse[]> { checkAuthMultiple(url?: string): Observable<LoginResponse[]> {
return this.configurationService return this.configurationService
.getOpenIDConfigurations() .getOpenIDConfigurations()
@ -336,16 +339,11 @@ export class OidcSecurityService {
* @param authOptions The custom options for the the authentication request. * @param authOptions The custom options for the the authentication request.
*/ */
authorize(configId?: string, authOptions?: AuthOptions): Observable<void> { authorize(configId?: string, authOptions?: AuthOptions): Observable<void> {
const result$ = this.configurationService return this.configurationService
.getOpenIDConfiguration(configId) .getOpenIDConfiguration(configId)
.pipe( .pipe(
map((config) => this.loginService.login(config, authOptions)), switchMap((config) => this.loginService.login(config, authOptions))
shareReplay(1)
); );
result$.subscribe();
return result$;
} }
/** /**
@ -458,17 +456,13 @@ export class OidcSecurityService {
* *
* @param configId The configId to perform the action in behalf of. If not passed, the first configs will be taken * @param configId The configId to perform the action in behalf of. If not passed, the first configs will be taken
*/ */
logoffLocal(configId?: string): Observable<void> { logoffLocal(configId?: string): Observable<undefined> {
const result$ = this.configurationService return this.configurationService.getOpenIDConfigurations(configId).pipe(
.getOpenIDConfigurations(configId) map(({ allConfigs, currentConfig }) => {
.pipe( this.logoffRevocationService.logoffLocal(currentConfig, allConfigs);
map(({ allConfigs, currentConfig }) => return undefined;
this.logoffRevocationService.logoffLocal(currentConfig, allConfigs) })
), );
shareReplay(1)
);
result$.subscribe();
return result$;
} }
/** /**
@ -476,16 +470,13 @@ export class OidcSecurityService {
* Use this method if you have _multiple_ configs enabled. * Use this method if you have _multiple_ configs enabled.
*/ */
logoffLocalMultiple(): Observable<void> { logoffLocalMultiple(): Observable<void> {
const result$ = this.configurationService.getOpenIDConfigurations().pipe( return this.configurationService
map(({ allConfigs }) => .getOpenIDConfigurations()
this.logoffRevocationService.logoffLocalMultiple(allConfigs) .pipe(
), map(({ allConfigs }) =>
shareReplay(1) this.logoffRevocationService.logoffLocalMultiple(allConfigs)
); )
);
result$.subscribe();
return result$;
} }
/** /**

View File

@ -1,7 +1,8 @@
import { TestBed, createSpyObj } from '@/testing'; import { TestBed } from '@/testing';
import { mockProvider } from '@/testing/mock'; import { mockClass, mockProvider } from '@/testing/mock';
import { APP_INITIALIZER } from 'oidc-client-rx'; import { APP_INITIALIZER } from 'oidc-client-rx';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { vi } from 'vitest';
import { PASSED_CONFIG } from './auth-config'; import { PASSED_CONFIG } from './auth-config';
import { ConfigurationService } from './config/config.service'; import { ConfigurationService } from './config/config.service';
import { import {
@ -60,12 +61,13 @@ describe('provideAuth', () => {
describe('features', () => { describe('features', () => {
let oidcSecurityServiceMock: OidcSecurityService; let oidcSecurityServiceMock: OidcSecurityService;
let spy: any;
beforeEach(async () => { beforeEach(async () => {
oidcSecurityServiceMock = createSpyObj<OidcSecurityService>( //@ts-ignore
'OidcSecurityService',
['checkAuthMultiple'] oidcSecurityServiceMock = new (mockClass(OidcSecurityService))();
); spy = vi.spyOn(oidcSecurityServiceMock, 'checkAuthMultiple');
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
providers: [ providers: [
provideAuth( provideAuth(
@ -83,14 +85,11 @@ describe('provideAuth', () => {
it('should provide APP_INITIALIZER config', () => { it('should provide APP_INITIALIZER config', () => {
const config = TestBed.inject(APP_INITIALIZER); const config = TestBed.inject(APP_INITIALIZER);
expect( expect(
config.length, config.length,
'Expected an APP_INITIALIZER to be registered' 'Expected an APP_INITIALIZER to be registered'
).toBe(1); ).toBe(1);
expect(oidcSecurityServiceMock.checkAuthMultiple).toHaveBeenCalledTimes( expect(spy).toHaveBeenCalledTimes(1);
1
);
}); });
}); });
}); });

View File

@ -1,4 +1,5 @@
import type { Provider } from 'injection-js'; import type { Provider } from 'injection-js';
import { firstValueFrom } from 'rxjs';
import { import {
PASSED_CONFIG, PASSED_CONFIG,
type PassedInitialConfig, type PassedInitialConfig,
@ -65,7 +66,7 @@ export function withAppInitializerAuthCheck(): AuthFeature {
ɵproviders: [ ɵproviders: [
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
useFactory: (oidcSecurityService: OidcSecurityService) => () => useFactory: (oidcSecurityService: OidcSecurityService) =>
oidcSecurityService.checkAuthMultiple(), oidcSecurityService.checkAuthMultiple(),
multi: true, multi: true,
deps: [OidcSecurityService], deps: [OidcSecurityService],

View File

@ -1,6 +1,6 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { lastValueFrom } from 'rxjs'; import { ReplaySubject, firstValueFrom, timer } from 'rxjs';
import { filter } from 'rxjs/operators'; import { filter, share } from 'rxjs/operators';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { EventTypes } from './event-types'; import { EventTypes } from './event-types';
import { PublicEventsService } from './public-events.service'; import { PublicEventsService } from './public-events.service';
@ -20,47 +20,63 @@ describe('Events Service', () => {
}); });
it('registering to single event with one event emit works', async () => { it('registering to single event with one event emit works', async () => {
const firedEvent = await lastValueFrom(eventsService.registerForEvents()); eventsService.fireEvent(EventTypes.ConfigLoaded, { myKey: 'myValue' });
const firedEvent = await firstValueFrom(eventsService.registerForEvents());
expect(firedEvent).toBeTruthy(); expect(firedEvent).toBeTruthy();
expect(firedEvent).toEqual({ expect(firedEvent).toEqual({
type: EventTypes.ConfigLoaded, type: EventTypes.ConfigLoaded,
value: { myKey: 'myValue' }, value: { myKey: 'myValue' },
}); });
eventsService.fireEvent(EventTypes.ConfigLoaded, { myKey: 'myValue' });
}); });
it('registering to single event with multiple same event emit works', async () => { it('registering to single event with multiple same event emit works', async () => {
const spy = vi.fn()('spy'); const spy = vi.fn();
eventsService.registerForEvents().subscribe((firedEvent) => {
spy(firedEvent);
expect(firedEvent).toBeTruthy();
});
const firedEvent = await lastValueFrom(eventsService.registerForEvents());
spy(firedEvent);
expect(firedEvent).toBeTruthy();
eventsService.fireEvent(EventTypes.ConfigLoaded, { myKey: 'myValue' }); eventsService.fireEvent(EventTypes.ConfigLoaded, { myKey: 'myValue' });
eventsService.fireEvent(EventTypes.ConfigLoaded, { myKey: 'myValue2' }); eventsService.fireEvent(EventTypes.ConfigLoaded, { myKey: 'myValue2' });
expect(spy.calls.count()).toBe(2); expect(spy.mock.calls.length).toBe(2);
expect(spy.calls.first().args[0]).toEqual({ expect(spy.mock.calls[0]?.[0]).toEqual({
type: EventTypes.ConfigLoaded, type: EventTypes.ConfigLoaded,
value: { myKey: 'myValue' }, value: { myKey: 'myValue' },
}); });
expect(spy.postSpy.mock.calls.at(-1)?.[0]).toEqual({ expect(spy.mock.calls.at(-1)?.[0]).toEqual({
type: EventTypes.ConfigLoaded, type: EventTypes.ConfigLoaded,
value: { myKey: 'myValue2' }, value: { myKey: 'myValue2' },
}); });
await firstValueFrom(timer(0));
}); });
it('registering to single event with multiple emit works', async () => { it('registering to single event with multiple emit works', async () => {
const firedEvent = await lastValueFrom( const o$ = eventsService.registerForEvents().pipe(
eventsService filter((x) => x.type === EventTypes.ConfigLoaded),
.registerForEvents() share({
.pipe(filter((x) => x.type === EventTypes.ConfigLoaded)) connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: true,
})
); );
expect(firedEvent).toBeTruthy();
expect(firedEvent).toEqual({ o$.subscribe((firedEvent) => {
type: EventTypes.ConfigLoaded, expect(firedEvent).toBeTruthy();
value: { myKey: 'myValue' }, expect(firedEvent).toEqual({
type: EventTypes.ConfigLoaded,
value: { myKey: 'myValue' },
});
return firedEvent;
}); });
eventsService.fireEvent(EventTypes.ConfigLoaded, { myKey: 'myValue' }); eventsService.fireEvent(EventTypes.ConfigLoaded, { myKey: 'myValue' });
eventsService.fireEvent(EventTypes.NewAuthenticationResult, true); eventsService.fireEvent(EventTypes.NewAuthenticationResult, true);
await firstValueFrom(o$);
}); });
}); });

View File

@ -13,6 +13,7 @@ describe('BrowserStorageService', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
BrowserStorageService,
mockProvider(LoggerService), mockProvider(LoggerService),
{ {
provide: AbstractSecurityStorage, provide: AbstractSecurityStorage,

View File

@ -1,5 +1,6 @@
import { inject, Injectable } from 'injection-js'; import { Injectable, inject } from 'injection-js';
import type { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
import { injectAbstractType } from '../injection';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
import { AbstractSecurityStorage } from './abstract-security-storage'; import { AbstractSecurityStorage } from './abstract-security-storage';
@ -7,7 +8,9 @@ import { AbstractSecurityStorage } from './abstract-security-storage';
export class BrowserStorageService { export class BrowserStorageService {
private readonly loggerService = inject(LoggerService); private readonly loggerService = inject(LoggerService);
private readonly abstractSecurityStorage = inject(AbstractSecurityStorage); private readonly abstractSecurityStorage = injectAbstractType(
AbstractSecurityStorage
);
read(key: string, configuration: OpenIdConfiguration): any { read(key: string, configuration: OpenIdConfiguration): any {
const { configId } = configuration; const { configId } = configuration;

View File

@ -18,7 +18,8 @@ describe('DefaultLocalStorageService', () => {
describe('read', () => { describe('read', () => {
it('should call localstorage.getItem', () => { it('should call localstorage.getItem', () => {
const spy = vi.spyOn(localStorage, 'getItem'); // https://github.com/jsdom/jsdom/issues/2318
const spy = vi.spyOn(Storage.prototype, 'getItem');
service.read('henlo'); service.read('henlo');
@ -28,7 +29,8 @@ describe('DefaultLocalStorageService', () => {
describe('write', () => { describe('write', () => {
it('should call localstorage.setItem', () => { it('should call localstorage.setItem', () => {
const spy = vi.spyOn(localStorage, 'setItem'); // https://github.com/jsdom/jsdom/issues/2318
const spy = vi.spyOn(Storage.prototype, 'setItem');
service.write('henlo', 'furiend'); service.write('henlo', 'furiend');
@ -38,7 +40,8 @@ describe('DefaultLocalStorageService', () => {
describe('remove', () => { describe('remove', () => {
it('should call localstorage.removeItem', () => { it('should call localstorage.removeItem', () => {
const spy = vi.spyOn(localStorage, 'removeItem'); // https://github.com/jsdom/jsdom/issues/2318
const spy = vi.spyOn(Storage.prototype, 'removeItem');
service.remove('henlo'); service.remove('henlo');
@ -48,7 +51,8 @@ describe('DefaultLocalStorageService', () => {
describe('clear', () => { describe('clear', () => {
it('should call localstorage.clear', () => { it('should call localstorage.clear', () => {
const spy = vi.spyOn(localStorage, 'clear'); // https://github.com/jsdom/jsdom/issues/2318
const spy = vi.spyOn(Storage.prototype, 'clear');
service.clear(); service.clear();

View File

@ -18,7 +18,8 @@ describe('DefaultSessionStorageService', () => {
describe('read', () => { describe('read', () => {
it('should call sessionstorage.getItem', () => { it('should call sessionstorage.getItem', () => {
const spy = vi.spyOn(sessionStorage, 'getItem'); // https://github.com/jsdom/jsdom/issues/2318
const spy = vi.spyOn(Storage.prototype, 'getItem');
service.read('henlo'); service.read('henlo');
@ -28,7 +29,8 @@ describe('DefaultSessionStorageService', () => {
describe('write', () => { describe('write', () => {
it('should call sessionstorage.setItem', () => { it('should call sessionstorage.setItem', () => {
const spy = vi.spyOn(sessionStorage, 'setItem'); // https://github.com/jsdom/jsdom/issues/2318
const spy = vi.spyOn(Storage.prototype, 'setItem');
service.write('henlo', 'furiend'); service.write('henlo', 'furiend');
@ -38,7 +40,8 @@ describe('DefaultSessionStorageService', () => {
describe('remove', () => { describe('remove', () => {
it('should call sessionstorage.removeItem', () => { it('should call sessionstorage.removeItem', () => {
const spy = vi.spyOn(sessionStorage, 'removeItem'); // https://github.com/jsdom/jsdom/issues/2318
const spy = vi.spyOn(Storage.prototype, 'removeItem');
service.remove('henlo'); service.remove('henlo');
@ -48,7 +51,8 @@ describe('DefaultSessionStorageService', () => {
describe('clear', () => { describe('clear', () => {
it('should call sessionstorage.clear', () => { it('should call sessionstorage.clear', () => {
const spy = vi.spyOn(sessionStorage, 'clear'); // https://github.com/jsdom/jsdom/issues/2318
const spy = vi.spyOn(Storage.prototype, 'clear');
service.clear(); service.clear();

View File

@ -1,4 +1,4 @@
import { TestBed } from '@/testing'; import { TestBed, mockImplementationWhenArgsEqual } from '@/testing';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { mockProvider } from '../testing/mock'; import { mockProvider } from '../testing/mock';
import { BrowserStorageService } from './browser-storage.service'; import { BrowserStorageService } from './browser-storage.service';
@ -10,7 +10,10 @@ describe('Storage Persistence Service', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [mockProvider(BrowserStorageService)], providers: [
StoragePersistenceService,
mockProvider(BrowserStorageService),
],
}); });
service = TestBed.inject(StoragePersistenceService); service = TestBed.inject(StoragePersistenceService);
securityStorage = TestBed.inject(BrowserStorageService); securityStorage = TestBed.inject(BrowserStorageService);
@ -239,10 +242,16 @@ describe('Storage Persistence Service', () => {
}; };
const spy = vi.spyOn(securityStorage, 'read'); const spy = vi.spyOn(securityStorage, 'read');
spy mockImplementationWhenArgsEqual(
.withArgs('reusable_refresh_token', config) spy,
.mockReturnValue(returnValue); ['reusable_refresh_token', config],
spy.withArgs('authnResult', config).mockReturnValue(undefined); () => returnValue
);
mockImplementationWhenArgsEqual(
spy,
['authnResult', config],
() => undefined
);
const result = service.getRefreshToken(config); const result = service.getRefreshToken(config);
expect(result).toBe(returnValue.reusable_refresh_token); expect(result).toBe(returnValue.reusable_refresh_token);

View File

@ -4,10 +4,12 @@ import { vi } from 'vitest';
// Create retriable observable stream to test retry / retryWhen. Credits to: // Create retriable observable stream to test retry / retryWhen. Credits to:
// https://stackoverflow.com/questions/51399819/how-to-create-a-mock-observable-to-test-http-rxjs-retry-retrywhen-in-angular // https://stackoverflow.com/questions/51399819/how-to-create-a-mock-observable-to-test-http-rxjs-retry-retrywhen-in-angular
export const createRetriableStream = (...resp$: any): Observable<any> => { export const createRetriableStream = (...resp$: any[]): Observable<any> => {
const fetchData = vi.fn()('fetchData'); const fetchData = vi.fn();
fetchData.mockReturnValues(...resp$); for (const r of resp$) {
fetchData.mockReturnValueOnce(r);
}
return of(null).pipe(switchMap((_) => fetchData())); return of(null).pipe(switchMap((_) => fetchData()));
}; };

View File

@ -1,14 +1,11 @@
import { getTestBed } from '@/testing/testbed'; import { TestBed } from '@/testing/testbed';
import { import { DOCUMENT } from 'oidc-client-rx/dom';
BrowserDynamicTestingModule, import 'reflect-metadata';
platformBrowserDynamicTesting,
} from '@angular/platform-browser-dynamic/testing';
// First, initialize the Angular testing environment. // First, initialize the Angular testing environment.
getTestBed().initTestEnvironment( TestBed.initTestEnvironment([
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
{ {
teardown: { destroyAfterEach: false }, provide: DOCUMENT,
} useValue: document,
); },
]);

View File

@ -1,6 +1,10 @@
import type { Provider } from 'injection-js'; import type { Provider } from 'injection-js';
export function mockClass<T>(obj: new (...args: any[]) => T): any { export function mockClass<T>(
obj: new (...args: any[]) => T
): new (
...args: any[]
) => T {
const keys = Object.getOwnPropertyNames(obj.prototype); const keys = Object.getOwnPropertyNames(obj.prototype);
const allMethods = keys.filter((key) => { const allMethods = keys.filter((key) => {
try { try {
@ -14,9 +18,16 @@ export function mockClass<T>(obj: new (...args: any[]) => T): any {
const mockedClass = class T {}; const mockedClass = class T {};
for (const method of allMethods) { for (const method of allMethods) {
(mockedClass.prototype as any)[method] = (): void => { const mockImplementation = Reflect.getMetadata(
return; 'mock:implementation',
}; obj.prototype,
method
);
(mockedClass.prototype as any)[method] =
mockImplementation ??
((): any => {
return;
});
} }
for (const method of allProperties) { for (const method of allProperties) {
@ -28,12 +39,15 @@ export function mockClass<T>(obj: new (...args: any[]) => T): any {
}); });
} }
return mockedClass; return mockedClass as any;
} }
export function mockProvider<T>(obj: new (...args: any[]) => T): Provider { export function mockProvider<T>(
obj: new (...args: any[]) => T,
token?: any
): Provider {
return { return {
provide: obj, provide: token ?? obj,
useClass: mockClass(obj), useClass: mockClass(obj),
}; };
} }

View File

@ -1,23 +1,31 @@
import type { Provider } from 'injection-js'; import type { Provider } from 'injection-js';
import { JSDOM } from 'jsdom';
import { AbstractRouter, type Navigation, type UrlTree } from 'oidc-client-rx'; import { AbstractRouter, type Navigation, type UrlTree } from 'oidc-client-rx';
export class MockRouter extends AbstractRouter { export class MockRouter extends AbstractRouter {
dom = new JSDOM('', {
url: 'http://localhost',
});
navigation: Navigation = { navigation: Navigation = {
id: 1, id: 1,
extras: {}, extras: {},
initialUrl: new URL('https://localhost/'), initialUrl: this.parseUrl(this.dom.window.location.href),
extractedUrl: new URL('https://localhost/'), extractedUrl: this.parseUrl(this.dom.window.location.href),
trigger: 'imperative', trigger: 'imperative',
previousNavigation: null, previousNavigation: null,
}; };
navigateByUrl(url: string): void { navigateByUrl(url: string): void {
const prevNavigation = this.navigation; const prevNavigation = this.navigation;
this.dom.reconfigure({
url: new URL(url, this.dom.window.location.href).href,
});
this.navigation = { this.navigation = {
id: prevNavigation.id + 1, id: prevNavigation.id + 1,
extras: {}, extras: {},
initialUrl: prevNavigation.initialUrl, initialUrl: prevNavigation.initialUrl,
extractedUrl: new URL(url), extractedUrl: this.parseUrl(this.dom.window.location.href),
trigger: prevNavigation.trigger, trigger: prevNavigation.trigger,
previousNavigation: prevNavigation, previousNavigation: prevNavigation,
}; };
@ -26,7 +34,8 @@ export class MockRouter extends AbstractRouter {
return this.navigation; return this.navigation;
} }
parseUrl(url: string): UrlTree { parseUrl(url: string): UrlTree {
return new URL(url); const u = new URL(url, this.dom.window.location.href);
return `${u.pathname}${u.search}${u.hash}`;
} }
} }

View File

@ -35,20 +35,80 @@ export function mockImplementationWhenArgsEqual<M extends MockInstance<any>>(
}); });
} }
export function mockImplementationWhenArgs<M extends MockInstance<any>>( type Procedure = (...args: any[]) => any;
mockInstance: M, type Methods<T> = keyof {
whenArgs: ( [K in keyof T as T[K] extends Procedure ? K : never]: T[K];
...args: Parameters<M extends MockInstance<infer T> ? T : never> };
) => boolean, type Classes<T> = {
implementation: Exclude<ReturnType<M['getMockImplementation']>, undefined> [K in keyof T]: T[K] extends new (...args: any[]) => any ? K : never;
): M { }[keyof T] &
const spyImpl = mockInstance.getMockImplementation()!; (string | symbol);
export type MockInstanceWithOrigin<M extends Procedure> = MockInstance<M> & {
getOriginImplementation?: () => any;
};
export function spyOnWithOrigin<
T,
M extends Classes<Required<T>> | Methods<Required<T>>,
>(
obj: T,
methodName: M
): Required<T>[M] extends {
new (...args: infer A): infer R;
}
? MockInstanceWithOrigin<(this: R, ...args: A) => R>
: T[M] extends Procedure
? MockInstanceWithOrigin<T[M]>
: never {
let currentObj = obj;
let origin:
| (Required<T>[M] extends {
new (...args: infer A): infer R;
}
? (this: R, ...args: A) => R
: T[M] extends Procedure
? T[M]
: never)
| undefined;
while (currentObj) {
origin = currentObj[methodName] as any;
if (origin) {
break;
}
currentObj = Object.getPrototypeOf(currentObj);
}
const spy = vi.spyOn(obj, methodName as any) as Required<T>[M] extends {
new (...args: infer A): infer R;
}
? MockInstanceWithOrigin<(this: R, ...args: A) => R>
: T[M] extends Procedure
? MockInstanceWithOrigin<T[M]>
: never;
spy.getOriginImplementation = () => origin;
return spy;
}
export function mockImplementationWhenArgs<T extends Procedure = Procedure>(
mockInstance: MockInstance<T> & { getOriginImplementation?: () => T },
whenArgs: (...args: Parameters<T>) => boolean,
implementation: T
): MockInstance<T> {
const spyImpl =
mockInstance.getMockImplementation() ??
mockInstance.getOriginImplementation?.();
return mockInstance.mockImplementation((...args) => { return mockInstance.mockImplementation((...args) => {
if (isEqual(args, whenArgs)) { if (whenArgs(...args)) {
return implementation(...args); return implementation(...args);
} }
return spyImpl?.(...args); if (spyImpl) {
return spyImpl(...args);
}
throw new Error('Mock implementation not defined for these arguments.');
}); });
} }
@ -58,46 +118,37 @@ export function mockImplementationWhenArgs<M extends MockInstance<any>>(
export function spyOnProperty<T, K extends keyof T>( export function spyOnProperty<T, K extends keyof T>(
obj: T, obj: T,
propertyKey: K, propertyKey: K,
accessType: 'get' | 'set' = 'get', accessType: 'get' | 'set' = 'get'
mockImplementation?: any
) { ) {
const originalDescriptor = Object.getOwnPropertyDescriptor(obj, propertyKey); const ownDescriptor = Object.getOwnPropertyDescriptor(obj, propertyKey);
let finalDescriptor: PropertyDescriptor | undefined;
if (!originalDescriptor) { let currentObj = obj;
throw new Error( while (currentObj) {
`Property ${String(propertyKey)} does not exist on the object.` finalDescriptor = Object.getOwnPropertyDescriptor(currentObj, propertyKey);
); if (finalDescriptor) {
break;
}
currentObj = Object.getPrototypeOf(currentObj);
} }
const spy = vi.fn(); const spy = vi.fn();
let value: T[K] | undefined;
if (accessType === 'get') { if (accessType === 'get') {
Object.defineProperty(obj, propertyKey, { Object.defineProperty(obj, propertyKey, {
get: mockImplementation get: spy,
? () => {
value = mockImplementation();
return value;
}
: spy,
configurable: true, configurable: true,
}); });
} else if (accessType === 'set') { } else if (accessType === 'set') {
Object.defineProperty(obj, propertyKey, { Object.defineProperty(obj, propertyKey, {
set: mockImplementation set: spy,
? (next) => {
value = next;
}
: spy,
configurable: true, configurable: true,
}); });
} }
// 恢复原始属性 // 恢复原始属性
spy.mockRestore = () => { spy.mockRestore = () => {
if (originalDescriptor) { if (ownDescriptor) {
Object.defineProperty(obj, propertyKey, originalDescriptor); Object.defineProperty(obj, propertyKey, ownDescriptor);
} else { } else {
delete obj[propertyKey]; delete obj[propertyKey];
} }

View File

@ -13,21 +13,33 @@ export interface TestModuleMetadata {
} }
export class TestBed { export class TestBed {
static environmentInjector?: Injector;
private injector: ReflectiveInjector; private injector: ReflectiveInjector;
private providers: Provider[] = []; private providers: Provider[] = [];
private imports: Injector[] = []; private imports: Injector[] = [];
constructor(metadata: TestModuleMetadata = {}) { constructor(
metadata: TestModuleMetadata = {},
environmentInjector?: Injector
) {
const providers = metadata.providers ?? []; const providers = metadata.providers ?? [];
const imports = metadata.imports ?? []; const imports = metadata.imports ?? [];
this.injector = ReflectiveInjector.resolveAndCreate(providers); this.injector = ReflectiveInjector.resolveAndCreate(
providers,
environmentInjector
);
this.imports = imports.map((importFn) => importFn(this.injector)); this.imports = imports.map((importFn) => importFn(this.injector));
} }
static #instance?: TestBed; static #instance?: TestBed;
static initTestEnvironment(providers: Provider[] = []) {
TestBed.environmentInjector =
ReflectiveInjector.resolveAndCreate(providers);
}
static configureTestingModule(metadata: TestModuleMetadata = {}) { static configureTestingModule(metadata: TestModuleMetadata = {}) {
const newTestBed = new TestBed(metadata); const newTestBed = new TestBed(metadata, TestBed.environmentInjector);
TestBed.#instance = newTestBed; TestBed.#instance = newTestBed;
return newTestBed; return newTestBed;

View File

@ -1,5 +1,5 @@
import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { TestBed, mockImplementationWhenArgsEqual } from '@/testing';
import { Observable, lastValueFrom, of, throwError } from 'rxjs'; import { Observable, firstValueFrom, of, throwError } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { DataService } from '../api/data.service'; import { DataService } from '../api/data.service';
import type { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
@ -30,6 +30,7 @@ describe('User Service', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
UserService,
mockProvider(StoragePersistenceService), mockProvider(StoragePersistenceService),
mockProvider(LoggerService), mockProvider(LoggerService),
mockProvider(DataService), mockProvider(DataService),
@ -70,7 +71,7 @@ describe('User Service', () => {
userDataInstore userDataInstore
); );
const token = await lastValueFrom( const token = await firstValueFrom(
userService.getAndPersistUserDataInStore( userService.getAndPersistUserDataInStore(
config, config,
[config], [config],
@ -99,7 +100,7 @@ describe('User Service', () => {
); );
vi.spyOn(userService, 'setUserDataToStore'); vi.spyOn(userService, 'setUserDataToStore');
const token = await lastValueFrom( const token = await firstValueFrom(
userService.getAndPersistUserDataInStore( userService.getAndPersistUserDataInStore(
config, config,
[config], [config],
@ -129,7 +130,7 @@ describe('User Service', () => {
userDataInstore userDataInstore
); );
const token = await lastValueFrom( const token = await firstValueFrom(
userService.getAndPersistUserDataInStore( userService.getAndPersistUserDataInStore(
config, config,
[config], [config],
@ -161,7 +162,7 @@ describe('User Service', () => {
.spyOn(userService as any, 'getIdentityUserData') .spyOn(userService as any, 'getIdentityUserData')
.mockReturnValue(of(userDataFromSts)); .mockReturnValue(of(userDataFromSts));
const token = await lastValueFrom( const token = await firstValueFrom(
userService.getAndPersistUserDataInStore( userService.getAndPersistUserDataInStore(
config, config,
[config], [config],
@ -202,7 +203,7 @@ describe('User Service', () => {
'accessToken' 'accessToken'
); );
const token = await lastValueFrom( const token = await firstValueFrom(
userService.getAndPersistUserDataInStore( userService.getAndPersistUserDataInStore(
config, config,
[config], [config],
@ -246,7 +247,7 @@ describe('User Service', () => {
); );
try { try {
await lastValueFrom( await firstValueFrom(
userService.getAndPersistUserDataInStore( userService.getAndPersistUserDataInStore(
config, config,
[config], [config],
@ -283,7 +284,7 @@ describe('User Service', () => {
.spyOn(userService as any, 'getIdentityUserData') .spyOn(userService as any, 'getIdentityUserData')
.mockReturnValue(of(userDataFromSts)); .mockReturnValue(of(userDataFromSts));
const token = await lastValueFrom( const token = await firstValueFrom(
userService.getAndPersistUserDataInStore( userService.getAndPersistUserDataInStore(
config, config,
[config], [config],
@ -539,7 +540,7 @@ describe('User Service', () => {
() => null () => null
); );
try { try {
await lastValueFrom(serviceAsAny.getIdentityUserData(config)); await firstValueFrom(serviceAsAny.getIdentityUserData(config));
} catch (err: any) { } catch (err: any) {
expect(err).toBeTruthy(); expect(err).toBeTruthy();
} }
@ -560,7 +561,7 @@ describe('User Service', () => {
); );
try { try {
await lastValueFrom(serviceAsAny.getIdentityUserData(config)); await firstValueFrom(serviceAsAny.getIdentityUserData(config));
} catch (err: any) { } catch (err: any) {
expect(err).toBeTruthy(); expect(err).toBeTruthy();
} }
@ -580,7 +581,7 @@ describe('User Service', () => {
() => ({ userInfoEndpoint: 'userInfoEndpoint' }) () => ({ userInfoEndpoint: 'userInfoEndpoint' })
); );
await lastValueFrom(serviceAsAny.getIdentityUserData(config)); await firstValueFrom(serviceAsAny.getIdentityUserData(config));
expect(spy).toHaveBeenCalledExactlyOnceWith( expect(spy).toHaveBeenCalledExactlyOnceWith(
'userInfoEndpoint', 'userInfoEndpoint',
config, config,
@ -607,7 +608,7 @@ describe('User Service', () => {
) )
); );
const res = await lastValueFrom( const res = await firstValueFrom(
(userService as any).getIdentityUserData(config) (userService as any).getIdentityUserData(config)
); );
@ -634,7 +635,7 @@ describe('User Service', () => {
) )
); );
const res = await lastValueFrom( const res = await firstValueFrom(
(userService as any).getIdentityUserData(config) (userService as any).getIdentityUserData(config)
); );
expect(res).toBeTruthy(); expect(res).toBeTruthy();
@ -662,7 +663,7 @@ describe('User Service', () => {
); );
try { try {
await lastValueFrom((userService as any).getIdentityUserData(config)); await firstValueFrom((userService as any).getIdentityUserData(config));
} catch (err: any) { } catch (err: any) {
expect(err).toBeTruthy(); expect(err).toBeTruthy();
} }

View File

@ -0,0 +1,17 @@
/// <reference types="reflect-metadata" />
// biome-ignore lint/complexity/noBannedTypes: <explanation>
export function MockUtil<F = Function>(options: { implementation: F }) {
return (
targetClass: any,
propertyKey: string,
_descriptor?: TypedPropertyDescriptor<(...args: any[]) => any>
): void => {
Reflect?.defineMetadata?.(
'mock:implementation',
options.implementation,
targetClass,
propertyKey
);
};
}

View File

@ -8,7 +8,7 @@ describe('Token Helper Service', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [mockProvider(LoggerService)], providers: [TokenHelperService, mockProvider(LoggerService)],
}); });
tokenHelperService = TestBed.inject(TokenHelperService); tokenHelperService = TestBed.inject(TokenHelperService);
}); });

View File

@ -13,6 +13,7 @@ describe('CurrentUrlService with existing Url', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
CurrentUrlService,
{ {
provide: DOCUMENT, provide: DOCUMENT,
useValue: documentValue, useValue: documentValue,

View File

@ -1,5 +1,5 @@
import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { TestBed, mockImplementationWhenArgsEqual } from '@/testing';
import { lastValueFrom, of } from 'rxjs'; import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import type { OpenIdConfiguration } from '../../config/openid-configuration'; import type { OpenIdConfiguration } from '../../config/openid-configuration';
import { FlowsDataService } from '../../flows/flows-data.service'; import { FlowsDataService } from '../../flows/flows-data.service';
@ -1041,14 +1041,14 @@ describe('UrlService Tests', () => {
describe('getAuthorizeUrl', () => { describe('getAuthorizeUrl', () => {
it('returns null if no config is given', async () => { it('returns null if no config is given', async () => {
const url = await lastValueFrom(service.getAuthorizeUrl(null)); const url = await firstValueFrom(service.getAuthorizeUrl(null));
expect(url).toBeNull(); expect(url).toBeNull();
}); });
it('returns null if current flow is code flow and no redirect url is defined', async () => { it('returns null if current flow is code flow and no redirect url is defined', async () => {
vi.spyOn(flowHelper, 'isCurrentFlowCodeFlow').mockReturnValue(true); vi.spyOn(flowHelper, 'isCurrentFlowCodeFlow').mockReturnValue(true);
const result = await lastValueFrom( const result = await firstValueFrom(
service.getAuthorizeUrl({ configId: 'configId1' }) service.getAuthorizeUrl({ configId: 'configId1' })
); );
expect(result).toBeNull(); expect(result).toBeNull();
@ -1062,7 +1062,7 @@ describe('UrlService Tests', () => {
redirectUrl: 'some-redirectUrl', redirectUrl: 'some-redirectUrl',
} as OpenIdConfiguration; } as OpenIdConfiguration;
const result = await lastValueFrom(service.getAuthorizeUrl(config)); const result = await firstValueFrom(service.getAuthorizeUrl(config));
expect(result).toBe(''); expect(result).toBe('');
}); });
@ -1090,7 +1090,7 @@ describe('UrlService Tests', () => {
() => ({ authorizationEndpoint }) () => ({ authorizationEndpoint })
); );
const result = await lastValueFrom(service.getAuthorizeUrl(config)); const result = await firstValueFrom(service.getAuthorizeUrl(config));
expect(result).toBe( expect(result).toBe(
'authorizationEndpoint?client_id=some-clientId&redirect_uri=some-redirectUrl&response_type=testResponseType&scope=testScope&nonce=undefined&state=undefined&code_challenge=some-code-challenge&code_challenge_method=S256' 'authorizationEndpoint?client_id=some-clientId&redirect_uri=some-redirectUrl&response_type=testResponseType&scope=testScope&nonce=undefined&state=undefined&code_challenge=some-code-challenge&code_challenge_method=S256'
); );
@ -1107,7 +1107,7 @@ describe('UrlService Tests', () => {
'createUrlImplicitFlowAuthorize' 'createUrlImplicitFlowAuthorize'
); );
await lastValueFrom(service.getAuthorizeUrl({ configId: 'configId1' })); await firstValueFrom(service.getAuthorizeUrl({ configId: 'configId1' }));
expect(spyCreateUrlCodeFlowAuthorize).not.toHaveBeenCalled(); expect(spyCreateUrlCodeFlowAuthorize).not.toHaveBeenCalled();
expect(spyCreateUrlImplicitFlowAuthorize).toHaveBeenCalled(); expect(spyCreateUrlImplicitFlowAuthorize).toHaveBeenCalled();
}); });
@ -1119,18 +1119,20 @@ describe('UrlService Tests', () => {
.mockReturnValue(''); .mockReturnValue('');
const resultObs$ = service.getAuthorizeUrl({ configId: 'configId1' }); const resultObs$ = service.getAuthorizeUrl({ configId: 'configId1' });
const result = await lastValueFrom(resultObs$); const result = await firstValueFrom(resultObs$);
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
expect(result).toBe(''); expect(result).toBe('');
}); });
}); });
describe('getRefreshSessionSilentRenewUrl', () => { describe('getRefreshSessionSilentRenewUrl', () => {
it('calls createUrlCodeFlowWithSilentRenew if current flow is code flow', () => { it('calls createUrlCodeFlowWithSilentRenew if current flow is code flow', async () => {
vi.spyOn(flowHelper, 'isCurrentFlowCodeFlow').mockReturnValue(true); vi.spyOn(flowHelper, 'isCurrentFlowCodeFlow').mockReturnValue(true);
const spy = vi.spyOn(service as any, 'createUrlCodeFlowWithSilentRenew'); const spy = vi.spyOn(service as any, 'createUrlCodeFlowWithSilentRenew');
service.getRefreshSessionSilentRenewUrl({ configId: 'configId1' }); await firstValueFrom(
service.getRefreshSessionSilentRenewUrl({ configId: 'configId1' })
);
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
}); });
@ -1159,7 +1161,7 @@ describe('UrlService Tests', () => {
configId: 'configId1', configId: 'configId1',
}); });
const result = await lastValueFrom(resultObs$); const result = await firstValueFrom(resultObs$);
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
expect(result).toBe(''); expect(result).toBe('');
}); });
@ -1344,7 +1346,7 @@ describe('UrlService Tests', () => {
redirectUrl: '', redirectUrl: '',
}); });
const result = await lastValueFrom(resultObs$); const result = await firstValueFrom(resultObs$);
expect(result).toBe(null); expect(result).toBe(null);
}); });
@ -1372,7 +1374,7 @@ describe('UrlService Tests', () => {
const resultObs$ = service.createBodyForParCodeFlowRequest(config); const resultObs$ = service.createBodyForParCodeFlowRequest(config);
const result = await lastValueFrom(resultObs$); const result = await firstValueFrom(resultObs$);
expect(result).toBe( expect(result).toBe(
'client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256' 'client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256'
); );
@ -1402,7 +1404,7 @@ describe('UrlService Tests', () => {
const resultObs$ = service.createBodyForParCodeFlowRequest(config); const resultObs$ = service.createBodyForParCodeFlowRequest(config);
const result = await lastValueFrom(resultObs$); const result = await firstValueFrom(resultObs$);
expect(result).toBe( expect(result).toBe(
'client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256&hd=testHdParam' 'client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256&hd=testHdParam'
); );
@ -1432,7 +1434,7 @@ describe('UrlService Tests', () => {
const resultObs$ = service.createBodyForParCodeFlowRequest(config); const resultObs$ = service.createBodyForParCodeFlowRequest(config);
const result = await lastValueFrom(resultObs$); const result = await firstValueFrom(resultObs$);
expect(result).toBe( expect(result).toBe(
'client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256&hd=testHdParam&any=thing' 'client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256&hd=testHdParam&any=thing'
); );
@ -1466,7 +1468,7 @@ describe('UrlService Tests', () => {
}, },
}); });
const result = await lastValueFrom(resultObs$); const result = await firstValueFrom(resultObs$);
expect(result).toBe( expect(result).toBe(
'client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256&hd=testHdParam&any=thing&any=otherThing' 'client_id=testClientId&redirect_uri=testRedirectUrl&response_type=testResponseType&scope=testScope&nonce=testNonce&state=testState&code_challenge=testCodeChallenge&code_challenge_method=S256&hd=testHdParam&any=thing&any=otherThing'
); );
@ -1596,7 +1598,7 @@ describe('UrlService Tests', () => {
const resultObs$ = serviceAsAny.createUrlCodeFlowWithSilentRenew(config); const resultObs$ = serviceAsAny.createUrlCodeFlowWithSilentRenew(config);
const result = await lastValueFrom(resultObs$); const result = await firstValueFrom(resultObs$);
expect(result).toBe(''); expect(result).toBe('');
}); });
@ -1639,7 +1641,7 @@ describe('UrlService Tests', () => {
const resultObs$ = serviceAsAny.createUrlCodeFlowWithSilentRenew(config); const resultObs$ = serviceAsAny.createUrlCodeFlowWithSilentRenew(config);
const result = await lastValueFrom(resultObs$); const result = await firstValueFrom(resultObs$);
expect(result).toBe( expect(result).toBe(
`authorizationEndpoint?client_id=${clientId}&redirect_uri=http%3A%2F%2Fany-url.com&response_type=${responseType}&scope=${scope}&nonce=${nonce}&state=${state}&prompt=none` `authorizationEndpoint?client_id=${clientId}&redirect_uri=http%3A%2F%2Fany-url.com&response_type=${responseType}&scope=${scope}&nonce=${nonce}&state=${state}&prompt=none`
); );
@ -1680,7 +1682,7 @@ describe('UrlService Tests', () => {
const resultObs$ = serviceAsAny.createUrlCodeFlowWithSilentRenew(config); const resultObs$ = serviceAsAny.createUrlCodeFlowWithSilentRenew(config);
const result = await lastValueFrom(resultObs$); const result = await firstValueFrom(resultObs$);
expect(result).toBe(''); expect(result).toBe('');
}); });
}); });
@ -1796,7 +1798,7 @@ describe('UrlService Tests', () => {
const resultObs$ = serviceAsAny.createUrlCodeFlowAuthorize(config); const resultObs$ = serviceAsAny.createUrlCodeFlowAuthorize(config);
const result = await lastValueFrom(resultObs$); const result = await firstValueFrom(resultObs$);
expect(result).toBeNull(); expect(result).toBeNull();
}); });
@ -1838,7 +1840,7 @@ describe('UrlService Tests', () => {
const resultObs$ = serviceAsAny.createUrlCodeFlowAuthorize(config); const resultObs$ = serviceAsAny.createUrlCodeFlowAuthorize(config);
const result = await lastValueFrom(resultObs$); const result = await firstValueFrom(resultObs$);
expect(result).toBe( expect(result).toBe(
`authorizationEndpoint?client_id=clientId&redirect_uri=http%3A%2F%2Fany-url.com&response_type=${responseType}&scope=${scope}&nonce=${nonce}&state=${state}` `authorizationEndpoint?client_id=clientId&redirect_uri=http%3A%2F%2Fany-url.com&response_type=${responseType}&scope=${scope}&nonce=${nonce}&state=${state}`
); );
@ -1887,7 +1889,7 @@ describe('UrlService Tests', () => {
customParams: { to: 'add', as: 'well' }, customParams: { to: 'add', as: 'well' },
}); });
const result = await lastValueFrom(resultObs$); const result = await firstValueFrom(resultObs$);
expect(result).toBe( expect(result).toBe(
`authorizationEndpoint?client_id=clientId&redirect_uri=http%3A%2F%2Fany-url.com&response_type=${responseType}&scope=${scope}&nonce=${nonce}&state=${state}&to=add&as=well` `authorizationEndpoint?client_id=clientId&redirect_uri=http%3A%2F%2Fany-url.com&response_type=${responseType}&scope=${scope}&nonce=${nonce}&state=${state}&to=add&as=well`
); );
@ -1924,7 +1926,7 @@ describe('UrlService Tests', () => {
const resultObs$ = serviceAsAny.createUrlCodeFlowAuthorize(config); const resultObs$ = serviceAsAny.createUrlCodeFlowAuthorize(config);
const result = await lastValueFrom(resultObs$); const result = await firstValueFrom(resultObs$);
expect(result).toBe(''); expect(result).toBe('');
}); });
}); });

View File

@ -1,10 +1,10 @@
import { HttpParams } from '@ngify/http';
import { Injectable, inject } from 'injection-js'; import { Injectable, inject } from 'injection-js';
import { type Observable, of } from 'rxjs'; import { type Observable, of } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import type { AuthOptions } from '../../auth-options'; import type { AuthOptions } from '../../auth-options';
import type { OpenIdConfiguration } from '../../config/openid-configuration'; import type { OpenIdConfiguration } from '../../config/openid-configuration';
import { FlowsDataService } from '../../flows/flows-data.service'; import { FlowsDataService } from '../../flows/flows-data.service';
import { HttpParams } from '../../http';
import { LoggerService } from '../../logging/logger.service'; import { LoggerService } from '../../logging/logger.service';
import { StoragePersistenceService } from '../../storage/storage-persistence.service'; import { StoragePersistenceService } from '../../storage/storage-persistence.service';
import { JwtWindowCryptoService } from '../../validation/jwt-window-crypto.service'; import { JwtWindowCryptoService } from '../../validation/jwt-window-crypto.service';
@ -873,8 +873,8 @@ export class UrlService {
private createHttpParams(existingParams?: string): HttpParams { private createHttpParams(existingParams?: string): HttpParams {
existingParams = existingParams ?? ''; existingParams = existingParams ?? '';
// @TODO @ngify/http return new HttpParams({
return new HttpParams(existingParams || undefined, { fromString: existingParams,
encoder: new UriEncoder(), encoder: new UriEncoder(),
}); });
} }

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { lastValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { CryptoService } from '../utils/crypto/crypto.service'; import { CryptoService } from '../utils/crypto/crypto.service';
import { JwtWindowCryptoService } from './jwt-window-crypto.service'; import { JwtWindowCryptoService } from './jwt-window-crypto.service';
@ -25,7 +25,7 @@ describe('JwtWindowCryptoService', () => {
'44445543344242132145455aaabbdc3b4' '44445543344242132145455aaabbdc3b4'
); );
const value = await lastValueFrom(observable); const value = await firstValueFrom(observable);
expect(value).toBe(outcome); expect(value).toBe(outcome);
}); });
}); });

View File

@ -1,12 +1,14 @@
import { inject, Injectable } from 'injection-js'; import { Injectable, inject } from 'injection-js';
import { from, type Observable } from 'rxjs'; import { BehaviorSubject, type Observable, from } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { CryptoService } from '../utils/crypto/crypto.service'; import { CryptoService } from '../utils/crypto/crypto.service';
import { MockUtil } from '../utils/reflect';
@Injectable() @Injectable()
export class JwtWindowCryptoService { export class JwtWindowCryptoService {
private readonly cryptoService = inject(CryptoService); private readonly cryptoService = inject(CryptoService);
@MockUtil({ implementation: () => new BehaviorSubject(undefined) })
generateCodeChallenge(codeVerifier: string): Observable<string> { generateCodeChallenge(codeVerifier: string): Observable<string> {
return this.calcHash(codeVerifier).pipe( return this.calcHash(codeVerifier).pipe(
map((challengeRaw: string) => this.base64UrlEncode(challengeRaw)) map((challengeRaw: string) => this.base64UrlEncode(challengeRaw))

View File

@ -2,22 +2,12 @@ import { ValidationResult } from './validation-result';
export class StateValidationResult { export class StateValidationResult {
constructor( constructor(
// biome-ignore lint/style/noParameterProperties: <explanation>
// biome-ignore lint/nursery/useConsistentMemberAccessibility: <explanation>
public accessToken = '', public accessToken = '',
// biome-ignore lint/style/noParameterProperties: <explanation>
// biome-ignore lint/nursery/useConsistentMemberAccessibility: <explanation>
public idToken = '', public idToken = '',
// biome-ignore lint/style/noParameterProperties: <explanation>
// biome-ignore lint/nursery/useConsistentMemberAccessibility: <explanation>
public authResponseIsValid = false, public authResponseIsValid = false,
// biome-ignore lint/style/noParameterProperties: <explanation>
// biome-ignore lint/nursery/useConsistentMemberAccessibility: <explanation>
public decodedIdToken: any = { public decodedIdToken: any = {
at_hash: '', at_hash: '',
}, },
// biome-ignore lint/style/noParameterProperties: <explanation>
// biome-ignore lint/nursery/useConsistentMemberAccessibility: <explanation>
public state: ValidationResult = ValidationResult.NotSet public state: ValidationResult = ValidationResult.NotSet
) {} ) {}
} }

View File

@ -1,5 +1,9 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { lastValueFrom, of } from 'rxjs'; import {
mockImplementationWhenArgs,
mockImplementationWhenArgsEqual,
} from '@/testing/spy';
import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import type { AuthWellKnownEndpoints } from '../config/auth-well-known/auth-well-known-endpoints'; import type { AuthWellKnownEndpoints } from '../config/auth-well-known/auth-well-known-endpoints';
import type { OpenIdConfiguration } from '../config/openid-configuration'; import type { OpenIdConfiguration } from '../config/openid-configuration';
@ -28,6 +32,7 @@ describe('State Validation Service', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
StateValidationService,
mockProvider(StoragePersistenceService), mockProvider(StoragePersistenceService),
mockProvider(TokenValidationService), mockProvider(TokenValidationService),
mockProvider(LoggerService), mockProvider(LoggerService),
@ -687,8 +692,8 @@ describe('State Validation Service', () => {
config config
); );
const isValid = await lastValueFrom(isValidObs$); const isValid = await firstValueFrom(isValidObs$);
expect(isValid.authResponseIsValid).toBe(false); expect(isValid.authResponseIsValid).toBeFalsy();
}); });
it('should return invalid context error', async () => { it('should return invalid context error', async () => {
@ -723,7 +728,7 @@ describe('State Validation Service', () => {
config config
); );
const isValid = await lastValueFrom(isValidObs$); const isValid = await firstValueFrom(isValidObs$);
expect(isValid.authResponseIsValid).toBe(false); expect(isValid.authResponseIsValid).toBe(false);
}); });
@ -787,13 +792,23 @@ describe('State Validation Service', () => {
).mockReturnValue(false); ).mockReturnValue(false);
const readSpy = vi.spyOn(storagePersistenceService, 'read'); const readSpy = vi.spyOn(storagePersistenceService, 'read');
readSpy mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) readSpy,
.mockReturnValue(authWellKnownEndpoints); ['authWellKnownEndPoints', config],
readSpy () => authWellKnownEndpoints
.withArgs('authStateControl', config) );
.mockReturnValue('authStateControl');
readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); mockImplementationWhenArgsEqual(
readSpy,
['authStateControl', config],
() => 'authStateControl'
);
mockImplementationWhenArgsEqual(
readSpy,
['authNonce', config],
() => 'authNonce'
);
const logWarningSpy = vi const logWarningSpy = vi
.spyOn(loggerService, 'logWarning') .spyOn(loggerService, 'logWarning')
@ -818,7 +833,7 @@ describe('State Validation Service', () => {
config config
); );
const state = await lastValueFrom(stateObs$); const state = await firstValueFrom(stateObs$);
expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith(
config, config,
'authCallback id token expired' 'authCallback id token expired'
@ -832,12 +847,18 @@ describe('State Validation Service', () => {
it('should return invalid result if validateStateFromHashCallback is false', async () => { it('should return invalid result if validateStateFromHashCallback is false', async () => {
const readSpy = vi.spyOn(storagePersistenceService, 'read'); const readSpy = vi.spyOn(storagePersistenceService, 'read');
readSpy mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) readSpy,
.mockReturnValue(authWellKnownEndpoints); ['authWellKnownEndPoints', config],
readSpy () => authWellKnownEndpoints
.withArgs('authStateControl', config) );
.mockReturnValue('authStateControl');
mockImplementationWhenArgsEqual(
readSpy,
['authStateControl', config],
() => 'authStateControl'
);
vi.spyOn( vi.spyOn(
tokenValidationService, tokenValidationService,
'validateStateFromHashCallback' 'validateStateFromHashCallback'
@ -870,7 +891,7 @@ describe('State Validation Service', () => {
tokenValidationService.validateStateFromHashCallback tokenValidationService.validateStateFromHashCallback
).toHaveBeenCalled(); ).toHaveBeenCalled();
const state = await lastValueFrom(stateObs$); const state = await firstValueFrom(stateObs$);
expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith(
config, config,
'authCallback incorrect state' 'authCallback incorrect state'
@ -940,13 +961,23 @@ describe('State Validation Service', () => {
const readSpy = vi.spyOn(storagePersistenceService, 'read'); const readSpy = vi.spyOn(storagePersistenceService, 'read');
readSpy mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) readSpy,
.mockReturnValue(authWellKnownEndpoints); ['authWellKnownEndPoints', config],
readSpy () => authWellKnownEndpoints
.withArgs('authStateControl', config) );
.mockReturnValue('authStateControl');
readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); mockImplementationWhenArgsEqual(
readSpy,
['authStateControl', config],
() => 'authStateControl'
);
mockImplementationWhenArgsEqual(
readSpy,
['authNonce', config],
() => 'authNonce'
);
const callbackContext = { const callbackContext = {
code: 'fdffsdfsdf', code: 'fdffsdfsdf',
@ -967,7 +998,7 @@ describe('State Validation Service', () => {
config config
); );
const state = await lastValueFrom(stateObs$); const state = await firstValueFrom(stateObs$);
expect(state.accessToken).toBe('access_tokenTEST'); expect(state.accessToken).toBe('access_tokenTEST');
expect(state.idToken).toBe('id_tokenTEST'); expect(state.idToken).toBe('id_tokenTEST');
expect(state.decodedIdToken).toBe('decoded_id_token'); expect(state.decodedIdToken).toBe('decoded_id_token');
@ -990,12 +1021,16 @@ describe('State Validation Service', () => {
const readSpy = vi.spyOn(storagePersistenceService, 'read'); const readSpy = vi.spyOn(storagePersistenceService, 'read');
readSpy mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) readSpy,
.mockReturnValue(authWellKnownEndpoints); ['authWellKnownEndPoints', config],
readSpy () => authWellKnownEndpoints
.withArgs('authStateControl', config) );
.mockReturnValue('authStateControl'); mockImplementationWhenArgsEqual(
readSpy,
['authStateControl', config],
() => 'authStateControl'
);
const logDebugSpy = vi const logDebugSpy = vi
.spyOn(loggerService, 'logDebug') .spyOn(loggerService, 'logDebug')
.mockImplementation(() => undefined); .mockImplementation(() => undefined);
@ -1020,8 +1055,8 @@ describe('State Validation Service', () => {
config config
); );
const state = await lastValueFrom(stateObs$); const state = await firstValueFrom(stateObs$);
expect(logDebugSpy).toBeCalledWith([ expect(logDebugSpy.mock.calls).toEqual([
[config, 'authCallback Signature validation failed id_token'], [config, 'authCallback Signature validation failed id_token'],
[config, 'authCallback token(s) invalid'], [config, 'authCallback token(s) invalid'],
]); ]);
@ -1049,13 +1084,21 @@ describe('State Validation Service', () => {
); );
const readSpy = vi.spyOn(storagePersistenceService, 'read'); const readSpy = vi.spyOn(storagePersistenceService, 'read');
readSpy mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) readSpy,
.mockReturnValue(authWellKnownEndpoints); ['authWellKnownEndPoints', config],
readSpy () => authWellKnownEndpoints
.withArgs('authStateControl', config) );
.mockReturnValue('authStateControl'); mockImplementationWhenArgsEqual(
readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); readSpy,
['authStateControl', config],
() => 'authStateControl'
);
mockImplementationWhenArgsEqual(
readSpy,
['authNonce', config],
() => 'authNonce'
);
const logWarningSpy = vi const logWarningSpy = vi
.spyOn(loggerService, 'logWarning') .spyOn(loggerService, 'logWarning')
@ -1080,7 +1123,7 @@ describe('State Validation Service', () => {
config config
); );
const state = await lastValueFrom(stateObs$); const state = await firstValueFrom(stateObs$);
expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith(
config, config,
'authCallback incorrect nonce, did you call the checkAuth() method multiple times?' 'authCallback incorrect nonce, did you call the checkAuth() method multiple times?'
@ -1118,13 +1161,21 @@ describe('State Validation Service', () => {
).mockReturnValue(false); ).mockReturnValue(false);
const readSpy = vi.spyOn(storagePersistenceService, 'read'); const readSpy = vi.spyOn(storagePersistenceService, 'read');
readSpy mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) readSpy,
.mockReturnValue(authWellKnownEndpoints); ['authWellKnownEndPoints', config],
readSpy () => authWellKnownEndpoints
.withArgs('authStateControl', config) );
.mockReturnValue('authStateControl'); mockImplementationWhenArgsEqual(
readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); readSpy,
['authStateControl', config],
() => 'authStateControl'
);
mockImplementationWhenArgsEqual(
readSpy,
['authNonce', config],
() => 'authNonce'
);
const logDebugSpy = vi const logDebugSpy = vi
.spyOn(loggerService, 'logDebug') .spyOn(loggerService, 'logDebug')
.mockImplementation(() => undefined); .mockImplementation(() => undefined);
@ -1148,7 +1199,7 @@ describe('State Validation Service', () => {
config config
); );
const state = await lastValueFrom(stateObs$); const state = await firstValueFrom(stateObs$);
expect(logDebugSpy).toHaveBeenCalledWith( expect(logDebugSpy).toHaveBeenCalledWith(
config, config,
'authCallback Validation, one of the REQUIRED properties missing from id_token' 'authCallback Validation, one of the REQUIRED properties missing from id_token'
@ -1193,13 +1244,21 @@ describe('State Validation Service', () => {
config.maxIdTokenIatOffsetAllowedInSeconds = 0; config.maxIdTokenIatOffsetAllowedInSeconds = 0;
const readSpy = vi.spyOn(storagePersistenceService, 'read'); const readSpy = vi.spyOn(storagePersistenceService, 'read');
readSpy mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) readSpy,
.mockReturnValue(authWellKnownEndpoints); ['authWellKnownEndPoints', config],
readSpy () => authWellKnownEndpoints
.withArgs('authStateControl', config) );
.mockReturnValue('authStateControl'); mockImplementationWhenArgsEqual(
readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); readSpy,
['authStateControl', config],
() => 'authStateControl'
);
mockImplementationWhenArgsEqual(
readSpy,
['authNonce', config],
() => 'authNonce'
);
const logWarningSpy = vi const logWarningSpy = vi
.spyOn(loggerService, 'logWarning') .spyOn(loggerService, 'logWarning')
.mockImplementation(() => undefined); .mockImplementation(() => undefined);
@ -1223,7 +1282,7 @@ describe('State Validation Service', () => {
config config
); );
const state = await lastValueFrom(stateObs$); const state = await firstValueFrom(stateObs$);
expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith(
config, config,
'authCallback Validation, iat rejected id_token was issued too far away from the current time' 'authCallback Validation, iat rejected id_token was issued too far away from the current time'
@ -1271,13 +1330,21 @@ describe('State Validation Service', () => {
); );
const readSpy = vi.spyOn(storagePersistenceService, 'read'); const readSpy = vi.spyOn(storagePersistenceService, 'read');
readSpy mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) readSpy,
.mockReturnValue(authWellKnownEndpoints); ['authWellKnownEndPoints', config],
readSpy () => authWellKnownEndpoints
.withArgs('authStateControl', config) );
.mockReturnValue('authStateControl'); mockImplementationWhenArgsEqual(
readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); readSpy,
['authStateControl', config],
() => 'authStateControl'
);
mockImplementationWhenArgsEqual(
readSpy,
['authNonce', config],
() => 'authNonce'
);
const logWarningSpy = vi const logWarningSpy = vi
.spyOn(loggerService, 'logWarning') .spyOn(loggerService, 'logWarning')
.mockImplementation(() => undefined); .mockImplementation(() => undefined);
@ -1301,7 +1368,7 @@ describe('State Validation Service', () => {
config config
); );
const state = await lastValueFrom(stateObs$); const state = await firstValueFrom(stateObs$);
expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith(
config, config,
'authCallback incorrect iss does not match authWellKnownEndpoints issuer' 'authCallback incorrect iss does not match authWellKnownEndpoints issuer'
@ -1339,11 +1406,21 @@ describe('State Validation Service', () => {
config.maxIdTokenIatOffsetAllowedInSeconds = 0; config.maxIdTokenIatOffsetAllowedInSeconds = 0;
const readSpy = vi.spyOn(storagePersistenceService, 'read'); const readSpy = vi.spyOn(storagePersistenceService, 'read');
readSpy.withArgs('authWellKnownEndPoints', config).mockReturnValue(null); mockImplementationWhenArgsEqual(
readSpy readSpy,
.withArgs('authStateControl', config) ['authWellKnownEndPoints', config],
.mockReturnValue('authStateControl'); () => null
readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); );
mockImplementationWhenArgsEqual(
readSpy,
['authStateControl', config],
() => 'authStateControl'
);
mockImplementationWhenArgsEqual(
readSpy,
['authNonce', config],
() => 'authNonce'
);
const logWarningSpy = vi const logWarningSpy = vi
.spyOn(loggerService, 'logWarning') .spyOn(loggerService, 'logWarning')
.mockImplementation(() => undefined); .mockImplementation(() => undefined);
@ -1367,7 +1444,7 @@ describe('State Validation Service', () => {
config config
); );
const state = await lastValueFrom(stateObs$); const state = await firstValueFrom(stateObs$);
expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith(
config, config,
'authWellKnownEndpoints is undefined' 'authWellKnownEndpoints is undefined'
@ -1414,13 +1491,21 @@ describe('State Validation Service', () => {
config.clientId = ''; config.clientId = '';
const readSpy = vi.spyOn(storagePersistenceService, 'read'); const readSpy = vi.spyOn(storagePersistenceService, 'read');
readSpy mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) readSpy,
.mockReturnValue(authWellKnownEndpoints); ['authWellKnownEndPoints', config],
readSpy () => authWellKnownEndpoints
.withArgs('authStateControl', config) );
.mockReturnValue('authStateControl'); mockImplementationWhenArgsEqual(
readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); readSpy,
['authStateControl', config],
() => 'authStateControl'
);
mockImplementationWhenArgsEqual(
readSpy,
['authNonce', config],
() => 'authNonce'
);
const logWarningSpy = vi const logWarningSpy = vi
.spyOn(loggerService, 'logWarning') .spyOn(loggerService, 'logWarning')
.mockImplementation(() => undefined); .mockImplementation(() => undefined);
@ -1444,7 +1529,7 @@ describe('State Validation Service', () => {
config config
); );
const state = await lastValueFrom(stateObs$); const state = await firstValueFrom(stateObs$);
expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith(
config, config,
'authCallback incorrect aud' 'authCallback incorrect aud'
@ -1494,13 +1579,21 @@ describe('State Validation Service', () => {
config.clientId = ''; config.clientId = '';
const readSpy = vi.spyOn(storagePersistenceService, 'read'); const readSpy = vi.spyOn(storagePersistenceService, 'read');
readSpy mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) readSpy,
.mockReturnValue(authWellKnownEndpoints); ['authWellKnownEndPoints', config],
readSpy () => authWellKnownEndpoints
.withArgs('authStateControl', config) );
.mockReturnValue('authStateControl'); mockImplementationWhenArgsEqual(
readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); readSpy,
['authStateControl', config],
() => 'authStateControl'
);
mockImplementationWhenArgsEqual(
readSpy,
['authNonce', config],
() => 'authNonce'
);
const logWarningSpy = vi const logWarningSpy = vi
.spyOn(loggerService, 'logWarning') .spyOn(loggerService, 'logWarning')
.mockImplementation(() => undefined); .mockImplementation(() => undefined);
@ -1524,7 +1617,7 @@ describe('State Validation Service', () => {
config config
); );
const state = await lastValueFrom(stateObs$); const state = await firstValueFrom(stateObs$);
expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith(
config, config,
'authCallback missing azp' 'authCallback missing azp'
@ -1579,13 +1672,21 @@ describe('State Validation Service', () => {
config.clientId = ''; config.clientId = '';
const readSpy = vi.spyOn(storagePersistenceService, 'read'); const readSpy = vi.spyOn(storagePersistenceService, 'read');
readSpy mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) readSpy,
.mockReturnValue(authWellKnownEndpoints); ['authWellKnownEndPoints', config],
readSpy () => authWellKnownEndpoints
.withArgs('authStateControl', config) );
.mockReturnValue('authStateControl'); mockImplementationWhenArgsEqual(
readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); readSpy,
['authStateControl', config],
() => 'authStateControl'
);
mockImplementationWhenArgsEqual(
readSpy,
['authNonce', config],
() => 'authNonce'
);
const logWarningSpy = vi const logWarningSpy = vi
.spyOn(loggerService, 'logWarning') .spyOn(loggerService, 'logWarning')
.mockImplementation(() => undefined); .mockImplementation(() => undefined);
@ -1609,7 +1710,7 @@ describe('State Validation Service', () => {
config config
); );
const state = await lastValueFrom(stateObs$); const state = await firstValueFrom(stateObs$);
expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith(
config, config,
'authCallback incorrect azp' 'authCallback incorrect azp'
@ -1668,13 +1769,21 @@ describe('State Validation Service', () => {
config.clientId = ''; config.clientId = '';
const readSpy = vi.spyOn(storagePersistenceService, 'read'); const readSpy = vi.spyOn(storagePersistenceService, 'read');
readSpy mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) readSpy,
.mockReturnValue(authWellKnownEndpoints); ['authWellKnownEndPoints', config],
readSpy () => authWellKnownEndpoints
.withArgs('authStateControl', config) );
.mockReturnValue('authStateControl'); mockImplementationWhenArgsEqual(
readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); readSpy,
['authStateControl', config],
() => 'authStateControl'
);
mockImplementationWhenArgsEqual(
readSpy,
['authNonce', config],
() => 'authNonce'
);
const logWarningSpy = vi const logWarningSpy = vi
.spyOn(loggerService, 'logWarning') .spyOn(loggerService, 'logWarning')
.mockImplementation(() => undefined); .mockImplementation(() => undefined);
@ -1698,7 +1807,7 @@ describe('State Validation Service', () => {
config config
); );
const state = await lastValueFrom(stateObs$); const state = await firstValueFrom(stateObs$);
expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith(
config, config,
'authCallback pre, post id_token claims do not match in refresh' 'authCallback pre, post id_token claims do not match in refresh'
@ -1769,13 +1878,21 @@ describe('State Validation Service', () => {
config.autoCleanStateAfterAuthentication = false; config.autoCleanStateAfterAuthentication = false;
const readSpy = vi.spyOn(storagePersistenceService, 'read'); const readSpy = vi.spyOn(storagePersistenceService, 'read');
readSpy mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) readSpy,
.mockReturnValue(authWellKnownEndpoints); ['authWellKnownEndPoints', config],
readSpy () => authWellKnownEndpoints
.withArgs('authStateControl', config) );
.mockReturnValue('authStateControl'); mockImplementationWhenArgsEqual(
readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); readSpy,
['authStateControl', config],
() => 'authStateControl'
);
mockImplementationWhenArgsEqual(
readSpy,
['authNonce', config],
() => 'authNonce'
);
const logDebugSpy = vi const logDebugSpy = vi
.spyOn(loggerService, 'logDebug') .spyOn(loggerService, 'logDebug')
@ -1801,7 +1918,7 @@ describe('State Validation Service', () => {
config config
); );
const state = await lastValueFrom(stateObs$); const state = await firstValueFrom(stateObs$);
expect(logDebugSpy).toHaveBeenCalledWith( expect(logDebugSpy).toHaveBeenCalledWith(
config, config,
'authCallback token(s) validated, continue' 'authCallback token(s) validated, continue'
@ -1875,13 +1992,21 @@ describe('State Validation Service', () => {
const readSpy = vi.spyOn(storagePersistenceService, 'read'); const readSpy = vi.spyOn(storagePersistenceService, 'read');
readSpy mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) readSpy,
.mockReturnValue(authWellKnownEndpoints); ['authWellKnownEndPoints', config],
readSpy () => authWellKnownEndpoints
.withArgs('authStateControl', config) );
.mockReturnValue('authStateControl'); mockImplementationWhenArgsEqual(
readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); readSpy,
['authStateControl', config],
() => 'authStateControl'
);
mockImplementationWhenArgsEqual(
readSpy,
['authNonce', config],
() => 'authNonce'
);
const logWarningSpy = vi const logWarningSpy = vi
.spyOn(loggerService, 'logWarning') .spyOn(loggerService, 'logWarning')
@ -1906,7 +2031,7 @@ describe('State Validation Service', () => {
config config
); );
const state = await lastValueFrom(stateObs$); const state = await firstValueFrom(stateObs$);
expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith( expect(logWarningSpy).toHaveBeenCalledExactlyOnceWith(
config, config,
'authCallback incorrect at_hash' 'authCallback incorrect at_hash'
@ -1974,13 +2099,21 @@ describe('State Validation Service', () => {
config.responseType = 'id_token token'; config.responseType = 'id_token token';
const readSpy = vi.spyOn(storagePersistenceService, 'read'); const readSpy = vi.spyOn(storagePersistenceService, 'read');
readSpy mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) readSpy,
.mockReturnValue(authWellKnownEndpoints); ['authWellKnownEndPoints', config],
readSpy () => authWellKnownEndpoints
.withArgs('authStateControl', config) );
.mockReturnValue('authStateControl'); mockImplementationWhenArgsEqual(
readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); readSpy,
['authStateControl', config],
() => 'authStateControl'
);
mockImplementationWhenArgsEqual(
readSpy,
['authNonce', config],
() => 'authNonce'
);
const logDebugSpy = vi.spyOn(loggerService, 'logDebug'); // .mockImplementation(() => undefined); const logDebugSpy = vi.spyOn(loggerService, 'logDebug'); // .mockImplementation(() => undefined);
@ -2003,8 +2136,9 @@ describe('State Validation Service', () => {
config config
); );
const state = await lastValueFrom(stateObs$); const state = await firstValueFrom(stateObs$);
expect(logDebugSpy).toBeCalledWith([
expect(logDebugSpy.mock.calls).toEqual([
[config, 'iss validation is turned off, this is not recommended!'], [config, 'iss validation is turned off, this is not recommended!'],
[config, 'authCallback token(s) validated, continue'], [config, 'authCallback token(s) validated, continue'],
]); ]);
@ -2060,13 +2194,21 @@ describe('State Validation Service', () => {
const readSpy = vi.spyOn(storagePersistenceService, 'read'); const readSpy = vi.spyOn(storagePersistenceService, 'read');
readSpy mockImplementationWhenArgsEqual(
.withArgs('authWellKnownEndPoints', config) readSpy,
.mockReturnValue(authWellKnownEndpoints); ['authWellKnownEndPoints', config],
readSpy () => authWellKnownEndpoints
.withArgs('authStateControl', config) );
.mockReturnValue('authStateControl'); mockImplementationWhenArgsEqual(
readSpy.withArgs('authNonce', config).mockReturnValue('authNonce'); readSpy,
['authStateControl', config],
() => 'authStateControl'
);
mockImplementationWhenArgsEqual(
readSpy,
['authNonce', config],
() => 'authNonce'
);
const callbackContext = { const callbackContext = {
code: 'fdffsdfsdf', code: 'fdffsdfsdf',
@ -2088,7 +2230,7 @@ describe('State Validation Service', () => {
config config
); );
const state = await lastValueFrom(stateObs$); const state = await firstValueFrom(stateObs$);
expect(state.accessToken).toBe('access_tokenTEST'); expect(state.accessToken).toBe('access_tokenTEST');
expect(state.idToken).toBe(''); expect(state.idToken).toBe('');
expect(state.decodedIdToken).toBeDefined(); expect(state.decodedIdToken).toBeDefined();
@ -2127,7 +2269,7 @@ describe('State Validation Service', () => {
config config
); );
const isValid = await lastValueFrom(isValidObs$); const isValid = await firstValueFrom(isValidObs$);
expect(isValid.state).toBe(ValidationResult.Ok); expect(isValid.state).toBe(ValidationResult.Ok);
expect(isValid.authResponseIsValid).toBe(true); expect(isValid.authResponseIsValid).toBe(true);
}); });
@ -2164,7 +2306,7 @@ describe('State Validation Service', () => {
config config
); );
const isValid = await lastValueFrom(isValidObs$); const isValid = await firstValueFrom(isValidObs$);
expect(isValid.state).toBe(ValidationResult.Ok); expect(isValid.state).toBe(ValidationResult.Ok);
expect(isValid.authResponseIsValid).toBe(true); expect(isValid.authResponseIsValid).toBe(true);
}); });
@ -2201,7 +2343,7 @@ describe('State Validation Service', () => {
config config
); );
const isValid = await lastValueFrom(isValidObs$); const isValid = await firstValueFrom(isValidObs$);
expect(isValid.state).toBe(ValidationResult.Ok); expect(isValid.state).toBe(ValidationResult.Ok);
expect(isValid.authResponseIsValid).toBe(true); expect(isValid.authResponseIsValid).toBe(true);
}); });

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing'; import { TestBed } from '@/testing';
import { lastValueFrom, of } from 'rxjs'; import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest'; import { vi } from 'vitest';
import { JwkExtractor } from '../extractors/jwk.extractor'; import { JwkExtractor } from '../extractors/jwk.extractor';
import { LoggerService } from '../logging/logger.service'; import { LoggerService } from '../logging/logger.service';
@ -503,7 +503,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' } { configId: 'configId1' }
); );
const valueFalse = await lastValueFrom(valueFalse$); const valueFalse = await firstValueFrom(valueFalse$);
expect(valueFalse).toEqual(false); expect(valueFalse).toEqual(false);
}); });
@ -514,7 +514,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' } { configId: 'configId1' }
); );
const valueFalse = await lastValueFrom(valueFalse$); const valueFalse = await firstValueFrom(valueFalse$);
expect(valueFalse).toEqual(true); expect(valueFalse).toEqual(true);
}); });
@ -525,7 +525,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' } { configId: 'configId1' }
); );
const valueFalse = await lastValueFrom(valueFalse$); const valueFalse = await firstValueFrom(valueFalse$);
expect(valueFalse).toEqual(false); expect(valueFalse).toEqual(false);
}); });
@ -542,7 +542,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' } { configId: 'configId1' }
); );
const valueFalse = await lastValueFrom(valueFalse$); const valueFalse = await firstValueFrom(valueFalse$);
expect(valueFalse).toEqual(false); expect(valueFalse).toEqual(false);
}); });
@ -561,7 +561,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' } { configId: 'configId1' }
); );
const valueFalse = await lastValueFrom(valueFalse$); const valueFalse = await firstValueFrom(valueFalse$);
expect(valueFalse).toEqual(false); expect(valueFalse).toEqual(false);
}); });
@ -597,7 +597,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' } { configId: 'configId1' }
); );
const valueFalse = await lastValueFrom(valueFalse$); const valueFalse = await firstValueFrom(valueFalse$);
expect(valueFalse).toEqual(false); expect(valueFalse).toEqual(false);
}); });
@ -634,7 +634,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' } { configId: 'configId1' }
); );
const valueTrue = await lastValueFrom(valueTrue$); const valueTrue = await firstValueFrom(valueTrue$);
expect(valueTrue).toEqual(true); expect(valueTrue).toEqual(true);
}); });
}); });
@ -651,7 +651,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' } { configId: 'configId1' }
); );
const result = await lastValueFrom(result$); const result = await firstValueFrom(result$);
expect(result).toEqual(true); expect(result).toEqual(true);
}); });
@ -660,7 +660,7 @@ describe('TokenValidationService', () => {
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1jNTdkTzZRR1RWQndhTmsifQ.eyJleHAiOjE1ODkyMTAwODYsIm5iZiI6MTU4OTIwNjQ4NiwidmVyIjoiMS4wIiwiaXNzIjoiaHR0cHM6Ly9kYW1pZW5ib2QuYjJjbG9naW4uY29tL2EwOTU4ZjQ1LTE5NWItNDAzNi05MjU5LWRlMmY3ZTU5NGRiNi92Mi4wLyIsInN1YiI6ImY4MzZmMzgwLTNjNjQtNDgwMi04ZGJjLTAxMTk4MWMwNjhmNSIsImF1ZCI6ImYxOTM0YTZlLTk1OGQtNDE5OC05ZjM2LTYxMjdjZmM0Y2RiMyIsIm5vbmNlIjoiMDA3YzQxNTNiNmEwNTE3YzBlNDk3NDc2ZmIyNDk5NDhlYzVjbE92UVEiLCJpYXQiOjE1ODkyMDY0ODYsImF1dGhfdGltZSI6MTU4OTIwNjQ4NiwibmFtZSI6ImRhbWllbmJvZCIsImVtYWlscyI6WyJkYW1pZW5AZGFtaWVuYm9kLm9ubWljcm9zb2Z0LmNvbSJdLCJ0ZnAiOiJCMkNfMV9iMmNwb2xpY3lkYW1pZW4iLCJhdF9oYXNoIjoiWmswZktKU19wWWhPcE04SUJhMTJmdyJ9.E5Z-0kOzNU7LBkeVHHMyNoER8TUapGzUUfXmW6gVu4v6QMM5fQ4sJ7KC8PHh8lBFYiCnaDiTtpn3QytUwjXEFnLDAX5qcZT1aPoEgL_OmZMC-8y-4GyHp35l7VFD4iNYM9fJmLE8SYHTVl7eWPlXSyz37Ip0ciiV0Fd6eoksD_aVc-hkIqngDfE4fR8ZKfv4yLTNN_SfknFfuJbZ56yN-zIBL4GkuHsbQCBYpjtWQ62v98p1jO7NhHKV5JP2ec_Ge6oYc_bKTrE6OIX38RJ2rIm7zU16mtdjnl_350Nw3ytHcTPnA1VpP_VLElCfe83jr5aDHc_UQRYaAcWlOgvmVg'; 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ilg1ZVhrNHh5b2pORnVtMWtsMll0djhkbE5QNC1jNTdkTzZRR1RWQndhTmsifQ.eyJleHAiOjE1ODkyMTAwODYsIm5iZiI6MTU4OTIwNjQ4NiwidmVyIjoiMS4wIiwiaXNzIjoiaHR0cHM6Ly9kYW1pZW5ib2QuYjJjbG9naW4uY29tL2EwOTU4ZjQ1LTE5NWItNDAzNi05MjU5LWRlMmY3ZTU5NGRiNi92Mi4wLyIsInN1YiI6ImY4MzZmMzgwLTNjNjQtNDgwMi04ZGJjLTAxMTk4MWMwNjhmNSIsImF1ZCI6ImYxOTM0YTZlLTk1OGQtNDE5OC05ZjM2LTYxMjdjZmM0Y2RiMyIsIm5vbmNlIjoiMDA3YzQxNTNiNmEwNTE3YzBlNDk3NDc2ZmIyNDk5NDhlYzVjbE92UVEiLCJpYXQiOjE1ODkyMDY0ODYsImF1dGhfdGltZSI6MTU4OTIwNjQ4NiwibmFtZSI6ImRhbWllbmJvZCIsImVtYWlscyI6WyJkYW1pZW5AZGFtaWVuYm9kLm9ubWljcm9zb2Z0LmNvbSJdLCJ0ZnAiOiJCMkNfMV9iMmNwb2xpY3lkYW1pZW4iLCJhdF9oYXNoIjoiWmswZktKU19wWWhPcE04SUJhMTJmdyJ9.E5Z-0kOzNU7LBkeVHHMyNoER8TUapGzUUfXmW6gVu4v6QMM5fQ4sJ7KC8PHh8lBFYiCnaDiTtpn3QytUwjXEFnLDAX5qcZT1aPoEgL_OmZMC-8y-4GyHp35l7VFD4iNYM9fJmLE8SYHTVl7eWPlXSyz37Ip0ciiV0Fd6eoksD_aVc-hkIqngDfE4fR8ZKfv4yLTNN_SfknFfuJbZ56yN-zIBL4GkuHsbQCBYpjtWQ62v98p1jO7NhHKV5JP2ec_Ge6oYc_bKTrE6OIX38RJ2rIm7zU16mtdjnl_350Nw3ytHcTPnA1VpP_VLElCfe83jr5aDHc_UQRYaAcWlOgvmVg';
const atHash = 'bad'; const atHash = 'bad';
const result = await lastValueFrom( const result = await firstValueFrom(
tokenValidationService.validateIdTokenAtHash( tokenValidationService.validateIdTokenAtHash(
accessToken, accessToken,
atHash, atHash,
@ -688,7 +688,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' } { configId: 'configId1' }
); );
const result = await lastValueFrom(result$); const result = await firstValueFrom(result$);
expect(result).toEqual(true); expect(result).toEqual(true);
}); });
@ -704,7 +704,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' } { configId: 'configId1' }
); );
const result = await lastValueFrom(result$); const result = await firstValueFrom(result$);
expect(result).toEqual(false); expect(result).toEqual(false);
}); });
@ -720,7 +720,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' } { configId: 'configId1' }
); );
const result = await lastValueFrom(result$); const result = await firstValueFrom(result$);
expect(result).toEqual(false); expect(result).toEqual(false);
}); });
}); });

View File

@ -390,7 +390,8 @@ export class TokenValidationService {
localState: any, localState: any,
configuration: OpenIdConfiguration configuration: OpenIdConfiguration
): boolean { ): boolean {
if ((state as string) !== (localState as string)) { console.error(state, localState, `${state}`, `${localState}`);
if (`${state}` !== `${localState}`) {
this.loggerService.logDebug( this.loggerService.logDebug(
configuration, configuration,
`ValidateStateFromHashCallback failed, state: ${state} local_state:${localState}` `ValidateStateFromHashCallback failed, state: ${state} local_state:${localState}`

View File

@ -1,437 +0,0 @@
import { test, expect, type Page } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('https://demo.playwright.dev/todomvc');
});
const TODO_ITEMS = [
'buy some cheese',
'feed the cat',
'book a doctors appointment'
] as const;
test.describe('New Todo', () => {
test('should allow me to add todo items', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create 1st todo.
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press('Enter');
// Make sure the list only has one todo item.
await expect(page.getByTestId('todo-title')).toHaveText([
TODO_ITEMS[0]
]);
// Create 2nd todo.
await newTodo.fill(TODO_ITEMS[1]);
await newTodo.press('Enter');
// Make sure the list now has two todo items.
await expect(page.getByTestId('todo-title')).toHaveText([
TODO_ITEMS[0],
TODO_ITEMS[1]
]);
await checkNumberOfTodosInLocalStorage(page, 2);
});
test('should clear text input field when an item is added', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create one todo item.
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press('Enter');
// Check that input is empty.
await expect(newTodo).toBeEmpty();
await checkNumberOfTodosInLocalStorage(page, 1);
});
test('should append new items to the bottom of the list', async ({ page }) => {
// Create 3 items.
await createDefaultTodos(page);
// create a todo count locator
const todoCount = page.getByTestId('todo-count')
// Check test using different methods.
await expect(page.getByText('3 items left')).toBeVisible();
await expect(todoCount).toHaveText('3 items left');
await expect(todoCount).toContainText('3');
await expect(todoCount).toHaveText(/3/);
// Check all items in one call.
await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS);
await checkNumberOfTodosInLocalStorage(page, 3);
});
});
test.describe('Mark all as completed', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
await checkNumberOfTodosInLocalStorage(page, 3);
});
test.afterEach(async ({ page }) => {
await checkNumberOfTodosInLocalStorage(page, 3);
});
test('should allow me to mark all items as completed', async ({ page }) => {
// Complete all todos.
await page.getByLabel('Mark all as complete').check();
// Ensure all todos have 'completed' class.
await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']);
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
});
test('should allow me to clear the complete state of all items', async ({ page }) => {
const toggleAll = page.getByLabel('Mark all as complete');
// Check and then immediately uncheck.
await toggleAll.check();
await toggleAll.uncheck();
// Should be no completed classes.
await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']);
});
test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => {
const toggleAll = page.getByLabel('Mark all as complete');
await toggleAll.check();
await expect(toggleAll).toBeChecked();
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
// Uncheck first todo.
const firstTodo = page.getByTestId('todo-item').nth(0);
await firstTodo.getByRole('checkbox').uncheck();
// Reuse toggleAll locator and make sure its not checked.
await expect(toggleAll).not.toBeChecked();
await firstTodo.getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
// Assert the toggle all is checked again.
await expect(toggleAll).toBeChecked();
});
});
test.describe('Item', () => {
test('should allow me to mark items as complete', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create two items.
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
// Check first item.
const firstTodo = page.getByTestId('todo-item').nth(0);
await firstTodo.getByRole('checkbox').check();
await expect(firstTodo).toHaveClass('completed');
// Check second item.
const secondTodo = page.getByTestId('todo-item').nth(1);
await expect(secondTodo).not.toHaveClass('completed');
await secondTodo.getByRole('checkbox').check();
// Assert completed class.
await expect(firstTodo).toHaveClass('completed');
await expect(secondTodo).toHaveClass('completed');
});
test('should allow me to un-mark items as complete', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create two items.
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
const firstTodo = page.getByTestId('todo-item').nth(0);
const secondTodo = page.getByTestId('todo-item').nth(1);
const firstTodoCheckbox = firstTodo.getByRole('checkbox');
await firstTodoCheckbox.check();
await expect(firstTodo).toHaveClass('completed');
await expect(secondTodo).not.toHaveClass('completed');
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await firstTodoCheckbox.uncheck();
await expect(firstTodo).not.toHaveClass('completed');
await expect(secondTodo).not.toHaveClass('completed');
await checkNumberOfCompletedTodosInLocalStorage(page, 0);
});
test('should allow me to edit an item', async ({ page }) => {
await createDefaultTodos(page);
const todoItems = page.getByTestId('todo-item');
const secondTodo = todoItems.nth(1);
await secondTodo.dblclick();
await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]);
await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter');
// Explicitly assert the new text value.
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
'buy some sausages',
TODO_ITEMS[2]
]);
await checkTodosInLocalStorage(page, 'buy some sausages');
});
});
test.describe('Editing', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
await checkNumberOfTodosInLocalStorage(page, 3);
});
test('should hide other controls when editing', async ({ page }) => {
const todoItem = page.getByTestId('todo-item').nth(1);
await todoItem.dblclick();
await expect(todoItem.getByRole('checkbox')).not.toBeVisible();
await expect(todoItem.locator('label', {
hasText: TODO_ITEMS[1],
})).not.toBeVisible();
await checkNumberOfTodosInLocalStorage(page, 3);
});
test('should save edits on blur', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur');
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
'buy some sausages',
TODO_ITEMS[2],
]);
await checkTodosInLocalStorage(page, 'buy some sausages');
});
test('should trim entered text', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages ');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
'buy some sausages',
TODO_ITEMS[2],
]);
await checkTodosInLocalStorage(page, 'buy some sausages');
});
test('should remove the item if an empty text string was entered', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
TODO_ITEMS[2],
]);
});
test('should cancel edits on escape', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape');
await expect(todoItems).toHaveText(TODO_ITEMS);
});
});
test.describe('Counter', () => {
test('should display the current number of todo items', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// create a todo count locator
const todoCount = page.getByTestId('todo-count')
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press('Enter');
await expect(todoCount).toContainText('1');
await newTodo.fill(TODO_ITEMS[1]);
await newTodo.press('Enter');
await expect(todoCount).toContainText('2');
await checkNumberOfTodosInLocalStorage(page, 2);
});
});
test.describe('Clear completed button', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
});
test('should display the correct text', async ({ page }) => {
await page.locator('.todo-list li .toggle').first().check();
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();
});
test('should remove completed items when clicked', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).getByRole('checkbox').check();
await page.getByRole('button', { name: 'Clear completed' }).click();
await expect(todoItems).toHaveCount(2);
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
});
test('should be hidden when there are no items that are completed', async ({ page }) => {
await page.locator('.todo-list li .toggle').first().check();
await page.getByRole('button', { name: 'Clear completed' }).click();
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden();
});
});
test.describe('Persistence', () => {
test('should persist its data', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
const todoItems = page.getByTestId('todo-item');
const firstTodoCheck = todoItems.nth(0).getByRole('checkbox');
await firstTodoCheck.check();
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
await expect(firstTodoCheck).toBeChecked();
await expect(todoItems).toHaveClass(['completed', '']);
// Ensure there is 1 completed item.
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
// Now reload.
await page.reload();
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
await expect(firstTodoCheck).toBeChecked();
await expect(todoItems).toHaveClass(['completed', '']);
});
});
test.describe('Routing', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
// make sure the app had a chance to save updated todos in storage
// before navigating to a new view, otherwise the items can get lost :(
// in some frameworks like Durandal
await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
});
test('should allow me to display active items', async ({ page }) => {
const todoItem = page.getByTestId('todo-item');
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole('link', { name: 'Active' }).click();
await expect(todoItem).toHaveCount(2);
await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
});
test('should respect the back button', async ({ page }) => {
const todoItem = page.getByTestId('todo-item');
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await test.step('Showing all items', async () => {
await page.getByRole('link', { name: 'All' }).click();
await expect(todoItem).toHaveCount(3);
});
await test.step('Showing active items', async () => {
await page.getByRole('link', { name: 'Active' }).click();
});
await test.step('Showing completed items', async () => {
await page.getByRole('link', { name: 'Completed' }).click();
});
await expect(todoItem).toHaveCount(1);
await page.goBack();
await expect(todoItem).toHaveCount(2);
await page.goBack();
await expect(todoItem).toHaveCount(3);
});
test('should allow me to display completed items', async ({ page }) => {
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole('link', { name: 'Completed' }).click();
await expect(page.getByTestId('todo-item')).toHaveCount(1);
});
test('should allow me to display all items', async ({ page }) => {
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole('link', { name: 'Active' }).click();
await page.getByRole('link', { name: 'Completed' }).click();
await page.getByRole('link', { name: 'All' }).click();
await expect(page.getByTestId('todo-item')).toHaveCount(3);
});
test('should highlight the currently applied filter', async ({ page }) => {
await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected');
//create locators for active and completed links
const activeLink = page.getByRole('link', { name: 'Active' });
const completedLink = page.getByRole('link', { name: 'Completed' });
await activeLink.click();
// Page change - active items.
await expect(activeLink).toHaveClass('selected');
await completedLink.click();
// Page change - completed items.
await expect(completedLink).toHaveClass('selected');
});
});
async function createDefaultTodos(page: Page) {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
for (const item of TODO_ITEMS) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
}
async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) {
return await page.waitForFunction(e => {
return JSON.parse(localStorage['react-todos']).length === e;
}, expected);
}
async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) {
return await page.waitForFunction(e => {
return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e;
}, expected);
}
async function checkTodosInLocalStorage(page: Page, title: string) {
return await page.waitForFunction(t => {
return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t);
}, title);
}

View File

@ -19,6 +19,7 @@
}, },
"files": [], "files": [],
"include": [], "include": [],
"exclude": ["node_modules"],
"references": [ "references": [
{ {
"path": "./tsconfig.lib.json" "path": "./tsconfig.lib.json"

View File

@ -4,6 +4,8 @@
"rootDir": ".", "rootDir": ".",
"outDir": "./dist/tsc-lib", "outDir": "./dist/tsc-lib",
"lib": ["dom", "es2018"], "lib": ["dom", "es2018"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"paths": { "paths": {
"injection-js": ["./node_modules/injection-js/lib/index.ts"] "injection-js": ["./node_modules/injection-js/lib/index.ts"]
} }

View File

@ -6,6 +6,8 @@
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"outDir": "./dist/tsc-test", "outDir": "./dist/tsc-test",
"types": ["vitest/globals", "node"], "types": ["vitest/globals", "node"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"paths": { "paths": {
"@/testing": ["./src/testing"], "@/testing": ["./src/testing"],
"@/testing/*": ["./src/testing/*"], "@/testing/*": ["./src/testing/*"],

View File

@ -1,17 +1,29 @@
import swc from 'unplugin-swc';
import tsconfigPaths from 'vite-tsconfig-paths'; import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
export default defineConfig({ export default defineConfig({
cacheDir: '.vitest', cacheDir: '.vitest',
test: { test: {
include: ['src/**/*.spec.ts', 'tests-examples'], setupFiles: ['src/testing/init-test.ts'],
environment: 'jsdom',
include: ['src/**/*.spec.ts'],
globals: true, globals: true,
browser: { restoreMocks: true,
provider: 'playwright', // or 'webdriverio' // browser: {
enabled: true, // provider: 'playwright', // or 'webdriverio'
// at least one instance is required // enabled: true,
instances: [{ browser: 'chromium' }], // instances: [{ browser: 'chromium' }],
}, // },
}, },
plugins: [tsconfigPaths({})], plugins: [
tsconfigPaths(),
swc.vite({
include: /\.[mc]?[jt]sx?$/,
exclude: [
/node_modules\/(?!injection-js|\.pnpm)/,
/node_modules\/\.pnpm\/(?!injection-js)/,
] as any,
}),
],
}); });