Jest testing - how to handle JsonWebToken response - reactjs

I am learning how to test my redux thunk actions and the response from my login includes a randomized JsonWebToken. I've written a variable called expectedActions that matches all the data coming back from the action except how to handle randomized strings (JWT). Any ideas on how to handle this?
-- Also, i need to pass real user information (usename/password) to get a LOGIN_SUCCESS response otherwise the function dispatches the LOGIN_FAIL action. Is that normal?
/* eslint-disable no-undef */
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import fetchMock from 'fetch-mock';
import * as actions from '../../../redux/actions/auth';
const middleware = [thunk];
const mockStore = configureMockStore(middleware);
describe('redux async actions', () => {
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it('returns expected login response', async () => {
const userData = {
username: 'user',
email: 'user#gmail.com',
password: 'password',
};
const config = {
headers: {
'Content-Type': 'application/json',
},
};
fetchMock.getOnce('http://localhost:5000/api/v1/users', {
body: { ...userData },
config,
});
const expectedActions = { payload: { token: '' }, type: 'LOGIN_SUCCESS' };
// the value of the token above in the response is a randomized jwt string
const store = mockStore({});
return store
.dispatch(actions.login('user#gmail.com', 'password'))
.then(() => {
// return of async actions
const actionsResponse = store.getActions();
expect(actionsResponse[0]).toEqual(expectedActions);
});
});
});
Bonus: What is the point of fetchMock ? I borrowed the above code from another StackOverflow question and I have yet to understand what the fetchMock is doing.

I overrode the responses JWT with my own token of "123". I don't know if this is correct though, nor do i ever expect a response to this post.
const middleware = [thunk];
const mockStore = configureMockStore(middleware);
describe('redux async actions', () => {
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it('returns expected login response', async () => {
const expectedActions = {
payload: { token: '123' },
type: 'LOGIN_SUCCESS',
};
const store = mockStore({ alert: [], auth: { token: '123' } });
return store
.dispatch(actions.login('user#gmail.com', 'somePassword'))
.then(() => {
// return of async actions
const actionsResponse = store.getActions();
actionsResponse[0].payload.token = '123';
expect(actionsResponse[0]).toEqual(expectedActions);
});
});
});

Related

Can I access state inside a createAsyncThunk w/axios with redux toolkit?

I'm fairly new to redux toolkit so I'm still having a few issues with it!
As per the code below, I'm trying to access state (loginDetails.username and loginDetails.password) inside my createAsyncThunk. I'm obviously doing something wrong here - I've tried writing the createAsyncThunk function inside a different file, attempting to access the state inside that file and then importing the function, but either way it's failing.
// Import: Packages
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import axios from "axios";
// AsyncThunk: getUserDetails
export const getUserDetails = createAsyncThunk(
"userDetails/getUserDetails",
async () => {
try {
const apiUrl = process.env.REACT_APP_URL;
var config = {
method: "get",
url: `${apiUrl}/claimSet?UserName=${state.loginDetails.username}&Password=${state.loginDetails.password}`,
headers: {
accept: "application/json",
},
};
const response = await axios(config);
const data = await response.data;
return data;
} catch (error) {
console.log(error);
}
}
);
// Slice: userDetailsSlice
export const userDetailsSlice = createSlice({
name: "userDetails",
initialState: {
loginDetails: {
username: "",
password: "",
},
details: [],
status: null,
},
reducers: {
addUsername: (state, { payload }) => {
state.loginDetails.username = payload;
},
addPassword: (state, { payload }) => {
state.loginDetails.password = payload;
},
},
extraReducers: {
[getUserDetails.pending]: (state, action) => {
state.status = "loading";
},
[getUserDetails.fulfilled]: (state, { payload }) => {
state.details = payload;
state.status = "success";
},
[getUserDetails.rejected]: (state, action) => {
state.status = "failed";
},
},
});
// Actions: addUsername, addPassword
export const { addUsername, addPassword } = userDetailsSlice.actions;
// Reducer: userDetailsSlice.reducer
export default userDetailsSlice.reducer;
The code in the config url ${state.loginDetails.username}, etc. is just one of many failed attempts to get hold of the state. I understand that part of the issue is that the createAsyncThunk is declared before the state/slide is below, but I still can't seem to find a way around it.
Any help would be really appreciated!
Thanks in advance <3
The async function consumes a "payload" argument, and secondly a thunkAPI object that contains a getState method.
payloadCreator
thunkAPI: an object containing all of the parameters that are normally
passed to a Redux thunk function, as well as additional options:
dispatch: the Redux store dispatch method
getState: the Redux store getState method
extra: the "extra argument" given to the thunk middleware on setup, if available
requestId: a unique string ID value that was automatically generated to identify this request sequence
signal: an AbortController.signal object that may be used to see if another part of the app logic has marked this request as needing
cancelation.
rejectWithValue: rejectWithValue is a utility function that you can return in your action creator to return a rejected response with a
defined payload. It will pass whatever value you give it and return it
in the payload of the rejected action.
// AsyncThunk: getUserDetails
export const getUserDetails = createAsyncThunk(
"userDetails/getUserDetails",
async (arg, { getState }) => { // <-- destructure getState method
const state = getState(); // <-- invoke and access state object
try {
const apiUrl = process.env.REACT_APP_URL;
var config = {
method: "get",
url: `${apiUrl}/claimSet?UserName=${state.loginDetails.username}&Password=${state.loginDetails.password}`,
headers: {
accept: "application/json",
},
};
const response = await axios(config);
const data = await response.data;
return data;
} catch (error) {
console.log(error);
}
}
);

