how to test axios interceptors with jest - javascript

In my project, I have a namespace that exports some functions that use Axios, in the same file I add an interceptor to axios instance like that :
axios.interceptors.response.use(
(res) => res,
(error) => {
if (
error.response &&
(error.response.status?.toString() === "400" ||
error.response.status?.toString() === "403" ||
error.response.status?.toString() === "404")
) {
return Promise.reject(
Error(JSON.stringify(error.response.data?.status?.errors[0]))
);
} else if (error.response) {
return Promise.reject(
Error(
`server responsed with the following code: ${error.response?.status} and the following message: ${error.response?.statusText}`
)
);
} else if (error.request) {
return Promise.reject(
Error(
"The request was made but no response was received, check your network connection"
)
);
} else Promise.reject(error);
}
);
I want to test that this interceptor works as expected, I search the forms here and googled a lot but all the answers are basically mocking the interceptor not testing it.
I have tried:
mocking the response of an axios post request and checking the AxiosPromise that gets returned but it only contained the result I mocked. it seems that it ignores the interceptor when I mock using mockResolvedValue.
I have tried adding an interceptor to the mocked axios instance but that did not work too.
Thanks

What about pulling the function out and testing it without axios?
import axios, { AxiosError, AxiosResponse } from 'axios'
export const onFullfilled = (response: AxiosResponse) => {
// Your interceptor handling a successful response
}
export const onRejected = (error: AxiosError) => {
// Your interceptor handling a failed response
}
axios.interceptors.response.use(onFullfilled, onRejected)
Now you can test the functions onFullfilled and onRejected with less dependencies to axios.

You have to mock the interceptor and run the callbacks.
Here is an example on how to do it:
httpService.ts
import axios from "axios";
import { toast } from "react-toastify";
axios.interceptors.request.use((config) => {
config.baseURL = process.env.API_URL || "http://localhost:5000";
return config;
});
axios.interceptors.response.use(null, (error) => {
const expectedError =
error.response &&
error.response.status >= 400 &&
error.response.status < 500;
if (!expectedError) {
toast.error("An unexpected error occured");
}
return Promise.reject(error);
});
export default {
get: axios.get,
post: axios.post,
put: axios.put,
delete: axios.delete,
};
httpService.test.ts
import axios from "axios";
import { toast } from "react-toastify";
import "./httpService";
jest.mock("axios", () => ({
__esModule: true,
default: {
interceptors: {
request: { use: jest.fn(() => {}) },
response: { use: jest.fn(() => {}) },
},
},
}));
const fakeError = {
response: {
status: undefined,
},
};
const mockRequestCallback = (axios.interceptors.request.use as jest.Mock).mock
.calls[0][0];
const mockResponseErrorCallback = (axios.interceptors.response.use as jest.Mock)
.mock.calls[0][1];
const toastErrorSpy = jest.spyOn(toast, "error");
beforeEach(() => {
toastErrorSpy.mockClear();
});
test("request error interceptor", () => {
expect(mockRequestCallback({})).toStrictEqual({
baseURL: "http://localhost:5000",
});
});
test("unexpected error on response interceptor", () => {
fakeError.response.status = 500;
mockResponseErrorCallback(fakeError).catch(() => {});
expect(toastErrorSpy).toHaveBeenCalled();
});
test("expected error on response interceptor", () => {
fakeError.response.status = 400;
mockResponseErrorCallback(fakeError).catch(() => {});
expect(toastErrorSpy).not.toHaveBeenCalled();
});

Use this mock functionality
jest.mock('axios', () => {
return {
interceptors: {
request: {
use: jest.fn(),
eject: jest.fn()
},
response: {
use: jest.fn(),
eject: jest.fn()
},
},
};
});

Related

jest axios test passes with advisory Network Error

