Skip to content

Commit

Permalink
[#25] Add tests for Axios interceptors wip
Browse files Browse the repository at this point in the history
  • Loading branch information
liamstevens111 committed Mar 20, 2023
1 parent 1ca5c97 commit e1cfc27
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 50 deletions.
94 changes: 91 additions & 3 deletions src/lib/requestManager.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
import axios from 'axios';

import requestManager, { defaultOptions } from './requestManager';
import requestManager, {
createRequestSuccessInterceptor,
createResponseErrorInterceptor,
defaultOptions,
} from './requestManager';
import { LOGIN_URL } from '../constants';
import { setItem, clearItem } from '../helpers/localStorage';

const windowHref = window.location.href;
const endPoint = 'https://sample-endpoint.com/api/';

jest.mock('axios');

describe('requestManager', () => {
const endPoint = 'https://sample-endpoint.com/api/';
/* eslint-disable camelcase */

const mockTokenData = {
access_token: 'test_access_token',
refresh_token: 'test_refresh_token',
token_type: 'Bearer',
expires_in: 7200,
created_at: 1677045997,
};

const mockUserProfileData = {
email: '[email protected]',
name: 'TestName',
avatar_url: 'https://secure.gravatar.com/avatar/6733d09432e89459dba795de8312ac2d',
};

/* eslint-enable camelcase */

describe('requestManager', () => {
it('fetches successfully data from an API', async () => {
const responseData = {
data: [
Expand Down Expand Up @@ -44,3 +69,66 @@ describe('requestManager', () => {
requestSpy.mockRestore();
});
});

describe('createRequestSuccessInterceptor', () => {
beforeEach(() => {
clearItem('UserProfile');
});

it('given an existing access_token, sets Authorization header in Axios config', async () => {
setItem('UserProfile', { auth: mockTokenData, user: mockUserProfileData });

const interceptor = createRequestSuccessInterceptor();
const config = interceptor({ headers: {}, url: endPoint });

expect(config.headers.Authorization).toBe(`Bearer ${mockTokenData.access_token}`);
});

it('given a non-existing access_token, does NOT set Authorization header in Axios config', async () => {
const interceptor = createRequestSuccessInterceptor();
const config = interceptor({ headers: {}, url: endPoint });

expect(config.headers.Authorization).toBeUndefined();
});
});

describe('createResponseErrorInterceptor', () => {
beforeEach(() => {
clearItem('UserProfile');

delete global.window.location;
window.location = {};
Object.defineProperty(window, 'location', {
value: {
href: windowHref,
},
});
});

it('given NON existing tokens and 401 error, redirects to login page', async () => {
const errorResponse = {
name: '',
message: '',
isAxiosError: true,
toJSON: () => ({}),
config: {},
code: '',
response: {
data: {},
status: 401,
statusText: '',
headers: {},
config: {},
},
};

try {
const interceptor = createResponseErrorInterceptor();
await interceptor(errorResponse);
} catch (err) {
console.log(err);
expect(err).toBe(errorResponse);
expect(window.location.href).toEqual(LOGIN_URL);
}
});
});
96 changes: 49 additions & 47 deletions src/lib/requestManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import axios, { Method as HTTPMethod, ResponseType, AxiosRequestConfig, AxiosResponse } from 'axios';
import axios, { Method as HTTPMethod, ResponseType, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';

import AuthAdapter from 'adapters/authAdapter';
import { setItem, getItem, clearItem } from 'helpers/localStorage';
Expand All @@ -9,6 +9,48 @@ export const defaultOptions: { responseType: ResponseType } = {
responseType: 'json',
};

export function createRequestSuccessInterceptor() {
return function (config: AxiosRequestConfig) {
const userProfile = getItem('UserProfile');

if (userProfile?.auth?.access_token) {
config.headers.Authorization = `Bearer ${userProfile.auth.access_token}`;
}
return config;
};
}

export function createResponseErrorInterceptor() {
return async function (error: AxiosError) {
if (error.response?.status === 401) {
const userProfile = getItem('UserProfile');

clearItem('UserProfile');

if (userProfile?.auth?.refresh_token) {
try {
const response = await AuthAdapter.loginWithRefreshToken(userProfile.auth.refresh_token);

const { attributes: authInfo } = await response.data;

/* eslint-disable camelcase */
setItem('UserProfile', { ...userProfile, auth: authInfo });
/* eslint-enable camelcase */

error.config.headers.Authorization = `Bearer ${authInfo.accessToken}`;
return axios.request(error.config);
} catch {
window.location.href = LOGIN_URL;
}
}

window.location.href = LOGIN_URL;
}

return Promise.reject(error);
};
}

export type RequestParamsType = AxiosRequestConfig;

/**
Expand All @@ -33,53 +75,13 @@ const requestManager = (
...requestOptions,
};

axios.interceptors.request.use(
function (config) {
const userProfile = getItem('UserProfile');

if (userProfile?.auth?.access_token) {
config.headers.Authorization = `Bearer ${userProfile.auth.access_token}`;
}
return config;
},
function (error) {
return Promise.reject(error);
}
);

axios.interceptors.response.use(
function (response) {
return response;
},
async function (error) {
if (error.response?.status === 401) {
const userProfile = getItem('UserProfile');

clearItem('UserProfile');

if (userProfile?.auth?.refresh_token) {
try {
const response = await AuthAdapter.loginWithRefreshToken(userProfile.auth.refresh_token);

const { attributes: authInfo } = await response.data;

/* eslint-disable camelcase */
setItem('UserProfile', { ...userProfile, auth: authInfo });
/* eslint-enable camelcase */

error.config.headers.Authorization = `Bearer ${authInfo.accessToken}`;
return axios.request(error.config);
} catch {
window.location.href = LOGIN_URL;
}
}

window.location.href = LOGIN_URL;
}
axios.interceptors.request.use(createRequestSuccessInterceptor(), function (error) {
return Promise.reject(error);
});

return Promise.reject(error);
}
);
axios.interceptors.response.use(function (response) {
return response;
}, createResponseErrorInterceptor());

return axios.request(requestParams).then((response: AxiosResponse) => {
return response.data;
Expand Down
6 changes: 6 additions & 0 deletions src/screens/Home/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import AuthAdapter from 'adapters/authAdapter';
import { useUser } from 'components/PrivateRoutes';

function fetchMyUser() {
return AuthAdapter.getUser();
}

const HomeScreen = (): JSX.Element => {
const user = useUser();

return (
<>
<div className="my-8 text-white opacity-50" data-test-id="app-main-heading">
Home Screen
<button onClick={fetchMyUser}>YOYOY</button>
</div>
{/* TODO: Remove when header implemented in #19 */}
<div className="my-8 text-white opacity-50">{`${user?.name} - ${user?.email} - ${user?.avatarUrl}`}</div>
Expand Down

0 comments on commit e1cfc27

Please sign in to comment.