import { symbolToken, useDependencies } from '@servicetitan/react-ioc'; import { fireEvent, renderHook, waitFor } from '@testing-library/react'; import axios, { AxiosRequestConfig, CanceledError } from 'axios'; import { HttpResponse, http } from 'msw'; import { FC, PropsWithChildren } from 'react'; import mock from 'xhr-mock'; import { server } from '../__mocks__/server'; import { TokenServerAuth, TokenServerAuthOptions, withMicroservice } from '../with-microservice'; class Page { get iframe() { return document.getElementsByTagName('iframe')[0]; } } describe(`[ajax-handlers] ${withMicroservice.name}`, () => { const page = new Page(); let options: TokenServerAuthOptions | undefined; let requestCounts: Record; // tracks how many times each request is made beforeAll(() => { mock.teardown(); 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 = undefined; requestCounts = {}; server.use( http.get('/bff/authInfo', () => { return HttpResponse.text(undefined, { status: 500 }); }) ); }); afterEach(() => { server.resetHandlers(); }); const UnwrappedComponent: FC = ({ children }) =>
{children}
; const BASE_URL_TOKEN = symbolToken('BASE_URL_TOKEN', false); const defaultBaseURL = '/mfe'; const defaultEndpoint = '/user'; const createComponentWithMicroservice = ({ baseURL = defaultBaseURL }: { baseURL?: string }) => withMicroservice({ authAdapter: options ? context => new TokenServerAuth(context, options) : 'token', baseURL, component: UnwrappedComponent, tokens: [BASE_URL_TOKEN], }); const useGetUser = ({ axiosRequestConfig, }: { axiosRequestConfig?: AxiosRequestConfig } = {}) => { const [baseURL] = useDependencies(BASE_URL_TOKEN); const request = async () => { const options = axiosRequestConfig ?? { baseURL, url: defaultEndpoint, method: 'GET', }; const result = await axios.request(options); return result.data; }; return request; }; async function simulateIframeMessage( baseURL: string, isLoggedIn: boolean, shouldFireEvent: boolean ) { await waitFor(() => { expect(page.iframe?.src).toMatch( new RegExp(`http://localhost${baseURL}/bff/silent-login\\?initiator=.+`) ); }); if (shouldFireEvent) { // Simulate the iframe postMessage const initiator = page.iframe.src.match(/initiator=(.+)/)?.[1]; fireEvent( window, new MessageEvent('message', { data: { source: 'bff-silent-login', isLoggedIn, initiator, }, origin: 'http://localhost', }) ); } else { jest.runOnlyPendingTimers(); } } it('authenticates before making a request', async () => { const hook = renderHook(() => useGetUser(), { wrapper: createComponentWithMicroservice({}), }); await waitFor(() => { expect(hook.result.current).not.toBeNull(); }); await waitFor(() => { expect(page.iframe?.src).toMatch( new RegExp(`http://localhost${defaultBaseURL}/bff/silent-login\\?initiator=.+`) ); }); }); describe('when an private api request is made', () => { const originalLocation = window.location; let isLoggedIn: boolean; let shouldFireEvent: boolean; interface SubjectOptions { baseURL?: string; isLoggedIn?: boolean; shouldFireEvent?: boolean; expectAuthTimes?: number; } const subject = async (options: SubjectOptions = {}) => { const baseURL = options.baseURL ?? defaultBaseURL; const hook = renderHook(() => useGetUser(), { wrapper: createComponentWithMicroservice({ baseURL, }), }); await waitFor(() => { expect(hook.result.current).not.toBeNull(); }); const getUser = hook.result.current; const getUserPromise = getUser(); const repeatTimes = options.expectAuthTimes ?? 1; for (let i = 0; i < repeatTimes; i++) { // eslint-disable-next-line no-await-in-loop await simulateIframeMessage( baseURL, options.isLoggedIn ?? isLoggedIn, options.shouldFireEvent ?? shouldFireEvent ); } return getUserPromise; }; beforeAll(() => { Object.defineProperty(window, 'location', { configurable: true, value: { ...originalLocation, reload: jest.fn() }, }); }); afterAll(() => { Object.defineProperty(window, 'location', { configurable: true, value: originalLocation, }); }); beforeEach(() => { jest.clearAllMocks(); server.use(http.get('/mfe/user', () => new HttpResponse('foo', { status: 200 }))); shouldFireEvent = true; isLoggedIn = true; }); function itRaisesCanceledError() { it('raises canceled request', async () => { await expect(subject()).rejects.toBeInstanceOf(CanceledError); }); } it('makes request successfully', async () => { const response = await subject(); expect(response).toBe('foo'); expect(requestCounts['/mfe/user']).toBe(1); }); describe('when iframe message is never sent', () => { beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(jest.fn()); // suppress AjaxHandlers.PreAuthenticate error jest.useFakeTimers(); shouldFireEvent = false; }); afterEach(() => { jest.useRealTimers(); }); itRaisesCanceledError(); }); describe('when request returns 401 once', () => { beforeEach(() => { server.use( http.get('/mfe/user', () => new HttpResponse(null, { status: 401 }), { once: true, }) ); }); it('re-authenticates', async () => { const response = await subject({ expectAuthTimes: 2 }); expect(response).toBe('foo'); expect(requestCounts['/bff/authInfo']).toBe(2); expect(requestCounts['/mfe/user']).toBe(2); }); }); describe('when user is not authenticated', () => { beforeEach(() => { isLoggedIn = false; window.sessionStorage.clear(); }); itRaisesCanceledError(); it('reloads the page', async () => { await expect(subject()).rejects.toThrow(); expect(window.location.reload).toHaveBeenCalled(); }); describe('with a custom error handler', () => { const onError = jest.fn(); beforeEach(() => (options = { onError })); test('calls error handler instead of reloading page', async () => { await expect(subject()).rejects.toThrow(); expect(window.location.reload).not.toHaveBeenCalled(); expect(onError).toHaveBeenCalled(); }); }); }); describe('when two instances of TokenServerAuth both authenticate at the same time with difference results', () => { beforeEach(() => { server.use(http.get('/mfe2/user', () => new HttpResponse('bar', { status: 200 }))); }); test('each instance receives the correct result', async () => { const responses = await Promise.allSettled([ subject(), subject({ baseURL: '/mfe2', isLoggedIn: false }), ]); expect((responses[0] as PromiseFulfilledResult).value).toBe('foo'); expect((responses[1] as PromiseRejectedResult).reason).toBeInstanceOf( CanceledError ); }); }); }); });