import { fireEvent, waitFor } from '@testing-library/react'; import { http, HttpResponse } from 'msw'; import { server } from '../__mocks__/server'; import { LOGIN_REQUIRED_EVENT } from '../ajax-handlers-login-required'; import { TokenServerAuth, TokenServerAuthError, TokenServerAuthOptions, WithMicroserviceContext, } from '../with-microservice'; class Page { get iframe() { return document.getElementsByTagName('iframe')[0]; } } describe(`[ajax-handlers] ${TokenServerAuth.name}`, () => { const MINUTE = 1000 * 60; const defaultAuthInfoURL = '/bff/authInfo'; const originalLocation = window.location; const page = new Page(); let context: WithMicroserviceContext; let options: TokenServerAuthOptions; let adapter: TokenServerAuth; let requestCounts: Record; // tracks how many times each request is made beforeAll(() => { Object.defineProperty(window, 'location', { configurable: true, value: { ...originalLocation, reload: jest.fn() }, }); server.listen(); server.events.on('request:start', ({ request }) => { const path = new URL(request.url).pathname; requestCounts[path] = (requestCounts[path] ?? 0) + 1; }); }); afterAll(() => { Object.defineProperty(window, 'location', { configurable: true, value: originalLocation, }); server.close(); server.events.removeAllListeners('request:start'); }); beforeEach(() => { jest.clearAllMocks(); window.sessionStorage.clear(); window.localStorage.clear(); context = { baseURL: '/mfe' }; options = {}; adapter = new TokenServerAuth(context, options); requestCounts = {}; server.use( http.get('/bff/authInfo', () => { return HttpResponse.text(undefined, { status: 500 }); }) ); }); afterEach(() => { server.resetHandlers(); document.querySelectorAll('iframe').forEach(iframe => iframe.remove()); }); describe('authenticate', () => { let data: Record; beforeEach(() => { data = { source: 'bff-silent-login', isLoggedId: true }; }); function fireLoginEvent(initiator?: string) { fireEvent( window, new MessageEvent('message', { data: { initiator, ...data }, origin: 'http://localhost', }) ); } const subject = async () => { const promise = adapter.authenticate(); await waitFor(() => { const authURL = context.authURL ?? `http://localhost${context.baseURL}/bff/silent-login`; expect(page.iframe.src).toMatch(new RegExp(`${authURL}\\?initiator=.+`)); }); const initiator = page.iframe.src.match(/initiator=(.+)/)?.[1]; fireLoginEvent(initiator); return promise; }; test('allows local network access in iframe', async () => { adapter.authenticate(); await waitFor(() => { expect(page.iframe.getAttribute('allow')).toBe('local-network-access *'); }); }); describe('with authURL', () => { beforeEach(() => { context.authURL = '/foo/bar'; }); test('uses specified authURL', async () => { adapter.authenticate(); await waitFor(() => { expect(page.iframe.src).toMatch( new RegExp(`${context.authURL}\\?initiator=.+`) ); }); }); }); describe('when custom authInfoURL endpoint provided', () => { beforeEach(() => { options.authInfoURL = '/foobar'; server.use( http.get(options.authInfoURL, () => HttpResponse.text(undefined, { status: 500 }) ) ); }); test('uses the specified authInfoURL', async () => { adapter.authenticate(); await waitFor(() => expect(page.iframe).toBeTruthy()); expect(requestCounts['/foobar']).toBe(1); }); }); function itCallsCustomErrorHandler( subject: Function, { message, data }: { message: string; data?: any } ) { describe('with custom error handler', () => { beforeEach(() => (options.onError = jest.fn())); test('calls error handler and does not reload page', async () => { await expect(subject()).rejects.toThrow(); expect(options.onError).toHaveBeenCalledWith( expect.any(Function), new Error(message) ); if (data) { const error: any = jest.mocked(options.onError)?.mock.calls[0][1]; expect(error.data).toEqual(data); } expect(window.location.reload).not.toHaveBeenCalled(); }); test('default error handler reloads page', async () => { await expect(subject()).rejects.toThrow(); jest.mocked(options.onError)?.mock.calls[0][0]?.( new TokenServerAuthError(message, {}) ); expect(window.location.reload).toHaveBeenCalled(); }); }); } describe('when authInfo endpoint returns isAuthenticated as false', () => { const message = 'Not authenticated'; const responseData = { isAuthenticated: false }; beforeEach(() => { server.use(http.get(defaultAuthInfoURL, () => HttpResponse.json(responseData))); }); test('throws error and reloads page', async () => { await expect(adapter.authenticate()).rejects.toThrow(message); expect(window.location.reload).toHaveBeenCalled(); }); itCallsCustomErrorHandler(() => adapter.authenticate(), { message, data: expect.objectContaining(responseData), }); }); describe('when authentication fails', () => { const message = 'Not logged in'; beforeEach(() => { jest.useFakeTimers({ doNotFake: ['queueMicrotask'] }); // doNotFake is required to work with MSW data.isLoggedIn = false; }); afterEach(() => { jest.useRealTimers(); }); function itReloadsThePage() { test('throws error and reloads page', async () => { await expect(subject()).rejects.toThrow(message); expect(window.location.reload).toHaveBeenCalled(); }); } itReloadsThePage(); itCallsCustomErrorHandler(subject, { message, data: expect.objectContaining({ isLoggedIn: false }), }); describe('when endpoint returns "login_required" error', () => { const dispatchSpy = jest.spyOn(window, 'dispatchEvent'); const loginRequiredEvent = expect.objectContaining({ type: LOGIN_REQUIRED_EVENT }); beforeEach(() => (data.errorCode = 'login_required')); function itDispatchesLoginRequiredEvent() { test('dispatches LOGIN_REQUIRED_EVENT instead of reloading page', async () => { await expect(subject()).rejects.toThrow(message); expect(window.location.reload).not.toHaveBeenCalled(); expect(dispatchSpy).toHaveBeenCalledWith(loginRequiredEvent); }); } itDispatchesLoginRequiredEvent(); describe('when authentication fails a second time', () => { beforeEach(async () => { await expect(subject()).rejects.toThrow(); dispatchSpy.mockClear(); }); test('does not dispatch event', async () => { await expect(subject()).rejects.toThrow(); expect(dispatchSpy).not.toHaveBeenCalledWith(loginRequiredEvent); }); describe('when last failure was one hour ago', () => { beforeEach(() => jest.setSystemTime(Date.now() + 60 * MINUTE + 1)); itDispatchesLoginRequiredEvent(); }); }); }); describe('when endpoint returns isAuthenticated as true', () => { const info = { idp: 'idp', sessionId: 'sessionId', subject: 'subject', tenantId: 'tenantId', }; beforeEach(() => { server.use( http.get(defaultAuthInfoURL, () => HttpResponse.json({ isAuthenticated: true, info }) ) ); }); test('visits silent-login with parameters from authInfo', async () => { const params = 'idp=idp&sid=sessionId&subject=subject&tenantId=tenantId&initiator=.+'; adapter.authenticate(); await waitFor(() => { const authURL = `http://localhost${context.baseURL}/bff/silent-login`; expect(page.iframe.src).toMatch(new RegExp(`${authURL}\\?${params}`)); }); }); }); describe('when authentication fails a second time', () => { beforeEach(async () => { await expect(subject()).rejects.toThrow(); jest.mocked(window.location.reload).mockClear(); }); test('does not reload the page', async () => { await expect(subject()).rejects.toThrow(); expect(window.location.reload).not.toHaveBeenCalled(); }); describe('when last failure was five minutes ago', () => { beforeEach(() => jest.setSystemTime(Date.now() + 5 * MINUTE + 1)); itReloadsThePage(); }); }); }); }); });