import { TestBed, mockImplementationWhenArgsEqual } from '@/testing'; import { HttpErrorResponse, HttpHeaders } from '@ngify/http'; import { firstValueFrom, of, throwError } from 'rxjs'; import { vi } from 'vitest'; import { DataService } from '../../api/data.service'; import { LoggerService } from '../../logging/logger.service'; import { StoragePersistenceService } from '../../storage/storage-persistence.service'; import { createRetriableStream } from '../../testing/create-retriable-stream.helper'; import { mockProvider } from '../../testing/mock'; import { UrlService } from '../../utils/url/url.service'; import type { CallbackContext } from '../callback-context'; import { RefreshTokenCallbackHandlerService } from './refresh-token-callback-handler.service'; describe('RefreshTokenCallbackHandlerService', () => { let service: RefreshTokenCallbackHandlerService; let storagePersistenceService: StoragePersistenceService; let dataService: DataService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ RefreshTokenCallbackHandlerService, mockProvider(UrlService), mockProvider(LoggerService), mockProvider(DataService), mockProvider(StoragePersistenceService), ], }); service = TestBed.inject(RefreshTokenCallbackHandlerService); storagePersistenceService = TestBed.inject(StoragePersistenceService); dataService = TestBed.inject(DataService); }); it('should create', () => { expect(service).toBeTruthy(); }); describe('refreshTokensRequestTokens', () => { const HTTP_ERROR = new HttpErrorResponse({}); const CONNECTION_ERROR = new HttpErrorResponse({ error: new ProgressEvent('error'), status: 0, statusText: 'Unknown Error', url: 'https://identity-server.test/openid-connect/token', }); it('throws error if no tokenEndpoint is given', async () => { try { await firstValueFrom( (service as any).refreshTokensRequestTokens({} as CallbackContext) ); } catch (err: unknown) { expect(err).toBeTruthy(); } }); it('calls data service if all params are good', async () => { const postSpy = vi.spyOn(dataService, 'post').mockReturnValue(of({})); mockImplementationWhenArgsEqual( vi.spyOn(storagePersistenceService, 'read'), ['authWellKnownEndPoints', { configId: 'configId1' }], () => ({ tokenEndpoint: 'tokenEndpoint' }) ); await firstValueFrom( service.refreshTokensRequestTokens({} as CallbackContext, { configId: 'configId1', }) ); expect(postSpy).toHaveBeenCalledExactlyOnceWith( 'tokenEndpoint', undefined, { configId: 'configId1' }, expect.any(HttpHeaders) ); const httpHeaders = postSpy.mock.calls.at(-1)?.[3] as HttpHeaders; expect(httpHeaders.has('Content-Type')).toBeTruthy(); expect(httpHeaders.get('Content-Type')).toBe( 'application/x-www-form-urlencoded' ); }); it('calls data service with correct headers if all params are good', async () => { const postSpy = vi.spyOn(dataService, 'post').mockReturnValue(of({})); mockImplementationWhenArgsEqual( vi.spyOn(storagePersistenceService, 'read'), ['authWellKnownEndPoints', { configId: 'configId1' }], () => ({ tokenEndpoint: 'tokenEndpoint' }) ); await firstValueFrom( service.refreshTokensRequestTokens({} as CallbackContext, { configId: 'configId1', }) ); const httpHeaders = postSpy.mock.calls.at(-1)?.[3] as HttpHeaders; expect(httpHeaders.has('Content-Type')).toBeTruthy(); expect(httpHeaders.get('Content-Type')).toBe( 'application/x-www-form-urlencoded' ); }); it('returns error in case of http error', async () => { vi.spyOn(dataService, 'post').mockReturnValue( throwError(() => HTTP_ERROR) ); const config = { configId: 'configId1', authority: 'authority' }; mockImplementationWhenArgsEqual( vi.spyOn(storagePersistenceService, 'read'), ['authWellKnownEndPoints', config], () => ({ tokenEndpoint: 'tokenEndpoint' }) ); try { await firstValueFrom( service.refreshTokensRequestTokens({} as CallbackContext, config) ); } catch (err: any) { expect(err).toBeTruthy(); } }); it('retries request in case of no connection http error and succeeds', async () => { const postSpy = vi.spyOn(dataService, 'post').mockReturnValue( createRetriableStream( throwError(() => CONNECTION_ERROR), of({}) ) ); const config = { configId: 'configId1', authority: 'authority' }; mockImplementationWhenArgsEqual( vi.spyOn(storagePersistenceService, 'read'), ['authWellKnownEndPoints', config], () => ({ tokenEndpoint: 'tokenEndpoint' }) ); try { const res = await firstValueFrom( service.refreshTokensRequestTokens({} as CallbackContext, config) ); expect(res).toBeTruthy(); expect(postSpy).toHaveBeenCalledTimes(1); } catch (err: any) { expect(err).toBeFalsy(); } }); it('retries request in case of no connection http error and fails because of http error afterwards', async () => { const postSpy = vi.spyOn(dataService, 'post').mockReturnValue( createRetriableStream( throwError(() => CONNECTION_ERROR), throwError(() => HTTP_ERROR) ) ); const config = { configId: 'configId1', authority: 'authority' }; mockImplementationWhenArgsEqual( vi.spyOn(storagePersistenceService, 'read'), ['authWellKnownEndPoints', config], () => ({ tokenEndpoint: 'tokenEndpoint' }) ); try { const res = await firstValueFrom( service.refreshTokensRequestTokens({} as CallbackContext, config) ); expect(res).toBeFalsy(); } catch (err: any) { expect(err).toBeTruthy(); expect(postSpy).toHaveBeenCalledTimes(1); } }); }); });