I have a utils class that uses axios to perform simple REST calls. When writing a jest unit test, although the tests pass, i seem to get an advisory error:
C:/home/dev/node_modules/axios/lib/core/createError.js:16
var error = new Error(message);
Error: Network Error
at createErrorC:/home/dev/node_modules/axios/lib/core/createError.js:16
config: {
// ....
header: { Accept: 'application/json, text/plain, */*' },
withCredentials: true,
responseType: 'json',
method: 'get',
url: 'http://mock.rest.server.com:1234/rest/user/data/adam',
data: undefined
},
request: XMLHttpRequest {},
response: undefined,
isAxiosError: true,
toJSON: [Function: toJSON]
utility.ts
import axios, { AxiosResponse } from 'axios'
axios.defaults.withCredentials = true;
axios.defaults.responseType = 'json';
export class UserUtils {
public getUserConfig(userName: string): Promise<AxiosResponse> {
if(!userName) {
return;
}
return axios.get('http://mock.rest.server.com:1234/rest/user/data/' + userName);
}
}
utility.test.ts
import axios from 'axios';
import { UserUtils } from '../../utility';
describe("Utility test", () => {
const utils = new UserUtils();
jest.mock('axios', () => {
return {
post: jest.fn(),
get: jest.fn()
}
}
// clear all mocks
beforEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
test("get user data",() => {
jest.spyOn(axios, 'get');
utils.getUserConfig('adam')
.then(repsonse => {
expect(axios.get).toHaveBeenCalledWith('http://mock.rest.server.com:1234/rest/user/data/adam');
});
});
});
Maybe this thread can help you: https://stackoverflow.com/a/51654713/20293448
Personally, I like using jest manual mocks (docs)
In my project, I have:
// src/__mocks__/axios.ts
import axios from 'axios';
const mockAxios = jest.genMockFromModule<typeof axios>('axios');
// this is the key to fix the axios.create() undefined error!
mockAxios.create = jest.fn(() => mockAxios);
// eslint-disable-next-line import/no-default-export
export default mockAxios;
// src/.../test.ts
import mockAxios from 'axios';
const mockedPost = mockAxios.post as jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
});
...
expect(mockedPost).toHaveBeenCalledWith(route, payload);
Hope this helps!

Return Response When First request failed And Try In Second Request

I try to explain the problem.in App.js I have Function getUser .when call this function.in first request get 401 error . For this reason in axios.interceptors.response I receive error 401.At this time, I receive a token and repeat my request again.And it is done successfully.But not return response in Function getUser.
I have hook for authentication and send request.
import React from "react";
import axios from "axios";
const API_URL = "http://127.0.0.1:4000/api/";
function useJWT() {
axios.interceptors.request.use(
(request) => {
request.headers.common["Accept"] = "application/json";
console.log("request Send ");
return request;
},
(error) => {
return Promise.reject(error);
}
);
axios.interceptors.response.use(
(response) => {
console.log("answer = ", response);
return response;
},
(error) => {
if (error?.response?.status) {
switch (error.response.status) {
case 401:
refreshToken().then((responseTwo) => {
return
sendPostRequest(
error.response.config.url
.split("/")
.findLast((item) => true)
.toString(),
error.response.config.data
);
});
break;
case 500:
// Actions for Error 500
throw error;
default:
console.error("from hook interceptor => ", error);
throw error;
}
} else {
// Occurs for axios error.message = 'Network Error'
throw error;
}
}
);
const refreshToken = () => {
const token = localStorage.getItem("refresh");
return axios
.post(API_URL + "token", {
token,
})
.then((response) => {
if (response.data.access) {
localStorage.setItem("access", response.data.access);
}
if (response.data.refresh) {
localStorage.setItem("refresh", response.data.refresh);
}
return response.data;
});
};
function login(email, password) {
return axios
.post(API_URL + "login", {
email,
password,
})
.then((response) => {
if (response.data.access) {
localStorage.setItem("access", response.data.access);
}
if (response.data.refresh) {
localStorage.setItem("refresh", response.data.refresh);
}
return response.data;
});
}
const sendPostRequest = (url, data) => {
console.log(300);
const token = localStorage.getItem("access");
axios.defaults.headers.common["jwt"] = token;
return axios.post(API_URL + url, {
data,
});
};
const logout = () => {
const token = localStorage.getItem("refresh");
return axios
.delete(API_URL + "logout", {
token,
})
.then((response) => {
localStorage.removeItem("access");
localStorage.removeItem("refresh");
});
};
return {
login,
logout,
refreshToken,
sendPostRequest,
};
}
export default useJWT;
In App.js ,I want to repeat the same request again if a 401 error is issued when I read the user information.
The request is successfully repeated but does not return the value.
When first request fail response is return equals null . and in catch when receive 401 error i am send second request but not return response.
I send request below code .
const getUser = () => {
console.log(12);
return sendPostRequest("user");
};
useEffect(() => {
let token = localStorage.getItem("access");
console.log("token = ", token);
if (token != null) {
//Here I have done simulation for 401 error
localStorage.setItem("access", "");
getUser()
.then((response) => {
console.log("response 1= ", response);
})
.catch((exception) => {
console.log("exception = ", exception.toString());
})
.then((response) => {
console.log("response 2= ", response);
});
} else {
navigate("/login");
}
}, []);
Best regards.
I didn't fully understand what exactly you want to do here.
But if you are looking to retry when 401 happens, you could use axios-retry to do it for you.
I'll pass the basics, but you can look more into what this does.
// First you need to create an axios instance
const axiosClient = axios.create({
baseURL: 'API_URL',
// not needed
timeout: 30000
});
// Then you need to add this to the axiosRetry lib
axiosRetry(axiosClient, {
retries: 3,
// Doesn't need to be this, it can be a number in ms
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
// You could do this way or try to implement your own
return error.response.status > 400
// something like this works too.
// error.response.status === 401 || error.response.status >= 500;
}
});
Just like in your code, we need to use interceptors if you want to avoid breaking your page, otherwise you can use try catch to catch any errors that may happen in a request.
// It could be something like this, like I said, it's not really needed.
axiosClient.interceptors.response.use(
(success) => success,
(err) => err
);
And finally, you could use the axiosClient directly since it now has your API_URL, calling it like this axiosClient.post('/user').
More or less that's it, you should just debug this code and see what is causing the return value to be null.
I would change these then/catch to be an async/await function, it would be more readable making your debugging easier.
axios-retry example if you didn't understand my explanation.
I find anwser for this question.
When error 401 occurs then create new Promise
I Wrote this code.
case 401:
return new Promise((resolve, reject) => {
refreshToken().then((responseTwo) => {
resolve(
sendPostRequest(
error.response.config.url
.split("/")
.findLast((item) => true)
.toString(),
error.response.config.data
)
);
});
});

updated query returns undefined in apollo

I'm using Observable for updating token when its expiered.the process works properly and when token has been expiered it'll send a request and gets new token and then retries the last request .the request gets data correctly and I can see it in the network but when I'm trying to use the data in the component I get undefined with this error :
index.js:1 Missing field 'query name' while writing result {}
here is my config for apollo :
import {
ApolloClient,
createHttpLink,
InMemoryCache,
from,
gql,
Observable,
} from "#apollo/client";
import { setContext } from "#apollo/client/link/context";
import { onError } from "#apollo/client/link/error";
import store from "../store";
import { set_user_token } from "../store/actions/login_actions";
const httpLink = createHttpLink({
uri: "http://myserver.com/graphql/",
});
const authLink = setContext((_, { headers }) => {
const token = JSON.parse(localStorage.getItem("carfillo"))?.Login?.token;
return {
headers: {
...headers,
"authorization-bearer": token || null,
},
};
});
const errorHandler = onError(({ graphQLErrors, operation, forward }) => {
if (graphQLErrors?.length) {
if (
graphQLErrors[0].extensions.exception.code === "ExpiredSignatureError"
) {
const refreshToken = JSON.parse(localStorage.getItem("carfillo"))?.Login
?.user?.refreshToken;
const getToken = gql`
mutation tokenRefresh($refreshToken: String) {
tokenRefresh(refreshToken: $refreshToken) {
token
}
}
`;
return new Observable((observer) => {
client
.mutate({
mutation: getToken,
variables: { refreshToken: refreshToken },
})
.then((res) => {
const token = res.data.tokenRefresh.token;
store.dispatch(set_user_token(token));
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
"authorization-bearer": token || null,
},
}));
})
.then(() => {
const subscriber = {
next: observer.next(() => observer),
error: observer.error(observer),
complete: observer.complete(observer),
};
return forward(operation).subscribe(subscriber);
});
});
}
}
});
export const client = new ApolloClient({
link: from([errorHandler, authLink, httpLink]),
cache: new InMemoryCache(),
});

Unhandled Runtime Error AxiosError: Request failed with status code 401 in next.js

I am using Next.js. I have created an Axios interceptor where a rejected Promise will be returned. But where there is a server-specific error that I need. Next.js is showing the error in the application like this.
And there is the code of the Axios interceptor and instance.
import axios from "axios";
import store from "../redux/store";
import getConfig from 'next/config';
const { publicRuntimeConfig } = getConfig();
let token = "";
if (typeof window !== 'undefined') {
const item = localStorage.getItem('key')
token = item;
}
const axiosInstance = axios.create({
baseURL: publicRuntimeConfig.backendURL,
headers: {
Authorization: token ? `Bearer ${token}` : "",
},
});
axiosInstance.interceptors.request.use(
function (config) {
const { auth } = store.getState();
if (auth.token) {
config.headers.Authorization = `Bearer ${auth.token}`;
}
return config;
},
function (error) {
return Promise.reject(error);
}
);
axiosInstance.interceptors.response.use(
(res) => {
console.log(res)
return res;
},
(error) => {
console.log(error)
return Promise.reject(error);
}
);
export default axiosInstance;
Also, I am using redux and there is the action.
import axios from "../../api/axios";
import { authConstants } from "../types";
export const login = (data) => {
return async (dispatch) => {
try {
dispatch({
type: authConstants.LOGIN_REQUEST,
});
const res = axios.post("/user/login", data);
if (res.status === 200) {
dispatch({
type: authConstants.LOGIN_SUCCESS,
payload: res.data,
});
}
} catch (error) {
console.log(error, authConstants);
dispatch({
type: authConstants.LOGIN_FAILURE,
payload: { error: error.response?.data?.error },
});
}
};
};
Your problem is here...
const res = axios.post("/user/login", data);
You're missing await to wait for the response
const res = await axios.post("/user/login", data);
This fixes two things...
Your code now waits for the response and res.status on the next line will be defined
Any errors thrown by Axios (which surface as rejected promises) will trigger your catch block. Without the await this does not happen and any eventual promise failure bubbles up to the top-level Next.js error handler, resulting in the popup in your screenshot.