react-testing-library mocking axios.create({}) instance

I want to test my api with react-testing-library
And I exporting the instance created by axios.create from a file called apiClient.ts
import axios from 'axios'
const apiClient = axios.create({
baseURL: process.env.REACT_APP_API_URL,
responseType: 'json',
headers: {
'Content-Type': 'application/json',
},
})
export default apiClient
Then use the axios instances I get from apiClient in my users.ts fetchUsersApi
import apiClient from './apiClient'
export interface ITrader {
id: number
name: string
username: string
email: string
address: any
phone: string
website: string
company: any
}
export const fetchTradersApi = async (): Promise<ITrader[]> => {
const response = await apiClient.get<ITrader[]>('/users')
return response.data
}
I created a mocks folder and added axios.ts in it
export default {
get: jest.fn(() => Promise.resolve({ data: {} })),
}
My users.spec.tsx looks like:
import { cleanup } from '#testing-library/react'
import axiosMock from 'axios'
import { fetchTradersApi } from './traders'
jest.mock('axios')
describe.only('fetchTradersApi', () => {
afterEach(cleanup)
it('Calls axios and returns traders', async () => {
axiosMock.get.mockImplementationOnce(() =>
Promise.resolve({
data: ['Jo Smith'],
})
)
const traders = await fetchTradersApi()
expect(traders).toBe([{ name: 'Jo Smith' }])
expect(axiosMock.get).toHaveBeenCalledTimes(1)
expect(axiosMock.get).toHaveBeenCalledWith(`${process.env.REACT_APP_API_URL}/users`)
})
})
I run my test and I get:
Test suite failed to run
TypeError: _axios.default.create is not a function
1 | import axios from 'axios'
2 |
> 3 | const apiClient = axios.create({
Please help me on solving the issue by creating a proper axios mock that work with react-testing-library, Tnx in advance.
After spending an entire day I found a solution for the exact same problem which I was having. The problem which I was having is related to JEST, Node and Typescript combo. Let me brief about the files which are playing role in this:
axios-instance.ts // initializing axios
api.ts // api controller
api.spec.ts // api test files
axios-instance.ts
import axios, { AxiosInstance } from "axios";
const axiosInstance: AxiosInstance = axios.create({
baseURL: `https://example-path/products/`,
headers: {
'Content-Type': 'application/json'
}
});
export default axiosInstance;
api.ts
"use strict";
import {Request, Response, RequestHandler, NextFunction} from "express";
import axiosInstance from "./axios-instance";
/**
* POST /:productId
* Save product by productId
*/
export const save: RequestHandler = async (req: Request, res: Response, next: NextFunction) => {
try {
const response = await axiosInstance.post(`${req.params.id}/save`, req.body);
res.status(response.status).json(response.data);
} catch (error) {
res.status(error.response.status).json(error.response.data);
}
};
api.spec.ts
import { save } from "./api";
import axiosInstance from "./axios-instance";
describe.only("controller", () => {
describe("test save", () => {
let mockPost: jest.SpyInstance;
beforeEach(() => {
mockPost = jest.spyOn(axiosInstance, 'post');
});
afterEach(() => {
jest.clearAllMocks();
});
it("should save data if resolved [200]", async () => {
const req: any = {
params: {
id: 5006
},
body: {
"productName": "ABC",
"productType": "Food",
"productPrice": "1000"
}
};
const res: any = {
status: () => {
return {
json: jest.fn()
}
},
};
const result = {
status: 200,
data: {
"message": "Product saved"
}
};
mockPost.mockImplementation(() => Promise.resolve(result));
await save(req, res, jest.fn);
expect(mockPost).toHaveBeenCalled();
expect(mockPost.mock.calls.length).toEqual(1);
const mockResult = await mockPost.mock.results[0].value;
expect(mockResult).toStrictEqual(result);
});
it("should not save data if rejected [500]", async () => {
const req: any = {
params: {
id: 5006
},
body: {}
};
const res: any = {
status: () => {
return {
json: jest.fn()
}
},
};
const result = {
response: {
status: 500,
data: {
"message": "Product is not supplied"
}
}
};
mockPost.mockImplementation(() => Promise.reject(result));
await save(req, res, jest.fn);
expect(mockPost).toHaveBeenCalled();
const calls = mockPost.mock.calls.length;
expect(calls).toEqual(1);
});
});
});
For the posted requirement we have to mock the "axiosInstance" not the actual "axios" object from the library as we are doing our calling from axiosInstance.
In the spec file we have imported the axiosInstance not the actual axios
import axiosInstance from "./axios-instance";
Then we have created a spy for the post method (get/post/put anything you can spy)
let mockPost: jest.SpyInstance;
Initializing in before each so that each test case will have a fresh spy to start with and also clearing the mocks are needed after each.
beforeEach(() => {
mockPost = jest.spyOn(axiosInstance, 'post');
});
afterEach(() => {
jest.clearAllMocks();
});
Mocking the implementation resolved/reject
mockPost.mockImplementation(() => Promise.resolve(result));
mockPost.mockImplementation(() => Promise.reject(result));
Then calling the actual method
await save(req, res, jest.fn);
Checking for the expected results
expect(mockPost).toHaveBeenCalled();
expect(mockPost.mock.calls.length).toEqual(1);
const mockResult = await mockPost.mock.results[0].value;
expect(mockResult).toStrictEqual(result);
Hope it helps and you can relate the solution with your problem. Thanks
Maybe is too late to register my answer, but it can help others.
What has happened in this case is the context. Your apiClient function runs in another context, so one of the ways is to mock your apiClient instead of the Axios library.
...
import apiClient from 'path-to-your-apiClient';
jest.mock('path-to-your-apiClient');
const mockedApi = apiClient as jest.Mocked<typeof apiClient>;
Now, let's make some changes to your api.spec.ts:
import { save } from "./api";
import axiosInstance from "./axios-instance";
import apiClient from 'path-to-your-apiClient';
jest.mock('path-to-your-apiClient');
const mockedApi = apiClient as jest.Mocked<typeof apiClient>;
describe.only("controller", () => {
describe("test save", () => {
beforeEach(() => {
jest.clearAllMocks();
mockedApi.post.mockeResolvedValue({ your-defalt-value }) // if you need
})
it("should save data if resolved [200]", async () => {
const req: any = {
params: {
id: 5006
},
body: {
"productName": "ABC",
"productType": "Food",
"productPrice": "1000"
}
};
const res: any = {
status: () => {
return {
json: jest.fn()
}
},
};
const result = {
status: 200,
data: {
"message": "Product saved"
}
};
mockedApi.post.mockResolvedValue(result);
await save(req, res, jest.fn);
expect(mockedApi.post).toHaveBeenCalled();
expect(mockedApi.post).toHaveBeenCalledTimes(1);
... // add may assertions as you want
});
....
});
});
Hope this piece of code can help others.

Testing an HTTP post Request with mocha using nock

I'm learning how to test a frontend webapp without any connection to the API.
My problem is: I have to test an POST HTTP Request but always get an error : TypeError: loginUser(...).then is not a function.
I know my expect is not correct. I must change the data for a JWT token, and also don't know yet hot to do it.
It's a simple user authentication. Http post sending an email and password, getting back a JWT (json web token). I have to write a test to make sure I've send the correct information and get a JWT as response.
Thanks for your help
Here is my code:
//login.test.js
const expect = require('chai').expect;
const loginUser = require('../src/actions/authActions').loginUser;
const res = require('./response/loginResponse');
const nock = require('nock');
const userData = {
email: 'test#test.com',
password: '123456'
};
describe('Post loginUser', () => {
beforeEach(() => {
nock('http://localhost:3000')
.post('/api/users/login', userData )
.reply(200, res);
});
it('Post email/pwd to get a token', () => {
return loginUser(userData)
.then(res => {
//expect an object back
expect(typeof res).to.equal('object');
//Test result of name, company and location for the response
expect(res.email).to.equal('test#test.com')
expect(res.name).to.equal('Tralala!!!')
});
});
});
//authActions.js
import axios from "axios";
import setAuthToken from "../utils/setAuthToken";
import jwt_decode from "jwt-decode";
import {
GET_ERRORS,
SET_CURRENT_USER,
USER_LOADING
} from "./types";
// Login - get user token
export const loginUser = userData => dispatch => {
axios
.post("/api/users/login", userData)
.then(res => {
// Save to localStorage
// Set token to localStorage
const { token } = res.data;
localStorage.setItem("jwtToken", token);
// Set token to Auth header
setAuthToken(token);
// Decode token to get user data
const decoded = jwt_decode(token);
// Set current user
dispatch(setCurrentUser(decoded));
})
.catch(err =>
dispatch({
type: GET_ERRORS,
payload: err.response.data
})
);
// loginResponse.js
module.exports = { email: 'test#test.com',
password: '123456',
name: "Tralala!!!"
};
Actual result:
1) Post loginUser
Post email/pwd to get a token:
TypeError: loginUser(...).then is not a function
at Context.then (test/login.test.js:37:12)
The way you called loginUser method is not correct. This method returns another function. So, instead of loginUser(userData), you must also specify the dispatch parameter e.g. loginUser(userData)(dispatch).then().
I changed the method to specify return before axios statement
export const loginUser = userData => dispatch => {
return axios // adding return
.post("/api/users/login", userData)
.then(res => {
...
})
.catch(err =>
dispatch({
type: GET_ERRORS,
payload: err.response.data
})
);
};
for test, I may involve Sinon to spy the dispatch
it("Post email/pwd to get a token", () => {
const dispatchSpy = sinon.spy();
return loginUser(userData)(dispatchSpy).then(res => {
//expect an object back
expect(typeof res).to.equal("object");
//Test result of name, company and location for the response
expect(res.email).to.equal("test#test.com");
expect(res.name).to.equal("Tralala!!!");
});
});
Hope it helps

Redux mock store only returning one action when multiple actions are dispatched

I'm trying to mock this axios call:
export const fetchCountry = (query) => {
return dispatch => {
dispatch(fetchCountryPending());
return axios.get(`${process.env.REACT_APP_API_URL}/api/v1/countries/?search=${query}`)
.then(response => {
const country = response.data;
dispatch(fetchCountryFulfilled(country));
})
.catch(err => {
dispatch(fetchCountryRejected());
dispatch({type: "ADD_ERROR", error: err});
})
}
}
Which on a successful call, should dispatch both action creators fetchCountryPending() and fetchCountryFullfilled(country). When I mock it like so:
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
// Async action tests
describe('country async actions', () => {
let store;
let mock;
beforeEach(function () {
mock = new MockAdapter(axios)
store = mockStore({ country: [], fetching: false, fetched: true })
});
afterEach(function () {
mock.restore();
store.clearActions();
});
it('dispatches FETCH_COUNTRY_FULFILLED after axios request', () => {
const query = 'Aland Islands'
mock.onGet(`${process.env.REACT_APP_API_URL}/api/v1/countries/?search=${query}`).replyOnce(200, country)
store.dispatch(countryActions.fetchCountry(query))
const actions = store.getActions()
console.log(actions)
expect(actions[0]).toEqual(countryActions.fetchCountryPending())
expect(actions[1]).toEqual(countryActions.fetchCountryFulfilled(country))
});
});
The second expect fails and console.log(actions) only shows an array with the one action, but it should contain both actions, fetchCountryPending and fetchCountrySuccess. When I log ('dispatched'), it shows the second action is getting dispatched in the terminal.
Can you try making your it block async and dispatch the action. I believe the tests are running before your get requests return the value
I couldn't get a then(() => {}) block to work but I was able to await the function and make it async:
it('dispatches FETCH_COUNTRY_FULFILLED after axios request', async () => {
const query = 'Aland Islands'
mock.onGet(`${process.env.REACT_APP_API_URL}/api/v1/countries/?search=${query}`).replyOnce(200, country)
await store.dispatch(countryActions.fetchCountry(query))
const actions = store.getActions()
console.log(actions)
expect(actions[0]).toEqual(countryActions.fetchCountryPending())
expect(actions[1]).toEqual(countryActions.fetchCountryFulfilled(country))
});
});

Testing fetch action in react/redux app

Im starting with unit testing and Jest. What I want is to test the action's response after fetching some resources from the db.
This is the action code:
export function loadPortlets() {
return function(dispatch) {
return portletApi.getAllPortlets().then(response => {
dispatch(loadPortletsSuccess(response));
dispatch(hideLoading());
}).catch(error => {
dispatch({ type: null, error: error });
dispatch(hideLoading());
throw(error);
});
};
}
This code is fetching data from:
static getAllPortlets() {
return fetch(`${API_HOST + API_URI}?${RES_TYPE}`)
.then(response =>
response.json().then(json => {
if (!response.ok) {
return Promise.reject(json);
}
return json;
})
);
}
And this is the test:
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import fetch from 'isomorphic-fetch';
import fetchMock from 'fetch-mock';
import * as actions from '../portletActions';
import * as types from '../actionTypes';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
const mockResponse = (status, statusText, response) => {
return new window.Response(response, {
status: status,
statusText: statusText,
headers: {
'Content-type': 'application/json'
}
});
};
describe('async actions', () => {
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
})
it('calls request and success actions if the fetch response was successful', () => {
window.fetch = jest.fn().mockImplementation(() =>
Promise.resolve(mockResponse(200, null, [{ portlets: ['do something'] }])));
const store = mockStore({ portlets: []});
return store.dispatch(actions.loadPortlets())
.then(() => {
const expectedActions = store.getActions();
expect(expectedActions[0]).toContain({ type: types.LOAD_PORTLETS_SUCCESS });
})
});
});
And this is the result of running the test:
FAIL src\actions\__tests__\portletActions.tests.js
● async actions › calls request and success actions if the fetch response was successful
expect(object).toContain(value)
Expected object:
{"portlets": [// here an array of objects], "type": "LOAD_PORTLETS_SUCCESS"}
To contain value:
{"type": "LOAD_PORTLETS_SUCCESS"}
at store.dispatch.then (src/actions/__tests__/portletActions.tests.js:56:34)
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)
In the redux docs for this example (https://redux.js.org/recipes/writing-tests), they receive a result containing only the action types executed, but I'm getting the real data and the action inside the array.
So I'm not sure if the code is wrong, or the test, or both!
Thanks in advance, any help is highly appreciated!
You're testing too much with this unit test. I see you are using thunks it looks like so you can change your fetch to be passed as a module to the thunk and do something like this. I used jasmine but it's basically the same thing. You don't want to mock your store here just the action and dispatch. The point of the unit test should be to test the async action, not to test getting real data from the db or redux store interactions so you can stub all that out.
For reference configureStore would look like this...
const createStoreWithMiddleware = compose(
applyMiddleware(thunk.withExtraArgument({ personApi }))
)(createStore);
And the test case...
it('dispatches an action when receiving', done => {
const person = [{ firstName: 'Francois' }];
const expectedAction = {
type: ActionTypes.RECEIVED,
payload: {
people,
},
};
const dispatch = jasmine.createSpy();
const promise = Q.resolve(person);
const personApi = {
fetchPerson: jasmine
.createSpy()
.and.returnValue(promise),
};
const thunk = requestPerson();
thunk(dispatch, undefined, { personApi });
promise.then(() => {
expect(dispatch.calls.count()).toBe(2);
expect(dispatch.calls.mostRecent().args[0]).toEqual(expectedAction);
done();
});
});

Resources