import { fireEvent, waitFor } from '@testing-library/dom'; import { http, HttpResponse } from 'msw'; import { server } from '../__mocks__/server'; import { silentLogin, SilentLoginOptions, TokenServerAuthError } from '../silent-login'; class Page { get iframe() { return document.getElementsByTagName('iframe')[0]; } } describe(`[ajax-handlers] silentLogin`, () => { const defaultAuthInfoURL = '/bff/authInfo'; const page = new Page(); let options: SilentLoginOptions; let requestCounts: Record; beforeAll(() => { server.listen(); server.events.on('request:start', ({ request }) => { const path = new URL(request.url).pathname; requestCounts[path] = (requestCounts[path] ?? 0) + 1; }); }); afterAll(() => { server.close(); server.events.removeAllListeners('request:start'); }); beforeEach(() => { options = { baseURL: '/mfe' }; requestCounts = {}; server.use( http.get(defaultAuthInfoURL, () => { return HttpResponse.text(undefined, { status: 500 }); }) ); }); afterEach(() => { server.resetHandlers(); document.querySelectorAll('iframe').forEach(iframe => iframe.remove()); }); function fireLoginEvent(initiator: string, isLoggedIn = true) { fireEvent( window, new MessageEvent('message', { data: { source: 'bff-silent-login', isLoggedIn, initiator }, origin: 'http://localhost', }) ); } const subject = async () => { const promise = silentLogin(options); await waitFor(() => { const authURL = `http://localhost${options.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('resolves on successful login', async () => { await expect(subject()).resolves.toBeUndefined(); }); test('creates a hidden iframe', async () => { silentLogin(options); await waitFor(() => { expect(page.iframe.getAttribute('hidden')).toBe(''); }); }); test('allows local network access in iframe', async () => { silentLogin(options); await waitFor(() => { expect(page.iframe.getAttribute('allow')).toBe('local-network-access *'); }); }); test('removes iframe after completion', async () => { await subject(); expect(page.iframe).toBeFalsy(); }); describe('when custom authURL is provided', () => { beforeEach(() => { options.authURL = '/foo/bar'; }); test('uses specified authURL', async () => { silentLogin(options); await waitFor(() => { expect(page.iframe.src).toMatch(new RegExp(`${options.authURL}\\?initiator=.+`)); }); }); }); describe('when custom authInfoURL is provided', () => { beforeEach(() => { options.authInfoURL = '/custom/authInfo'; server.use( http.get(options.authInfoURL, () => HttpResponse.text(undefined, { status: 500 })) ); }); test('uses the specified authInfoURL', async () => { silentLogin(options); await waitFor(() => expect(page.iframe).toBeTruthy()); expect(requestCounts[options.authInfoURL!]).toBe(1); }); }); describe('when authInfo 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=.+'; silentLogin(options); await waitFor(() => { const authURL = `http://localhost${options.baseURL}/bff/silent-login`; expect(page.iframe.src).toMatch(new RegExp(`${authURL}\\?${params}`)); }); }); }); function itRejectsWithTokenServerAuthError( subject: () => Promise, { message, data }: { message: string; data: Record } ) { test('rejects with TokenServerAuthError', async () => { await expect(subject()).rejects.toThrow(message); }); test('error contains response data', async () => { expect.hasAssertions(); try { await subject(); } catch (error) { expect(error).toBeInstanceOf(TokenServerAuthError); expect((error as TokenServerAuthError).data).toEqual(expect.objectContaining(data)); } }); } describe('when authInfo returns isAuthenticated as false', () => { const responseData = { isAuthenticated: false }; beforeEach(() => { server.use(http.get(defaultAuthInfoURL, () => HttpResponse.json(responseData))); }); itRejectsWithTokenServerAuthError(() => silentLogin(options), { message: 'Not authenticated', data: responseData, }); }); describe('when authInfo returns non-2xx with a JSON body', () => { beforeEach(() => { server.use( http.get(defaultAuthInfoURL, () => HttpResponse.json({ isAuthenticated: false }, { status: 500 }) ) ); }); test('falls back to silent login without params', async () => { silentLogin(options); await waitFor(() => { const authURL = `http://localhost${options.baseURL}/bff/silent-login`; expect(page.iframe.src).toMatch(new RegExp(`${authURL}\\?initiator=.+`)); expect(page.iframe.src).not.toMatch(/idp=/); }); }); }); describe('when login response indicates not logged in', () => { const responseData = { isLoggedIn: false, errorCode: 'login_required' }; function fireFailedLoginEvent(initiator: string) { fireEvent( window, new MessageEvent('message', { data: { source: 'bff-silent-login', ...responseData, initiator, }, origin: 'http://localhost', }) ); } const subject = async () => { const promise = silentLogin(options); await waitFor(() => { expect(page.iframe.src).toMatch(/initiator=.+/); }); const initiator = page.iframe.src.match(/initiator=(.+)/)?.[1]; fireFailedLoginEvent(initiator!); return promise; }; itRejectsWithTokenServerAuthError(subject, { message: 'Not logged in', data: responseData, }); }); describe('when timeout is reached', () => { beforeEach(() => { options.timeout = 1000; }); test('rejects with timeout error', async () => { const promise = silentLogin(options); // Wait for fetch to complete and iframe src to be set before faking timers await waitFor(() => { expect(page.iframe?.src).toMatch(/initiator=.+/); }); // doNotFake is required to work with MSW jest.useFakeTimers({ doNotFake: ['queueMicrotask', 'performance'] }); jest.advanceTimersByTime(1001); await expect(promise).rejects.toThrow('Authentication timed out'); jest.useRealTimers(); }); }); describe('when signal is provided', () => { const abortMessage = 'The operation was aborted.'; let controller: AbortController; beforeEach(() => { controller = new AbortController(); options.signal = controller.signal; }); describe('when aborted during login', () => { let promise: Promise; beforeEach(async () => { promise = silentLogin(options); await waitFor(() => expect(page.iframe).toBeTruthy()); controller.abort(); }); test('rejects with abort error', async () => { await expect(promise).rejects.toThrow(abortMessage); }); test('removes iframe', async () => { await promise.catch(() => {}); expect(page.iframe).toBeFalsy(); }); }); describe('when signal is already aborted', () => { beforeEach(() => { controller.abort(); }); test('rejects with abort error', async () => { await expect(silentLogin(options)).rejects.toThrow(abortMessage); }); }); }); });