Testing Async Redux Action Jest

I'm having trouble getting the correct output from an async redux action. I am using Jest, redux-mock-adapter, and thunk as the tools.
According to redux's documentation on testing async thunks (https://redux.js.org/docs/recipes/WritingTests.html#async-action-creators), my tests should be returning an array of two actions. However, my test is only returning the first action, and not the second one that should return on a successful fetch. I think I'm just missing something small here, but it has been bothersome to say the least.
Redux Action
export const getRemoveFileMetrics = cacheKey => dispatch => {
dispatch({ type: IS_FETCHING_DELETE_METRICS });
return axios
.get("GetRemoveFileMetrics", { params: { cacheKey } })
.then(response => dispatch({ type: GET_REMOVE_FILE_METRICS, payload: response.data }))
.catch(err => err);
};
Test
it("getRemoveFileMetrics() should dispatch GET_REMOVE_FILE_METRICS on successful fetch", () => {
const store = mockStore({});
const cacheKey = "abc123doremi";
const removeFileMetrics = {
cacheKey,
duplicateFileCount: 3,
uniqueFileCount: 12,
};
const expectedActions = [
{
type: MOA.IS_FETCHING_DELETE_METRICS,
},
{
type: MOA.GET_REMOVE_FILE_METRICS,
payload: removeFileMetrics,
}
];
mockRequest.onGet(`/GetRemoveFileMetrics?cacheKey=${cacheKey}`).reply(200, removeFileMetrics);
return store.dispatch(MOA.getRemoveFileMetrics(cacheKey)).then(() => {
const returnedActions = store.getActions();
expect(returnedActions).toEqual(expectedActions);
});
});
The Output
Expected value to equal:
[{ "type": "IS_FETCHING_DELETE_METRICS" }, { "payload": { "cacheKey": "abc123doremi", "duplicateFileCount": 3, "uniqueFileCount": 12 }, "type": "GET_REMOVE_FILE_METRICS" }]
Received:
[{ "type": "IS_FETCHING_DELETE_METRICS" }]
I am using jest-fetch-mock and no axios. The following is working for me with the actions. You could refactor to async await as first step. For me it only worked that way.
I am now trying to figure out how to test the side effect (showErrorAlert(jsonResponse);). If I mock out the showErrorAlert implementation at the top of the test file (commented out in my example) then I get the same problem just like you. Actions that uses fetch won't get triggered for some reason.
export const submitTeammateInvitation = (data) => {
const config = {
//.....
};
return async (dispatch) => {
dispatch(submitTeammateInvitationRequest());
try {
const response = await fetch(inviteTeammateEndpoint, config);
const jsonResponse = await response.json();
if (!response.ok) {
showErrorAlert(jsonResponse);
dispatch(submitTeammateInvitationError(jsonResponse));
throw new Error(response.statusText);
}
dispatch(submitTeammateInvitationSuccess());
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.log('Request failed', error);
}
}
};
};
test
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
// jest.mock('../../../../_helpers/alerts', ()=> ({ showAlertError: jest.fn() }));
const middlewares = [thunk];
const createMockStore = configureMockStore(middlewares);
......
it('dispatches the correct actions on a failed fetch request', () => {
fetch.mockResponse(
JSON.stringify(error),
{ status: 500, statusText: 'Internal Server Error' }
);
const store = createMockStore({});
const expectedActions = [
{
type: 'SUBMIT_TEAMMATE_INVITATION_REQUEST',
},
{
type: 'SUBMIT_TEAMMATE_INVITATION_FAILURE',
payload: { error }
}
];
return store.dispatch(submitTeammateInvitation(data))
.then(() => {
// expect(alerts.showAlertError).toBeCalled();
expect(store.getActions()).toEqual(expectedActions);
});
});

Categories