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() {
this.oidcSecurityService.authorize();
this.oidcSecurityService.authorize().subscribe();
}
logout() {

View File

@ -26,15 +26,14 @@
"scripts": {
"build": "rslib build",
"dev": "rslib build --watch",
"test": "vitest --browser.headless",
"test-ci": "vitest --watch=false --browser.headless --coverage",
"test": "vitest --coverage",
"test-ci": "vitest --watch=false --coverage",
"pack": "npm run build && npm pack ./dist",
"publish": "npm run build && npm publish ./dist",
"coverage": "vitest run --coverage",
"lint": "ultracite lint",
"format": "ultracite format",
"cli": "tsx scripts/cli.ts",
"test:browser": "vitest --workspace=vitest.workspace.ts"
"cli": "tsx scripts/cli.ts"
},
"dependencies": {
"@ngify/http": "^2.0.4",
@ -51,11 +50,14 @@
"@evilmartians/lefthook": "^1.0.3",
"@playwright/test": "^1.49.1",
"@rslib/core": "^0.3.1",
"@swc/core": "^1.10.12",
"@types/jsdom": "^21.1.7",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.12.0",
"@vitest/browser": "^3.0.4",
"@vitest/coverage-v8": "^3.0.4",
"commander": "^13.1.0",
"jsdom": "^26.0.0",
"lodash-es": "^4.17.21",
"oxc-parser": "^0.48.1",
"oxc-walker": "^0.2.2",
@ -65,9 +67,9 @@
"tsx": "^4.19.2",
"typescript": "^5.7.3",
"ultracite": "^4.1.15",
"unplugin-swc": "^1.5.1",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.4",
"webdriverio": "^9.7.2"
"vitest": "^3.0.4"
},
"keywords": [
"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
import { Command } from 'commander';
import { rewriteAllObservableSubscribeToLastValueFrom } from './code-transform';
import { rewriteAllObservableSubscribeTofirstValueFrom } from './code-transform';
const program = new Command();
@ -13,7 +13,7 @@ program
.command('rewrite <pattern>')
.description('Rewrite files matching the given glob pattern')
.action(async (pattern: string) => {
await rewriteAllObservableSubscribeToLastValueFrom(pattern);
await rewriteAllObservableSubscribeTofirstValueFrom(pattern);
});
program.parse(process.argv);

View File

@ -1,11 +1,11 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
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 () => {
const actual = await rewriteObservableSubscribeToLastValueFrom(
const actual = await rewriteObservableSubscribeTofirstValueFrom(
'index.ts',
`refreshSessionIframeService
.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);`;
const biome = await Biome.create({
@ -34,7 +34,7 @@ describe('rewriteSpecObservableSubscribeToLastValueFrom', () => {
});
it('should rewrite complex exmaple to valid string', async () => {
const actual = await rewriteObservableSubscribeToLastValueFrom(
const actual = await rewriteObservableSubscribeTofirstValueFrom(
'index.ts',
`codeFlowCallbackService
.authenticatedCallbackWithCode('some-url4', config, [config])
@ -56,7 +56,7 @@ describe('rewriteSpecObservableSubscribeToLastValueFrom', () => {
const expect = `
try {
const abc = await lastValueFrom(codeFlowCallbackService.authenticatedCallbackWithCode('some-url4', config, [config]));
const abc = await firstValueFrom(codeFlowCallbackService.authenticatedCallbackWithCode('some-url4', config, [config]));
expect(abc).toBeTruthy();
} catch (err: any) {
if (err instanceof EmptyError) {

View File

@ -21,7 +21,7 @@ function sourceTextFromNode(
return magicString.getSourceText(start, end);
}
export async function rewriteObservableSubscribeToLastValueFrom(
export async function rewriteObservableSubscribeTofirstValueFrom(
filename: string,
content?: string
) {
@ -83,7 +83,7 @@ export async function rewriteObservableSubscribeToLastValueFrom(
error = args[1];
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) {
const nextParam =
@ -161,12 +161,12 @@ export async function rewriteObservableSubscribeToLastValueFrom(
return result;
}
export async function rewriteAllObservableSubscribeToLastValueFrom(
export async function rewriteAllObservableSubscribeTofirstValueFrom(
pattern: string | string[]
) {
const files = fsp.glob(pattern);
for await (const file of files) {
const result = await rewriteObservableSubscribeToLastValueFrom(file);
const result = await rewriteObservableSubscribeTofirstValueFrom(file);
await fsp.writeFile(file, result, 'utf-8');
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
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 { catchError, mergeMap, retryWhen, switchMap } from 'rxjs/operators';
import { DataService } from '../../api/data.service';
@ -116,7 +116,7 @@ export class CodeFlowCallbackHandlerService {
switchMap((response) => {
if (response) {
const authResult: AuthResult = {
...response,
...(response as any),
state: callbackContext.state,
session_state: callbackContext.sessionState,
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -247,15 +247,15 @@ describe('Flows Data Service', () => {
});
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 = {
silentRenewTimeoutInSeconds: 10,
configId: 'configId1',
};
vi.useRealTimers();
vi.useFakeTimers();
const baseTime = new Date();
vi.useFakeTimers();
vi.setSystemTime(baseTime);
@ -271,7 +271,7 @@ describe('Flows Data Service', () => {
);
const spyWrite = vi.spyOn(storagePersistenceService, 'write');
vi.advanceTimersByTimeAsync(
await vi.advanceTimersByTimeAsync(
(config.silentRenewTimeoutInSeconds + 1) * 1000
);

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import type { HttpFeature, HttpInterceptor } from '@ngify/http';
import { InjectionToken } from 'injection-js';
export { HttpParams, HttpParamsOptions } from './params';
export const HTTP_INTERCEPTORS = new InjectionToken<readonly HttpInterceptor[]>(
'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 { lastValueFrom, of } from 'rxjs';
import { skip } from 'rxjs/operators';
import { ReplaySubject, firstValueFrom, of } from 'rxjs';
import { share, skip } from 'rxjs/operators';
import { vi } from 'vitest';
import { LoggerService } from '../logging/logger.service';
import { OidcSecurityService } from '../oidc.security.service';
@ -333,7 +333,7 @@ describe('CheckSessionService', () => {
});
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 dateNow = new Date();
const lastRefresh = dateNow.setMinutes(dateNow.getMinutes() + 30);
@ -341,7 +341,7 @@ describe('CheckSessionService', () => {
serviceAsAny.lastIFrameRefresh = lastRefresh;
serviceAsAny.iframeRefreshInterval = lastRefresh;
const result = await lastValueFrom(serviceAsAny.init());
const result = await firstValueFrom(serviceAsAny.init());
expect(result).toBeUndefined();
});
});
@ -366,18 +366,27 @@ describe('CheckSessionService', () => {
describe('checkSessionChanged$', () => {
it('emits when internal event is thrown', async () => {
const result = await lastValueFrom(
checkSessionService.checkSessionChanged$.pipe(skip(1))
const test$ = checkSessionService.checkSessionChanged$.pipe(
skip(1),
share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: true,
})
);
expect(result).toBe(true);
test$.subscribe();
const serviceAsAny = checkSessionService as any;
serviceAsAny.checkSessionChangedInternal$.next(true);
const result = await firstValueFrom(test$);
expect(result).toBe(true);
});
it('emits false initially', async () => {
const result = await lastValueFrom(
const result = await firstValueFrom(
checkSessionService.checkSessionChanged$
);
expect(result).toBe(false);
@ -387,7 +396,7 @@ describe('CheckSessionService', () => {
const expectedResultsInOrder = [false, true];
let counter = 0;
const result = await lastValueFrom(
const result = await firstValueFrom(
checkSessionService.checkSessionChanged$
);
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 { take } from 'rxjs/operators';
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
@Injectable()
export class CheckSessionService implements OnDestroy {
export class CheckSessionService {
private readonly loggerService = inject(LoggerService);
private readonly storagePersistenceService = inject(
@ -25,8 +25,6 @@ export class CheckSessionService implements OnDestroy {
private readonly eventService = inject(PublicEventsService);
private readonly zone = inject(NgZone);
private readonly document = inject(DOCUMENT);
private checkSessionReceived = false;
@ -54,7 +52,7 @@ export class CheckSessionService implements OnDestroy {
return this.checkSessionChangedInternal$.asObservable();
}
ngOnDestroy(): void {
[Symbol.dispose]() {
this.stop();
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()) {
return of();
return of(undefined);
}
const authWellKnownEndPoints = this.storagePersistenceService.read(
@ -120,7 +118,7 @@ export class CheckSessionService implements OnDestroy {
'CheckSession - init check session: authWellKnownEndpoints is undefined. Returning.'
);
return of();
return of(undefined);
}
const existingIframe = this.getOrCreateIframe(configuration);
@ -138,7 +136,7 @@ export class CheckSessionService implements OnDestroy {
'CheckSession - init check session: checkSessionIframe is not configured to run'
);
return of();
return of(undefined);
}
if (contentWindow) {
@ -228,14 +226,12 @@ export class CheckSessionService implements OnDestroy {
);
}
this.zone.runOutsideAngular(() => {
this.scheduledHeartBeatRunning =
this.document?.defaultView?.setTimeout(
() => this.zone.run(pollServerSessionRecur),
pollServerSessionRecur,
this.heartBeatInterval
) ?? null;
});
});
};
pollServerSessionRecur();

View File

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

View File

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

View File

@ -1,5 +1,12 @@
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 { AuthStateService } from '../auth-state/auth-state.service';
import { ImplicitFlowCallbackService } from '../callback/implicit-flow-callback.service';
@ -28,6 +35,7 @@ describe('SilentRenewService ', () => {
let intervalService: IntervalService;
beforeEach(() => {
vi.useFakeTimers();
TestBed.configureTestingModule({
providers: [
SilentRenewService,
@ -54,6 +62,11 @@ describe('SilentRenewService ', () => {
intervalService = TestBed.inject(IntervalService);
});
// biome-ignore lint/correctness/noUndeclaredVariables: <explanation>
afterEach(() => {
vi.useRealTimers();
});
it('should create', () => {
expect(silentRenewService).toBeTruthy();
});
@ -149,7 +162,7 @@ describe('SilentRenewService ', () => {
const urlParts =
'code=some-code&state=some-state&session_state=some-session-state';
await lastValueFrom(
await firstValueFrom(
silentRenewService.codeFlowCallbackSilentRenewIframe(
[url, urlParts],
config,
@ -188,7 +201,7 @@ describe('SilentRenewService ', () => {
const urlParts = 'error=some_error';
try {
await lastValueFrom(
await firstValueFrom(
silentRenewService.codeFlowCallbackSilentRenewIframe(
[url, urlParts],
config,
@ -312,19 +325,31 @@ describe('SilentRenewService ', () => {
const eventData = { detail: 'detail?detail2' } as CustomEvent;
const allConfigs = [{ configId: 'configId1' }];
const result = await lastValueFrom(
silentRenewService.refreshSessionWithIFrameCompleted$
const test$ = silentRenewService.refreshSessionWithIFrameCompleted$.pipe(
share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: true,
})
);
expect(result).toEqual({
refreshToken: 'callbackContext',
} as CallbackContext);
test$.subscribe();
await firstValueFrom(
silentRenewService.silentRenewEventHandler(
eventData,
allConfigs[0]!,
allConfigs
)
);
await vi.advanceTimersByTimeAsync(1000);
const result = await firstValueFrom(test$);
expect(result).toEqual({
refreshToken: 'callbackContext',
} as CallbackContext);
});
it('loggs and calls flowsDataService.resetSilentRenewRunning in case of an error', async () => {
@ -341,10 +366,12 @@ describe('SilentRenewService ', () => {
const allConfigs = [{ configId: 'configId1' }];
const eventData = { detail: 'detail?detail2' } as CustomEvent;
await firstValueFrom(
silentRenewService.silentRenewEventHandler(
eventData,
allConfigs[0]!,
allConfigs
)
);
await vi.advanceTimersByTimeAsync(1000);
expect(resetSilentRenewRunningSpy).toHaveBeenCalledTimes(1);
@ -360,17 +387,28 @@ describe('SilentRenewService ', () => {
const eventData = { detail: 'detail?detail2' } as CustomEvent;
const allConfigs = [{ configId: 'configId1' }];
const result = await lastValueFrom(
silentRenewService.refreshSessionWithIFrameCompleted$
const test$ = silentRenewService.refreshSessionWithIFrameCompleted$.pipe(
share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: true,
})
);
expect(result).toBeNull();
test$.subscribe();
await firstValueFrom(
silentRenewService.silentRenewEventHandler(
eventData,
allConfigs[0]!,
allConfigs
)
);
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 { type Observable, Subject, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Injectable, inject } from 'injection-js';
import { type Observable, Subject, of, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { AuthStateService } from '../auth-state/auth-state.service';
import { ImplicitFlowCallbackService } from '../callback/implicit-flow-callback.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 { FlowsService } from '../flows/flows.service';
import { ResetAuthDataService } from '../flows/reset-auth-data.service';
import { HttpParams } from '../http';
import { LoggerService } from '../logging/logger.service';
import { FlowHelper } from '../utils/flowHelper/flow-helper.service';
import { ValidationResult } from '../validation/validation-result';
@ -70,8 +70,9 @@ export class SilentRenewService {
config: OpenIdConfiguration,
allConfigs: OpenIdConfiguration[]
): Observable<CallbackContext> {
// TODO: fix @ngify/http
const params = new HttpParams(urlParts[1] || undefined);
const params = new HttpParams({
fromString: urlParts[1],
});
const errorParam = params.get('error');
@ -120,10 +121,10 @@ export class SilentRenewService {
e: CustomEvent,
config: OpenIdConfiguration,
allConfigs: OpenIdConfiguration[]
): void {
): Observable<undefined> {
this.loggerService.logDebug(config, 'silentRenewEventHandler');
if (!e.detail) {
return;
return of(undefined);
}
let callback$: Observable<CallbackContext>;
@ -146,17 +147,19 @@ export class SilentRenewService {
);
}
callback$.subscribe({
next: (callbackContext) => {
return callback$.pipe(
map((callbackContext) => {
this.refreshSessionWithIFrameCompletedInternal$.next(callbackContext);
this.flowsDataService.resetSilentRenewRunning(config);
},
error: (err: unknown) => {
return undefined;
}),
catchError((err: unknown) => {
this.loggerService.logError(config, `Error: ${err}`);
this.refreshSessionWithIFrameCompletedInternal$.next(null);
this.flowsDataService.resetSilentRenewRunning(config);
},
});
return of(undefined);
})
);
}
private getExistingIframe(): HTMLIFrameElement | null {

View File

@ -1,6 +1,6 @@
// Public classes.
export { PassedInitialConfig } from './auth-config';
export type { PassedInitialConfig } from './auth-config';
export * from './auth-options';
export * from './auth-state/auth-result';
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 { injectAbstractType } from './inject';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { Injectable, inject } from 'injection-js';
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 { CheckAuthService } from '../../auth-state/check-auth.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!');
return;
return of(undefined);
}
this.loggerService.logDebug(
@ -55,14 +55,13 @@ export class ParLoginService {
'BEGIN Authorize OIDC Flow, no auth data'
);
const result$ = this.authWellKnownService
return this.authWellKnownService
.queryAndStoreAuthWellKnownEndPoints(configuration)
.pipe(
switchMap(() =>
this.parService.postParRequest(configuration, authOptions)
),
map(() => {
(response) => {
map((response) => {
this.loggerService.logDebug(
configuration,
'par response: ',
@ -74,11 +73,7 @@ export class ParLoginService {
configuration
);
this.loggerService.logDebug(
configuration,
'par request url: ',
url
);
this.loggerService.logDebug(configuration, 'par request url: ', url);
if (!url) {
this.loggerService.logError(
@ -94,14 +89,8 @@ export class ParLoginService {
} else {
this.redirectService.redirectTo(url);
}
};
}),
shareReplay(1)
})
);
result$.subscribe();
return result$;
}
loginWithPopUpPar(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { Injectable, inject } from 'injection-js';
import type { Observable } from 'rxjs';
import { concatMap, map, shareReplay } from 'rxjs/operators';
import { BehaviorSubject, type Observable } from 'rxjs';
import { concatMap, map, switchMap } from 'rxjs/operators';
import type { AuthOptions, LogoutAuthOptions } from './auth-options';
import type { AuthenticatedResult } from './auth-state/auth-result';
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 { UserService } from './user-data/user.service';
import type { UserDataResult } from './user-data/userdata-result';
import { MockUtil } from './utils/reflect/index';
import { TokenHelperService } from './utils/tokenHelper/token-helper.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
*/
@MockUtil({ implementation: () => new BehaviorSubject(undefined) })
checkAuthMultiple(url?: string): Observable<LoginResponse[]> {
return this.configurationService
.getOpenIDConfigurations()
@ -336,16 +339,11 @@ export class OidcSecurityService {
* @param authOptions The custom options for the the authentication request.
*/
authorize(configId?: string, authOptions?: AuthOptions): Observable<void> {
const result$ = this.configurationService
return this.configurationService
.getOpenIDConfiguration(configId)
.pipe(
map((config) => this.loginService.login(config, authOptions)),
shareReplay(1)
switchMap((config) => this.loginService.login(config, authOptions))
);
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
*/
logoffLocal(configId?: string): Observable<void> {
const result$ = this.configurationService
.getOpenIDConfigurations(configId)
.pipe(
map(({ allConfigs, currentConfig }) =>
this.logoffRevocationService.logoffLocal(currentConfig, allConfigs)
),
shareReplay(1)
logoffLocal(configId?: string): Observable<undefined> {
return this.configurationService.getOpenIDConfigurations(configId).pipe(
map(({ allConfigs, currentConfig }) => {
this.logoffRevocationService.logoffLocal(currentConfig, allConfigs);
return undefined;
})
);
result$.subscribe();
return result$;
}
/**
@ -476,16 +470,13 @@ export class OidcSecurityService {
* Use this method if you have _multiple_ configs enabled.
*/
logoffLocalMultiple(): Observable<void> {
const result$ = this.configurationService.getOpenIDConfigurations().pipe(
return this.configurationService
.getOpenIDConfigurations()
.pipe(
map(({ allConfigs }) =>
this.logoffRevocationService.logoffLocalMultiple(allConfigs)
),
shareReplay(1)
)
);
result$.subscribe();
return result$;
}
/**

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@ describe('BrowserStorageService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
BrowserStorageService,
mockProvider(LoggerService),
{
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 { injectAbstractType } from '../injection';
import { LoggerService } from '../logging/logger.service';
import { AbstractSecurityStorage } from './abstract-security-storage';
@ -7,7 +8,9 @@ import { AbstractSecurityStorage } from './abstract-security-storage';
export class BrowserStorageService {
private readonly loggerService = inject(LoggerService);
private readonly abstractSecurityStorage = inject(AbstractSecurityStorage);
private readonly abstractSecurityStorage = injectAbstractType(
AbstractSecurityStorage
);
read(key: string, configuration: OpenIdConfiguration): any {
const { configId } = configuration;

View File

@ -18,7 +18,8 @@ describe('DefaultLocalStorageService', () => {
describe('read', () => {
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');
@ -28,7 +29,8 @@ describe('DefaultLocalStorageService', () => {
describe('write', () => {
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');
@ -38,7 +40,8 @@ describe('DefaultLocalStorageService', () => {
describe('remove', () => {
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');
@ -48,7 +51,8 @@ describe('DefaultLocalStorageService', () => {
describe('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();

View File

@ -18,7 +18,8 @@ describe('DefaultSessionStorageService', () => {
describe('read', () => {
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');
@ -28,7 +29,8 @@ describe('DefaultSessionStorageService', () => {
describe('write', () => {
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');
@ -38,7 +40,8 @@ describe('DefaultSessionStorageService', () => {
describe('remove', () => {
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');
@ -48,7 +51,8 @@ describe('DefaultSessionStorageService', () => {
describe('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();

View File

@ -1,4 +1,4 @@
import { TestBed } from '@/testing';
import { TestBed, mockImplementationWhenArgsEqual } from '@/testing';
import { vi } from 'vitest';
import { mockProvider } from '../testing/mock';
import { BrowserStorageService } from './browser-storage.service';
@ -10,7 +10,10 @@ describe('Storage Persistence Service', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [mockProvider(BrowserStorageService)],
providers: [
StoragePersistenceService,
mockProvider(BrowserStorageService),
],
});
service = TestBed.inject(StoragePersistenceService);
securityStorage = TestBed.inject(BrowserStorageService);
@ -239,10 +242,16 @@ describe('Storage Persistence Service', () => {
};
const spy = vi.spyOn(securityStorage, 'read');
spy
.withArgs('reusable_refresh_token', config)
.mockReturnValue(returnValue);
spy.withArgs('authnResult', config).mockReturnValue(undefined);
mockImplementationWhenArgsEqual(
spy,
['reusable_refresh_token', config],
() => returnValue
);
mockImplementationWhenArgsEqual(
spy,
['authnResult', config],
() => undefined
);
const result = service.getRefreshToken(config);
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:
// 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> => {
const fetchData = vi.fn()('fetchData');
export const createRetriableStream = (...resp$: any[]): Observable<any> => {
const fetchData = vi.fn();
fetchData.mockReturnValues(...resp$);
for (const r of resp$) {
fetchData.mockReturnValueOnce(r);
}
return of(null).pipe(switchMap((_) => fetchData()));
};

View File

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

View File

@ -1,6 +1,10 @@
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 allMethods = keys.filter((key) => {
try {
@ -14,9 +18,16 @@ export function mockClass<T>(obj: new (...args: any[]) => T): any {
const mockedClass = class T {};
for (const method of allMethods) {
(mockedClass.prototype as any)[method] = (): void => {
const mockImplementation = Reflect.getMetadata(
'mock:implementation',
obj.prototype,
method
);
(mockedClass.prototype as any)[method] =
mockImplementation ??
((): any => {
return;
};
});
}
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 {
provide: obj,
provide: token ?? obj,
useClass: mockClass(obj),
};
}

View File

@ -1,23 +1,31 @@
import type { Provider } from 'injection-js';
import { JSDOM } from 'jsdom';
import { AbstractRouter, type Navigation, type UrlTree } from 'oidc-client-rx';
export class MockRouter extends AbstractRouter {
dom = new JSDOM('', {
url: 'http://localhost',
});
navigation: Navigation = {
id: 1,
extras: {},
initialUrl: new URL('https://localhost/'),
extractedUrl: new URL('https://localhost/'),
initialUrl: this.parseUrl(this.dom.window.location.href),
extractedUrl: this.parseUrl(this.dom.window.location.href),
trigger: 'imperative',
previousNavigation: null,
};
navigateByUrl(url: string): void {
const prevNavigation = this.navigation;
this.dom.reconfigure({
url: new URL(url, this.dom.window.location.href).href,
});
this.navigation = {
id: prevNavigation.id + 1,
extras: {},
initialUrl: prevNavigation.initialUrl,
extractedUrl: new URL(url),
extractedUrl: this.parseUrl(this.dom.window.location.href),
trigger: prevNavigation.trigger,
previousNavigation: prevNavigation,
};
@ -26,7 +34,8 @@ export class MockRouter extends AbstractRouter {
return this.navigation;
}
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>>(
mockInstance: M,
whenArgs: (
...args: Parameters<M extends MockInstance<infer T> ? T : never>
) => boolean,
implementation: Exclude<ReturnType<M['getMockImplementation']>, undefined>
): M {
const spyImpl = mockInstance.getMockImplementation()!;
type Procedure = (...args: any[]) => any;
type Methods<T> = keyof {
[K in keyof T as T[K] extends Procedure ? K : never]: T[K];
};
type Classes<T> = {
[K in keyof T]: T[K] extends new (...args: any[]) => any ? K : never;
}[keyof T] &
(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) => {
if (isEqual(args, whenArgs)) {
if (whenArgs(...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>(
obj: T,
propertyKey: K,
accessType: 'get' | 'set' = 'get',
mockImplementation?: any
accessType: 'get' | 'set' = 'get'
) {
const originalDescriptor = Object.getOwnPropertyDescriptor(obj, propertyKey);
if (!originalDescriptor) {
throw new Error(
`Property ${String(propertyKey)} does not exist on the object.`
);
const ownDescriptor = Object.getOwnPropertyDescriptor(obj, propertyKey);
let finalDescriptor: PropertyDescriptor | undefined;
let currentObj = obj;
while (currentObj) {
finalDescriptor = Object.getOwnPropertyDescriptor(currentObj, propertyKey);
if (finalDescriptor) {
break;
}
currentObj = Object.getPrototypeOf(currentObj);
}
const spy = vi.fn();
let value: T[K] | undefined;
if (accessType === 'get') {
Object.defineProperty(obj, propertyKey, {
get: mockImplementation
? () => {
value = mockImplementation();
return value;
}
: spy,
get: spy,
configurable: true,
});
} else if (accessType === 'set') {
Object.defineProperty(obj, propertyKey, {
set: mockImplementation
? (next) => {
value = next;
}
: spy,
set: spy,
configurable: true,
});
}
// 恢复原始属性
spy.mockRestore = () => {
if (originalDescriptor) {
Object.defineProperty(obj, propertyKey, originalDescriptor);
if (ownDescriptor) {
Object.defineProperty(obj, propertyKey, ownDescriptor);
} else {
delete obj[propertyKey];
}

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { TestBed, mockImplementationWhenArgsEqual } from '@/testing';
import { lastValueFrom, of } from 'rxjs';
import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest';
import type { OpenIdConfiguration } from '../../config/openid-configuration';
import { FlowsDataService } from '../../flows/flows-data.service';
@ -1041,14 +1041,14 @@ describe('UrlService Tests', () => {
describe('getAuthorizeUrl', () => {
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();
});
it('returns null if current flow is code flow and no redirect url is defined', async () => {
vi.spyOn(flowHelper, 'isCurrentFlowCodeFlow').mockReturnValue(true);
const result = await lastValueFrom(
const result = await firstValueFrom(
service.getAuthorizeUrl({ configId: 'configId1' })
);
expect(result).toBeNull();
@ -1062,7 +1062,7 @@ describe('UrlService Tests', () => {
redirectUrl: 'some-redirectUrl',
} as OpenIdConfiguration;
const result = await lastValueFrom(service.getAuthorizeUrl(config));
const result = await firstValueFrom(service.getAuthorizeUrl(config));
expect(result).toBe('');
});
@ -1090,7 +1090,7 @@ describe('UrlService Tests', () => {
() => ({ authorizationEndpoint })
);
const result = await lastValueFrom(service.getAuthorizeUrl(config));
const result = await firstValueFrom(service.getAuthorizeUrl(config));
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'
);
@ -1107,7 +1107,7 @@ describe('UrlService Tests', () => {
'createUrlImplicitFlowAuthorize'
);
await lastValueFrom(service.getAuthorizeUrl({ configId: 'configId1' }));
await firstValueFrom(service.getAuthorizeUrl({ configId: 'configId1' }));
expect(spyCreateUrlCodeFlowAuthorize).not.toHaveBeenCalled();
expect(spyCreateUrlImplicitFlowAuthorize).toHaveBeenCalled();
});
@ -1119,18 +1119,20 @@ describe('UrlService Tests', () => {
.mockReturnValue('');
const resultObs$ = service.getAuthorizeUrl({ configId: 'configId1' });
const result = await lastValueFrom(resultObs$);
const result = await firstValueFrom(resultObs$);
expect(spy).toHaveBeenCalled();
expect(result).toBe('');
});
});
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);
const spy = vi.spyOn(service as any, 'createUrlCodeFlowWithSilentRenew');
service.getRefreshSessionSilentRenewUrl({ configId: 'configId1' });
await firstValueFrom(
service.getRefreshSessionSilentRenewUrl({ configId: 'configId1' })
);
expect(spy).toHaveBeenCalled();
});
@ -1159,7 +1161,7 @@ describe('UrlService Tests', () => {
configId: 'configId1',
});
const result = await lastValueFrom(resultObs$);
const result = await firstValueFrom(resultObs$);
expect(spy).toHaveBeenCalled();
expect(result).toBe('');
});
@ -1344,7 +1346,7 @@ describe('UrlService Tests', () => {
redirectUrl: '',
});
const result = await lastValueFrom(resultObs$);
const result = await firstValueFrom(resultObs$);
expect(result).toBe(null);
});
@ -1372,7 +1374,7 @@ describe('UrlService Tests', () => {
const resultObs$ = service.createBodyForParCodeFlowRequest(config);
const result = await lastValueFrom(resultObs$);
const result = await firstValueFrom(resultObs$);
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'
);
@ -1402,7 +1404,7 @@ describe('UrlService Tests', () => {
const resultObs$ = service.createBodyForParCodeFlowRequest(config);
const result = await lastValueFrom(resultObs$);
const result = await firstValueFrom(resultObs$);
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'
);
@ -1432,7 +1434,7 @@ describe('UrlService Tests', () => {
const resultObs$ = service.createBodyForParCodeFlowRequest(config);
const result = await lastValueFrom(resultObs$);
const result = await firstValueFrom(resultObs$);
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'
);
@ -1466,7 +1468,7 @@ describe('UrlService Tests', () => {
},
});
const result = await lastValueFrom(resultObs$);
const result = await firstValueFrom(resultObs$);
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'
);
@ -1596,7 +1598,7 @@ describe('UrlService Tests', () => {
const resultObs$ = serviceAsAny.createUrlCodeFlowWithSilentRenew(config);
const result = await lastValueFrom(resultObs$);
const result = await firstValueFrom(resultObs$);
expect(result).toBe('');
});
@ -1639,7 +1641,7 @@ describe('UrlService Tests', () => {
const resultObs$ = serviceAsAny.createUrlCodeFlowWithSilentRenew(config);
const result = await lastValueFrom(resultObs$);
const result = await firstValueFrom(resultObs$);
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`
);
@ -1680,7 +1682,7 @@ describe('UrlService Tests', () => {
const resultObs$ = serviceAsAny.createUrlCodeFlowWithSilentRenew(config);
const result = await lastValueFrom(resultObs$);
const result = await firstValueFrom(resultObs$);
expect(result).toBe('');
});
});
@ -1796,7 +1798,7 @@ describe('UrlService Tests', () => {
const resultObs$ = serviceAsAny.createUrlCodeFlowAuthorize(config);
const result = await lastValueFrom(resultObs$);
const result = await firstValueFrom(resultObs$);
expect(result).toBeNull();
});
@ -1838,7 +1840,7 @@ describe('UrlService Tests', () => {
const resultObs$ = serviceAsAny.createUrlCodeFlowAuthorize(config);
const result = await lastValueFrom(resultObs$);
const result = await firstValueFrom(resultObs$);
expect(result).toBe(
`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' },
});
const result = await lastValueFrom(resultObs$);
const result = await firstValueFrom(resultObs$);
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`
);
@ -1924,7 +1926,7 @@ describe('UrlService Tests', () => {
const resultObs$ = serviceAsAny.createUrlCodeFlowAuthorize(config);
const result = await lastValueFrom(resultObs$);
const result = await firstValueFrom(resultObs$);
expect(result).toBe('');
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { TestBed } from '@/testing';
import { lastValueFrom, of } from 'rxjs';
import { firstValueFrom, of } from 'rxjs';
import { vi } from 'vitest';
import { JwkExtractor } from '../extractors/jwk.extractor';
import { LoggerService } from '../logging/logger.service';
@ -503,7 +503,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' }
);
const valueFalse = await lastValueFrom(valueFalse$);
const valueFalse = await firstValueFrom(valueFalse$);
expect(valueFalse).toEqual(false);
});
@ -514,7 +514,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' }
);
const valueFalse = await lastValueFrom(valueFalse$);
const valueFalse = await firstValueFrom(valueFalse$);
expect(valueFalse).toEqual(true);
});
@ -525,7 +525,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' }
);
const valueFalse = await lastValueFrom(valueFalse$);
const valueFalse = await firstValueFrom(valueFalse$);
expect(valueFalse).toEqual(false);
});
@ -542,7 +542,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' }
);
const valueFalse = await lastValueFrom(valueFalse$);
const valueFalse = await firstValueFrom(valueFalse$);
expect(valueFalse).toEqual(false);
});
@ -561,7 +561,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' }
);
const valueFalse = await lastValueFrom(valueFalse$);
const valueFalse = await firstValueFrom(valueFalse$);
expect(valueFalse).toEqual(false);
});
@ -597,7 +597,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' }
);
const valueFalse = await lastValueFrom(valueFalse$);
const valueFalse = await firstValueFrom(valueFalse$);
expect(valueFalse).toEqual(false);
});
@ -634,7 +634,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' }
);
const valueTrue = await lastValueFrom(valueTrue$);
const valueTrue = await firstValueFrom(valueTrue$);
expect(valueTrue).toEqual(true);
});
});
@ -651,7 +651,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' }
);
const result = await lastValueFrom(result$);
const result = await firstValueFrom(result$);
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';
const atHash = 'bad';
const result = await lastValueFrom(
const result = await firstValueFrom(
tokenValidationService.validateIdTokenAtHash(
accessToken,
atHash,
@ -688,7 +688,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' }
);
const result = await lastValueFrom(result$);
const result = await firstValueFrom(result$);
expect(result).toEqual(true);
});
@ -704,7 +704,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' }
);
const result = await lastValueFrom(result$);
const result = await firstValueFrom(result$);
expect(result).toEqual(false);
});
@ -720,7 +720,7 @@ describe('TokenValidationService', () => {
{ configId: 'configId1' }
);
const result = await lastValueFrom(result$);
const result = await firstValueFrom(result$);
expect(result).toEqual(false);
});
});

View File

@ -390,7 +390,8 @@ export class TokenValidationService {
localState: any,
configuration: OpenIdConfiguration
): boolean {
if ((state as string) !== (localState as string)) {
console.error(state, localState, `${state}`, `${localState}`);
if (`${state}` !== `${localState}`) {
this.loggerService.logDebug(
configuration,
`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": [],
"include": [],
"exclude": ["node_modules"],
"references": [
{
"path": "./tsconfig.lib.json"

View File

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

View File

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

View File

@ -1,17 +1,29 @@
import swc from 'unplugin-swc';
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
export default defineConfig({
cacheDir: '.vitest',
test: {
include: ['src/**/*.spec.ts', 'tests-examples'],
setupFiles: ['src/testing/init-test.ts'],
environment: 'jsdom',
include: ['src/**/*.spec.ts'],
globals: true,
browser: {
provider: 'playwright', // or 'webdriverio'
enabled: true,
// at least one instance is required
instances: [{ browser: 'chromium' }],
restoreMocks: true,
// browser: {
// provider: 'playwright', // or 'webdriverio'
// enabled: true,
// 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,
}),
],
});