Skip to content

Instantly share code, notes, and snippets.

@rexebin
Last active January 11, 2022 15:07
Show Gist options
  • Save rexebin/71416657c6842d756fb732dad43932df to your computer and use it in GitHub Desktop.
Save rexebin/71416657c6842d756fb732dad43932df to your computer and use it in GitHub Desktop.
Testing a Error Handler Utility Hook
import { act } from '@testing-library/react-hooks';
import axios, { AxiosError } from 'axios';
import { useErrorHandler } from './useErrorHandler';
import { MessageProvider, useMessage } from '../message/MessageContext';
import { localStorageKey } from './useClient';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { environment } from '@epic/environments';
jest.mock('axios');
jest.mock('@epic/environments');
afterAll(() => {
jest.clearAllMocks();
});
function TestComponent() {
const { errorHandler } = useErrorHandler({ message: 'Oops!' });
const sendRequest = async () => {
try {
await axios.get('/test');
} catch (e) {
errorHandler(e as AxiosError);
}
};
const { message } = useMessage();
return (
<div>
<button onClick={() => sendRequest()}>Send Request</button>
{message?.message ? <div data-testid={'error-message'}>{message?.message}</div> : null}
{message?.type ? <div data-testid={'error-type'}>{message?.type}</div> : null}
</div>
);
}
function setup() {
const mockedAxios = axios as jest.Mocked<typeof axios>;
const queries = render(
<MessageProvider>
<TestComponent />
</MessageProvider>
);
return { ...queries, mockedAxios };
}
describe('Error Handler', function () {
const token = 'I am a token';
beforeEach(() => {
localStorage.setItem(localStorageKey, token);
});
it('should remove token when authentication is failed', async function () {
const { getByText, findByTestId, mockedAxios } = setup();
mockedAxios.get.mockRejectedValueOnce({
response: {
status: 401,
statusText: 'Unauthorized',
data: {
message: 'Unauthorized',
},
},
} as AxiosError);
act(() => {
userEvent.click(getByText(/Send Request/i));
});
const message = await findByTestId('error-message');
const type = await findByTestId('error-type');
expect(message.innerHTML).toMatch(/login failed/i);
expect(type.innerHTML).toBe('error');
expect(localStorage.getItem(localStorageKey)).toBe(null);
});
it('should alert to login in to an account with permission when authorisation fails', async function () {
const { getByText, findByTestId, mockedAxios } = setup();
mockedAxios.get.mockRejectedValueOnce({
response: {
status: 403,
statusText: 'Unauthorized',
data: {
message: 'Unauthorized',
},
},
} as AxiosError);
act(() => {
userEvent.click(getByText(/Send Request/i));
});
const message = await findByTestId('error-message');
const type = await findByTestId('error-type');
expect(message.innerHTML).toContain('sign in with an account with the required permissions');
expect(type.innerHTML).toBe('error');
expect(localStorage.getItem(localStorageKey)).toBe(token);
});
it('should return error detail, prepended with custom message', async function () {
const { getByText, findByTestId, mockedAxios } = setup();
mockedAxios.get.mockRejectedValueOnce({
response: {
status: 400,
statusText: 'Bad Request',
data: {
detail: 'Unable to process request',
errors: ['Invalid request'],
},
},
} as AxiosError);
act(() => {
userEvent.click(getByText(/Send Request/i));
});
const message = await findByTestId('error-message');
const type = await findByTestId('error-type');
expect(message.innerHTML).toBe('Oops! Unable to process request');
expect(type.innerHTML).toBe('error');
expect(localStorage.getItem(localStorageKey)).toBe(token);
});
it('should return concatenated errors messages if error detail is absent, prepended with custom message', async function () {
const { getByText, findByTestId, mockedAxios } = setup();
mockedAxios.get.mockRejectedValueOnce({
response: {
status: 400,
statusText: 'Bad Request',
data: {
errors: ['Invalid request', 'Invalid Id'],
},
},
} as AxiosError);
act(() => {
userEvent.click(getByText(/Send Request/i));
});
const message = await findByTestId('error-message');
const type = await findByTestId('error-type');
expect(message.innerHTML).toBe('Oops! Invalid request, Invalid Id');
expect(type.innerHTML).toBe('error');
expect(localStorage.getItem(localStorageKey)).toBe(token);
});
it('should return serialised data object if error detail and errors are absent', async function () {
const { getByText, findByTestId, mockedAxios } = setup();
mockedAxios.get.mockRejectedValueOnce({
response: {
status: 400,
statusText: 'Bad Request',
data: {
message: 'Invalid request',
},
},
} as AxiosError);
act(() => {
userEvent.click(getByText(/Send Request/i));
});
const message = await findByTestId('error-message');
const type = await findByTestId('error-type');
expect(message.innerHTML).toMatchInlineSnapshot(
`"Oops! {\\"message\\":\\"Invalid request\\"}"`
);
expect(type.innerHTML).toBe('error');
expect(localStorage.getItem(localStorageKey)).toBe(token);
});
it('should alert server connection failed if no response was received', async function () {
const { getByText, findByTestId, mockedAxios } = setup();
mockedAxios.get.mockRejectedValueOnce({
request: {},
} as AxiosError);
act(() => {
userEvent.click(getByText(/Send Request/i));
});
const message = await findByTestId('error-message');
const type = await findByTestId('error-type');
expect(message.innerHTML).toMatchInlineSnapshot(`"Server connection failed"`);
expect(type.innerHTML).toBe('error');
expect(localStorage.getItem(localStorageKey)).toBe(token);
});
it(`if these is no response/request properties in the returned error, in development mode,
1. should alert message on error object,
2. should log error json.
3. should print error in the console`, async function () {
jest.spyOn(console, 'error').mockImplementation(() => {});
jest.spyOn(console, 'log').mockImplementation(() => {});
environment.production = false;
const { getByText, findByTestId, mockedAxios } = setup();
mockedAxios.get.mockRejectedValueOnce({
message: 'Invalid request',
} as AxiosError);
act(() => {
userEvent.click(getByText(/Send Request/i));
});
const message = await findByTestId('error-message');
const type = await findByTestId('error-type');
expect(message.innerHTML).toMatchInlineSnapshot(`"Invalid request"`);
expect(type.innerHTML).toBe('error');
expect(localStorage.getItem(localStorageKey)).toBe(token);
expect(console.log).toHaveBeenCalledWith(`{"message":"Invalid request"}`);
expect(console.error).toHaveBeenCalledWith({
message: 'Invalid request',
});
jest.restoreAllMocks();
});
it(`if these is no response/request properties in the returned error, in production mode,
1. should not alert message on error object,
2. should not log error json.
3. should print error in the console`, async function () {
jest.spyOn(console, 'error').mockImplementation(() => {});
environment.production = true;
const { getByText, mockedAxios } = setup();
mockedAxios.get.mockRejectedValueOnce({
message: 'Invalid request',
} as AxiosError);
act(() => {
userEvent.click(getByText(/Send Request/i));
});
await waitFor(() => {
expect(console.error).toHaveBeenCalledWith({
message: 'Invalid request',
});
});
jest.restoreAllMocks();
});
});
import { AxiosError, AxiosResponse } from 'axios';
import { environment } from '@epic/environments';
import { useCallback } from 'react';
import { useMessage } from '../message/MessageContext';
import { concatValues } from '../utils/concatValues';
import { localStorageKey } from './useClient';
type Translator = (english?: string) => string | undefined;
function extractErrorMessage(response: AxiosResponse): string {
if (response.data.detail) {
return response.data.detail;
}
if (response.data && response.data.errors) {
return concatValues(response.data.errors);
}
return JSON.stringify(response.data);
}
export function useErrorHandler({
message,
t = (english?: string) => english,
}: {
message?: string;
t?: Translator;
} = {}) {
const { sendError } = useMessage();
const processResponseError = useCallback(
({ response, message, t }: { response: AxiosResponse; message?: string; t: Translator }) => {
if (response?.status === 401) {
localStorage.removeItem(localStorageKey);
sendError(t('Login failed'));
window.location.assign(window.location.href);
return;
}
if (response?.status === 403) {
sendError(
t('Authorisation failed. Please sign in with an account with the required permissions.')
);
return;
}
sendError(`${message} ${t(extractErrorMessage(response))}`.trim());
return;
},
[sendError]
);
const errorHandler = useCallback(
(error: AxiosError<any>) => {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
return processResponseError({ response: error.response, message, t });
}
if (error.request) {
// The request was made but no response was received
// `error.request`
// is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
sendError(t(`Server connection failed`));
return;
}
if (!environment.production) {
// Something happened in setting up the request that triggered an Error
sendError(t(error.message));
console.log(JSON.stringify(error));
}
console.error(error);
},
[message, processResponseError, sendError, t]
);
return { errorHandler };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment