how to jest test an async action with axios in react? - reactjs

I have an action-generator register.js:
import { REGISTER_SUCCESS, REGISTER_FAIL } from "./types";
import axios from "axios";
export const register = (formData) => async (dispatch) => {
const { name, email, password } = formData;
const configRegister = {
method: "post",
url: "/api/users",
headers: { "Content-Type": "application/json" },
data: { name, email, password },
};
try {
const res = await axios(configRegister);
const token = res.data.token;
dispatch({
type: REGISTER_SUCCESS,
payload: {
token,
isAuthenticated: true,
loading: false,
},
});
} catch (err) {
console.error(err);
dispatch({
type: REGISTER_FAIL,
payload: {
token: null,
isAuthenticated: false,
loading: true,
},
});
}
};
the endpoint /api/users returns {token:'a_token_string'} on being successful.
How should i test this action-generator using jest ?
I tried doing this, register.test.js :-
import {
REGISTER_SUCCESS,
} from "./types";
import thunk from "redux-thunk";
import configureMockStore from "redux-mock-store";
import axios from "axios";
jest.mock("axios");
/** mock-store */
const createMockStore = configureMockStore([thunk]);
const defaultState = [];
const store = createMockStore(defaultState);
/** reset mock */
afterEach(() => jest.resetAllMocks());
test("should register a user ", async () => {
axios.post.mockImplementation(() => {
return Promise.resolve({
status: 200,
body: {
token: "testToken",
},
});
});
const res = await axios.post("/api/users");
console.log(res.body);
const testUser = {
name: "testName",
email: "test#email.com",
password: "testPassword",
};
await store.dispatch(register(testUser)).then(() => {
expect(store.getActions()[0]).toEqual({
type: REGISTER_SUCCESS,
payload: {
token: "testToken",
isAuthenticated: true,
loading: false,
},
});
});
});

You're quite close to get it done. The thing is you're mocking axios.post while your implementation is using directly from axios object. As long as you mock axios object then it would work as it should. Here is proposed changes, please check inline comments for things you should also change:
test("should register a user ", async () => {
// Mock `axios` object directly
axios.mockImplementation(() => {
return Promise.resolve({
status: 200,
// should also change from `body` to `data` as well
data: {
token: "testToken",
},
});
});
// it will no longer work since the mock is changed
// const res = await axios.post("/api/users");
// console.log(res.body);
const testUser = {
name: "testName",
email: "test#email.com",
password: "testPassword",
};
await store.dispatch(register(testUser)).then(() => {
expect(store.getActions()[0]).toEqual({
type: REGISTER_SUCCESS,
payload: {
token: "testToken",
isAuthenticated: true,
loading: false,
},
});
});
});

Related

How to use Context with PassportJs Auth

I'm trying to let users log in and register using google Auth, I'm using Context to let the app knows if the user is signed in or not, Register and Login works fine, but i didn't know how to configure Google login.
Register.jsx (Register Page):
const [username,setUsername] = useState("");
const [email,setEmail] = useState("");
const [password,setPassword] = useState("");
const [repeatPassword, setRepeatPassword] = useState("");
const { dispatch, isFetching } = useContext(Context);
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: "REGISTER_START" });
setError(false);
try {
const res = await axios.post("/register", {
username,
email,
password,
repeatPassword,
});
dispatch({ type: "REGISTER_SUCCESS", payload: res.data });
} catch (err) {
dispatch({ type: "REGISTER_FAILURE" });
setError(true);
}
};
Login.jsx (Login Page):
const userRef = useRef();
const passwordRef = useRef();
const { dispatch, isFetching } = useContext(Context);
const [error, setError] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: "LOGIN_START" });
try {
const res = await axios.post("/login", {
email: userRef.current.value,
password: passwordRef.current.value,
});
dispatch({ type: "LOGIN_SUCCESS", payload: res.data });
} catch (err) {
dispatch({ type: "LOGIN_FAILURE" });
setError(true)
}
};
Context Actions.js
export const LoginStart = (userCredentials) => ({
type: "LOGIN_START",
});
export const LoginSuccess = (user) => ({
type: "LOGIN_SUCCESS",
payload: user,
});
export const LoginFailure = () => ({
type: "LOGIN_FAILURE",
});
// THE SAME FOR REGISTER
Context Reducer.js
const Reducer = (state, action) => {
switch (action.type) {
case "LOGIN_START":
return {
user: null,
isFetching: true,
error: false,
};
case "LOGIN_SUCCESS":
return {
user: action.payload,
isFetching: false,
error: false,
};
case "LOGIN_FAILURE":
return {
user: null,
isFetching: false,
error: true,
};
// THE SAME FOR REGISTER
default:
return state;
}
};
export default Reducer;
I tried something like this but it didn't work
const handleGoogleLogin = async (e) => {
window.open("http://localhost:4000/auth/google/callback", "_self");
dispatch({ type: "LOGIN_START" });
try {
const res = await fetch("/login/success", {
method: "GET",
credentials: "include",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"Access-Control-Allow-Credentials": true,
},
});
dispatch({ type: "LOGIN_SUCCESS", payload: res.user });
} catch (err) {
dispatch({ type: "LOGIN_FAILURE" });
setError(true)
console.log(err)
}
};
I don't if I have to make new actions just for social media authentication, or if this is the correct way but I'm missing something.
This is Auth.js route:
router.get("/login/success", (req, res) => {
if (req.user) {
res.status(200).json({
error: false,
message: "succesfull",
user: req.user,
});
} else {
res.status(403).json({ error: true, message: "Not Authorized" });
}
});

Error Cannot set headers after they are sent to the client

I got this error whenever I try to log In using Google Login API
In my console I get this error:
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent
to the client
and in the screen I get white page with this error:
InternalOAuthError: Failed to fetch user profile
I'm using two Login method, one is normal and one using Passport JS
Login.jsx Login Page
import { useEffect, useContext, useRef } from "react";
import { Context } from "../../context/Context";
import axios from "axios";
import { useState } from "react"
export default function Login() {
const userRef = useRef();
const passwordRef = useRef();
const { dispatch, isFetching } = useContext(Context);
const [error, setError] = useState(false);
// FOR LOGIN
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: "LOGIN_START" });
try {
const res = await axios.post("/login", {
email: userRef.current.value,
password: passwordRef.current.value,
});
dispatch({ type: "LOGIN_SUCCESS", payload: res.data });
} catch (err) {
dispatch({ type: "LOGIN_FAILURE" });
setError(true)
}
};
// FOR GOOGLE LOGIN
useEffect(() => {
fetch(`http://localhost:4000/login/success`, {
method: 'GET',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'Access-Control-Allow-Credentials': true,
},
})
.then((response) => {
dispatch({ type: "LOGIN_START" });
if (response.status === 200) return response.json();
throw new Error('failed to authenticate user');
})
.then((responseJson) => {
dispatch({ type: "LOGIN_SUCCESS", payload: responseJson.data });
})
.catch((error) => {
dispatch({ type: "LOGIN_FAILURE" });
// eslint-disable-next-line no-console
console.error("Failed to authenticate user", error)
});
}, []);
const google = () => {
window.open("http://localhost:4000/auth/google/callback", "_self");
};
return()
}
auth.js Route:
router.get("/login/success", (req, res) => {
if (req.user) {
res.status(200).json({
error: false,
message: "succesfull",
user: req.user,
cookies: req.cookies
});
} else {
res.status(403).json({ error: true, message: "Not Authorized" });
}
});

GraphQL + Redux + JWT User Authentication

I am working on trying to set up JWT authentication using qraphql and redux.
Currently the register function is working and posts the new user to the database, but I cant seem to get the loadUser function to work.
authAction.js
const API_URL = 'https://my-server.com/api';
export const register = ({name, email, password}) => dispatch => {
axios.post(
API_URL, {
query: `mutation {
userSignup( name: "${name}", email: "${email}", password: "${password}"){
name,
email,
password
}
}
`,
})
.then(res =>
dispatch({
type: REGISTER_SUCCESS,
payload: res.data.data
}))
.catch(err => {
dispatch({
type: REGISTER_FAIL
})
})
}
export const loadUser = (email, password) => dispatch => {
dispatch({type: USER_LOADING})
axios.post(
API_URL, {
query: `query {
userLogin(email: "${email}", password: "${password}"){
email,
password,
token
}
}
`,
})
.then(res => dispatch({
type: USER_LOADED,
type: LOGIN_SUCCESS,
payload: res.data.data
}))
.catch(err => {
dispatch(retrunErrors(err.response.data, err.response.status));
dispatch({
type: AUTH_ERROR
});
});
}
Both of these functions should return the token, and the reducer should set the token to localStorage.
authReducer.js
...
case LOGIN_SUCCESS:
case REGISTER_SUCCESS:
localStorage.setItem('token', action.payload.token)
console.log(action.payload)
return {
...state,
...action.payload,
isAuthenticated: true,
isLoading: false,
}
...
I did have the login working using hooks (did not get to configuring the signUp), but it was all in app.js and it really needed to be broken down. I was told that i needed to move everything to redux, so here i am. I have dug through tons of documentation, but cant find the solution.
FWIW, here is the old app.js (its long)
const API_URL = 'https://my-server.com/api';
function App() {
const initialLoginState = {
isLoading: false,
userName: null,
userToken: null,
};
const setUser = (token, user) => {
if (token) {
axios.defaults.headers.common.Authorization = `Bearer ${token}`;
} else {
delete axios.defaults.headers.common.Authorization;
}
return {type: 'LOGIN', id: user.email, token: token};
};
// Set a user after login or using local (AsyncStorage) token
const setUserLocally = async (key, value) => {
// Set token & user object
// const items = [
// ['userToken', token],
// ['user', JSON.stringify(user)],
// ];
await localStorage.setItem(key, value);
};
const unsetUserLocally = () => {
// Remove token
localStorage.removeItem('userToken');
localStorage.removeItem('user');
return {type: 'LOGOUT'};
};
const loginReducer = (prevState, action) => {
switch (action.type) {
default:
return {
...prevState,
userToken: action.token,
isloading: false,
}
case 'RETRIEVE_TOKEN':
return {
...prevState,
userToken: action.token,
isloading: false,
};
case 'LOGIN':
return {
...prevState,
userName: action.id,
userToken: action.token,
isloading: false,
};
case 'LOGOUT':
return {
...prevState,
userName: null,
userToken: null,
isloading: false,
};
case 'REGISTER':
return {
...prevState,
userName: action.id,
userToken: action.token,
isloading: false,
};
}
};
const [loginState, dispatch] = useReducer(loginReducer, initialLoginState);
const auth = {
signIn: async (userName, password) => {
try {
const response = await axios
.post(
API_URL,
query({
operation: 'userLogin',
variables: {
email: userName,
password: password,
},
fields: ['user {name, email, role}', 'token'],
})
);
let message = 'Please try again.';
if (response.data.errors && response.data.errors.length > 0) {
message = response.data.errors[0].message;
} else if (response.data.data.userLogin.token !== '') {
const token = response.data.data.userLogin.token;
const user = response.data.data.userLogin.user;
setUserLocally('userToken', token).then(() => {
return setUserLocally('user', JSON.stringify(user));
});
dispatch(setUser(token, user));
}
} catch (error) {
console.log(error);
}
},
signOut: () => {
dispatch(unsetUserLocally());
},
signUp: () => {
// setUserToken('sdf');
// setIsLoading(false);
},
getCurrentUser: () => {
//return 'test'; //JSON.parse(AsyncStorage.getItem('userToken'));
let userArr = {};
const value = async () => {
await localStorage.multiGet(['user', 'userToken']).then(
(response) => {
response.forEach((item) => {
userArr[item[0]] = item[1];
});
return userArr;
},
);
};
return value;
},
};
console.log(loginState.userToken)
useEffect(() => {
let users = async () => {
let userToken;
try {
userToken = await localStorage.getItem('userToken');
} catch (e) {
console.log(e);
}
dispatch({type: 'RETRIEVE_TOKEN', token: userToken});
};
users();
}, []);
// if (loginState.isLoading === true) {
// return <Loading />;
// }
return (
<AuthContext.Provider value={auth}>
<Router>
<AuthSwitcher user={loginState.userToken}/>
</Router>
</AuthContext.Provider>
)
}
export default App

fetchMock function makes actual API calls instead of mocking the requests

I am trying to test my signupAction shown below.
import axios from 'axios';
import actionTypes from '../action_types';
import { apiRequest } from '../common_dispatch';
export const signupAction = (user) => async (dispatch) => {
dispatch(apiRequest(true));
await axios
.post(`${process.env.REACT_APP_API_URL}/users`, { ...user }, {
headers: { 'Content-Type': 'application/json' },
})
.then((response) => {
dispatch(
{
type: actionTypes.REGISTER_SUCCESS,
payload: response.data.user,
},
);
dispatch(apiRequest(false));
})
.catch((error) => {
let errors = 'ERROR';
if (error.message === 'Network Error') {
errors = error.message;
} else {
errors = error.response.data.errors;
console.log(error);
}
dispatch(
{
type: actionTypes.REGISTER_FAIL,
payload: errors,
},
);
dispatch(apiRequest(false));
});
};
I figured I could mock the API call above using the fetchMock library. The problem is fetchMock makes actual calls hence the test passes in the first intance but fails when I run it the second time because the user I am trying to sign up already exists. my test is as shown below.
mport configureMockStore from 'redux-mock-store';
import * as actions from './signup.action';
import mocks from './mocks';
import thunk from 'redux-thunk';
import fetchMock from 'fetch-mock';
const middlewares = [thunk]
const mockStore = configureMockStore(middlewares)
describe('signUp actions', () => {
afterEach(() => {
fetchMock.resetMocks();
console.log('yess bro am called')
})
it('dispatches create REGISTER_FAIL when signup has been done', async () => {
fetchMock.postOnce('/users', { ...mocks.user }, {
headers: { 'Content-Type': 'application/json' },
});
const expectedActions = [
{ type: 'API_REQUEST', payload: true },
{ type: 'REGISTER_FAIL', payload: { email: "Email karanilarrygmail.com is not a valid email" } },
{ type: 'API_REQUEST', payload: false },
]
const store = mockStore(mocks.user);
return store.dispatch(actions.signupAction(mocks.user)).then(() => {
expect(store.getActions()).toEqual(expectedActions)
})
});
mocks.user is an object containing the user signup data.
What am I doing wrong

Cannot read property 'data' of undefined when user successfully register

I keep getting an error everytime a user signs up successfully,
Unhandled Rejection (TypeError): Cannot read property 'data' of
undefined
dispatch({
type: GET_ERRORS,
payload: err.response.data
})
why is it executing this catch method if their is no error when a user signs up successfully ?
console.log(err.response) shows when a user is registering with the same username or email.
What could i be doing wrong ?
authActions.js
export const registerUser = (userData) => dispatch => {
Axios
.post('/users/register', userData)
.then( res => {
const token = res.data.token;
console.log(token);
// pass the token in session
sessionStorage.setItem("jwtToken", token);
// set the auth token
setAuthToken(token);
// decode the auth token
const decoded = jwt_decode(token);
// pass the decoded token
dispatch(setCurrentUser(decoded))
this.props.history.push("/dashboard")
}).catch( (err) => {
console.log(err.response)
dispatch({
type: GET_ERRORS,
payload: err.response.data
})
})
};
reducer
import {SET_CURRENT_USER } from '../actions/types';
import isEmpty from '../actions/utils/isEmpty';
const initialState = {
isAuthenticated: false
}
export default (state = initialState, action) => {
switch (action.type) {
case SET_CURRENT_USER:
return{
...state,
isAuthenticated: !isEmpty(action.payload),
user:action.payload
}
default:
return state;
}
}
console.log(res)
{
"data": {
"message": "user created",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NDEsImlhdCI6MTU1OTI2ODcxNX0.t7xK5VVdOmj2BbExBmOdUrZHwYuyEJgjvUTQq1Mw5qY",
"auth": true
},
"status": 200,
"statusText": "OK",
"headers": {
"content-type": "application/json; charset=utf-8",
"content-length": "165"
},
"config": {
"transformRequest": {},
"transformResponse": {},
"timeout": 0,
"xsrfCookieName": "XSRF-TOKEN",
"xsrfHeaderName": "X-XSRF-TOKEN",
"maxContentLength": -1,
"headers": {
"Accept": "application/json",
"Content-Type": "application/json"
},
"method": "post",
"baseURL": "http://localhost:3000",
"withCredentials": true,
"url": "http://localhost:3000/users/register",
"data": "{\"username\":\"billyssss999\",\"email\":\"j0hnnnysssraddddddin#yahoo.com\",\"password\":\"janeddmdddba\"}"
},
"request": {}
So it seems to be working after i remove this
this.props.history.push("/dashboard")
and redirects to the dashboard already by this existing code.
Signup
....
componentDidMount() {
// console.log(this.props.auth);
if (this.props.auth.isAuthenticated) {
this.props.history.push("/dashboard");
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.auth.isAuthenticated) {
this.props.history.push("/dashboard");
}
if (nextProps.errors) {
this.setState({ errors: nextProps.errors });
}
}
handleChange = (e) => {
e.preventDefault();
const {formData} = this.state;
this.setState({
formData: {
...formData,
[e.target.name]: e.target.value
}
});
}
handleSubmit = (e) => {
e.preventDefault();
const {formData} = this.state;
const {username, email, password, passwordConf} = formData;
this.setState({
username: this.state.username,
password: this.state.password,
passwordConf: this.state.passwordConf,
email: this.state.email
});
const creds = {
username,
email,
password
}
console.log(creds);
if (password === passwordConf) {
this.props.registerUser(creds, this.props.history);
} else {
this.setState({passErr: "Passwords Don't Match"})
}
}

Resources