jest testing nodejs controller - javascript

I have the following controller
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { UserModel, isPasswordAllowed } from '../../models/User';
const saltRounds = 10;
function userController() {
function add(req, res) {
try {
if (req.body.administrator) {
res.status(400).json({
error: {
message: 'Bad Request',
},
});
return;
}
if (!isPasswordAllowed(req.body.password)) {
res.status(400).json({
error: {
message: 'La contraseña no cumple con los requisitos minimos',
},
});
return;
}
bcrypt.hash(req.body.password, saltRounds, async (err, hash) => {
if (err) {
res.status(500).json({ error: { code: '500', message: err.errmsg } });
return;
}
const user = new UserModel();
user.email = req.body.email.toLowerCase();
user.password = hash;
await user
.save()
.then(() => {
const token = jwt.sign(
{
username: user.email,
userId: user.id,
},
process.env.JWT_KEY,
{
expiresIn: '7d',
},
);
res.status(200).json({
message: 'Usuario Creado',
token,
email: user.email,
});
})
.catch((error) => {
if (error.code === 11000) {
res.status(400).json({
error: { code: '500', message: 'El correo ya existe' },
});
} else {
console.log(error);
res.status(500).json({ error: { code: '500', message: error.message } });
}
});
});
} catch (error) {
res.status(503).json({ error });
}
}
return {
add,
};
}
export default userController();
As you can expect this controller works great, the user is created in the database, but I have the following test:
import UserController from './UserController';
import { connect, closeDatabase, clearDatabase } from '../../__test__/db-handler';
describe('test UserController', () => {
const res = {};
beforeEach(async () => {
await connect();
res.send = jest.fn().mockReturnValue(res);
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);
});
afterEach(async () => {
await clearDatabase();
});
afterAll(async () => {
await closeDatabase();
});
test('should return the expect api method', () => {
const userControllerApi = {
add: expect.any(Function),
};
expect(UserController).toMatchObject(userControllerApi);
});
test('should return 400 error bad request is body contains administrator: true', async () => {
const req = {
body: {
administrator: true,
},
};
await UserController.add(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledTimes(1);
expect(res.json).toHaveBeenCalledWith({
error: {
message: 'Bad Request',
},
});
});
test('should return 400 error bad request is password is not allow', async () => {
const req = {
body: {
password: '123456',
},
};
await UserController.add(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledTimes(1);
expect(res.json).toHaveBeenCalledWith({
error: {
message: 'La contraseña no cumple con los requisitos minimos',
},
});
});
// this test is not passing
test('should create an user and return a token', async () => {
const req = {
body: {
email: 'test#test.com',
password: 'Abc123456',
},
};
const expectObject = {
message: 'Usuario Creado',
email: 'test#test.com',
};
await UserController.add(req, res);
jest.useFakeTimers();
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledTimes(1);
expect(res.json).toMatchObject(expectObject);
});
});
but the last test 'should create an user and return a token' never pass and I get the following:
● test UserController › should create an user and return a token
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: 200
Number of calls: 0
78 | jest.useFakeTimers();
79 |
> 80 | expect(res.status).toHaveBeenCalledWith(200);
| ^
81 | expect(res.json).toHaveBeenCalledTimes(1);
82 | expect(res.json).toMatchObject(expectObject);
83 | });
I also debbug this code in testing mode and as you can see in the following image, the code is enter in the res.status(200).json({ .... }), so I don't understand what it is happening here.

The problem is that you're mixing callbacks with async/await, meaning that execution of add() will be finished before the callback of bcrypt.hash has been finished. This results in res.status not to have been called yet in your test.
You can fix this by awaiting the bcrypt.hash call (it supports returning a promise by default):
// await hashing function instead of using callback
const hash = await bcrypt.hash(req.body.password, saltRounds);
const user = new UserModel();
user.email = req.body.email.toLowerCase();
user.password = hash;
// rest of the code ...

Related

How to Update User With JWT Token When he Tries To Login in Nodejs and Reactjs with MongoDB

I am trying to create a login functionality for my Reactjs Webiste using Nodejs express backend.
I want to set a JWT token when the user tries to log in and update that token in my mongoDB database and then verify the token on the frontend and save it to localStorage.
However, when the user tries to log in after registration, it returns back the result without the token, and thus not allowing the user to log in, unless he clicks the login button again, then my code would generate and update the user with the JWT token.
Why is this behavior happening? Why is the first response only returning the found user from the findOne() operation when i am resolving the result from the findOneAndUpdate operation?
Here is my code:
Auth Controller:
login(params) {
params.email = params.email.toLowerCase();
return new Promise((resolve, reject) => {
db.collection("Users").findOne({ email: params.email }).then((response) => {
console.log(response)
if(response) {
bcrypt.compare(params.password, response.password, (err, success) => {
if(success) {
let token = jwt.sign({
name: response.name,
id: response._id
}, proccess.env.JWT_SECRET);
db.collection("Users").findOneAndUpdate({
email: params.email
}, {
$set: { token: token, lastLogin: new Date() },
}, function (e, s) {
if(e) {
console.log(e)
reject(e)
} else {
console.log("updated")
resolve(s)
}
})
} else {
reject({msg: 'Incorrect email or password.'})
}
})
} else {
reject({msg: 'cannot log in user'});
}
})
})
}
Auth Router:
router.post('/login', (req, res) => {
let User = new models.User()
let processes = [];
processes.push(function (callback) {
User.login(req.body).then(function (response) {
callback(null, response);
}, function (error) {
console.log(error)
callback(error);
});
});
async.waterfall(processes, function (error, data) {
if (!error) {
return res.json({
statusCode: 200,
msg: 'User logged in successfully.',
result: data
});
} else {
return res.json({
statusCode: 401,
msg: 'Cannot login user.',
error: error
});
}
});
})
React Login.js:
const login = () => {
axios.post('/login', data).then(async (response) => {
console.log(response)
if(response && response.data.result.value.token ) {
localStorage.setItem("authUser", JSON.stringify(response.data.result.value.token))
history.push("/")
console.log(response.data.result)
} else {
console.log("ERROR")
}
})
}
MongoDBs method findOneAndUpdate does return the old document by default.
In order to return the updated document pass returnNewDocument: true as option:
https://www.mongodb.com/docs/manual/reference/method/db.collection.findOneAndUpdate/
In your case:
db.collection("Users").findOneAndUpdate({
email: params.email
}, {
$set: { token: token, lastLogin: new Date() },
}, {
returnNewDocument: true
}, function (e, s) {
if(e) {
console.log(e)
reject(e)
} else {
console.log("updated")
resolve(s)
}
})
PS: You might should use async functions with await. This could make your code way more readable (at least within the User Model) :)
This can help you.
In your model
async login(params) {
params.email = params.email.toLowerCase();
try {
const user = await db.collection("Users").findOne({ email: params.email });
if(!user) {
throw {message: "Incorrect email"}
}
const vaild = await bcrypt.compare(params.password, user.password);
if(!valid) {
throw {msg: 'Incorrect email or password.'}
}
let token = jwt.sign({
name: user.name,
id: user._id
}, proccess.env.JWT_SECRET);
return db.collection("Users").findOneAndUpdate({
email: params.email
}, {
$set: { token: token, lastLogin: new Date() },
}, {new: true}); //FOR THE RETRIEVE NEW UPDATEs FROM MONGODB
} catch(e) {
throw e
}
}

Getting erros using passport-google-oauth20 InternalOAuthError: Failed to fetch user profile and Cannot set headers after they are sent to the client

I'm using passport strategies for different socialMedia logins and getting the following two errors
InternalOAuthError: Failed to fetch user profile
Cannot set headers after they are sent to the client
I have doubt there somewhere I have returned a callback or response so getting 2nd error but for 1st don't know reasons scope seems to be correct!
strategy code
passport.use(new GoogleStrategy({
clientID: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_SECRET_KEY,
callbackURL: GOOGLE_CALLBACK_URL
}, async (acessToken, refreshToken, profile, done) => {
await User.findOne({ email: profile._json.email }, async (err, user) => {
if (err) {
console.log("passport.config --> err", err);
done(err, null);
} else if (user) {
if (user.socialType !== "GOOGLE" || user.socialType === null)
done(`LOGIN_CREDENTIALS_WITH_${(user.socialType || "PASSWORD").toUpperCase()}`, false);
else {
done(null, user);
}
} else {
// console.log(profile);
const user = {
email: profile._json.email,
socialId: profile.id,
socialType: "GOOGLE",
firstName: profile.name.givenName,
lastName: profile.name.familyName,
isActive: profile._json.email_verified,
isVerified: profile._json.email_verified,
socialImageUrl: profile._json.picture,
userType: "CUSTOMER"
};
const newUser = new User({ ...user });
const newUserData = await newUser.save();
done(null, newUserData);
}
});
}));
route code:
router.get('/auth/:socialType', customerCtrl.socialTypeLogin);
router.get('/auth/:socialType/callback', customerCtrl.socialTypeLoginCallback);
controller code:
const socialTypeLogin = async (req, res) => {
await customerService.socialTypeLogin(req, res);
};
const socialTypeLoginCallback = async (req,res) => {
await customerService.socialTypeLoginCallback(req,res);
};
service code:
const socialTypeLogin = async (req, res) => {
try {
const socialType = (req.params.socialType || '').toLowerCase();
const GOOGLE_SCOPE = ['email', 'profile'];
const FACEBOOK_SCOPE = ['email'];
let scope = [];
if (socialType === 'google') {
scope = GOOGLE_SCOPE;
} else if (socialType === 'facebook') {
scope = FACEBOOK_SCOPE;
}
let oauthOptions = { scope: scope};
const { returnUrl } = req.query;
if(returnUrl && returnUrl.trim().length !== 0) {
oauthOptions['state'] =JSON.stringify({ returnUrl: returnUrl });
}
passport.authenticate(socialType, oauthOptions)(req, res);
}
catch (error) {
}
}
/**
* #param {string} socialType
*/
const socialTypeLoginCallback = async (req, res) => {
const socialType = (req.params.socialType || '').toLowerCase();
// return new Promise((resolve, reject) => {
try {
passport.authenticate(socialType, async (err, user) => {
let webappRedirectURL = WEBAPP_LOGIN_URL;
try {
const state = req.query.state;
if(state) {
const stateObj = JSON.parse(state);
webappRedirectURL = stateObj.returnUrl;
}
} catch (err1) {
console.log("customer.service --> parsing error",err1);
}
if (err || !user) {
console.log("customer.service --> !user",err);
res.render('oauth-redirect', {
webappRedirectURL: webappRedirectURL,
success: false,
error: err,
timerCounter: 5,
accessToken: undefined
});
}
else {
console.log("customer.service --> Generating Token",user.generateJWT());
res.render('oauth-redirect', {
webappRedirectURL: webappRedirectURL,
success: true,
timerCounter: 5,
accessToken: user.generateJWT(),
error: undefined
});
}
})(req, res);
}
catch (error) {
console.log("customerService.js ==> socialTypeLoginCallback -->",error);
}
};
Thanks for help in advance!
I have doubt there somewhere I have returned a callback or response so getting 2nd error but for 1st don't know reasons scope seems to be correct!
In socialTypeLogin
add line
oauthOptions['session'] = false;

Failed to load resource: the server responded with a status of 400 (Bad Request) http://localhost:5000/api/refresh_token

Im getting this error, but I don't know what's wrong with my code, since it works perfectly on Postman, but doesn't when I run it on Node.js or localhost:3000 (which is my client side).
Here's the controller authCtrl.js code:
const Users = require("../models/userModel");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const authCtrl = {
register: async (req, res) => {
try {
const { fullname, username, email, password, gender } = req.body;
let newUserName = username.toLowerCase().replace(/ /g, "");
const user_name = await Users.findOne({ username: newUserName });
if (user_name)
return res.status(400).json({ msg: "This user name already exists." });
const user_email = await Users.findOne({ email });
if (user_email)
return res.status(400).json({ msg: "This email already exists." });
if (password.length < 6)
return res
.status(400)
.json({ msg: "Password must be at least 6 characters." });
const passwordHash = await bcrypt.hash(password, 12);
const newUser = new Users({
fullname,
username: newUserName,
email,
password: passwordHash,
gender,
});
const access_token = createAccessToken({ id: newUser._id });
const refresh_token = createRefreshToken({ id: newUser._id });
res.cookie("refreshtoken", refresh_token, {
httpOnly: true,
path: "http://localhost:5000/api/refresh_token",
maxAge: 30 * 24 * 60 * 60 * 1000, // 30days
});
await newUser.save();
res.json({
msg: "Register Success!",
access_token,
user: {
...newUser._doc,
password: "",
},
});
} catch (err) {
return res.status(500).json({ msg: err.message });
}
},
login: async (req, res) => {
try {
const { email, password } = req.body;
const user = await Users.findOne({ email }).populate(
"followers following",
"avatar username fullname followers following"
);
if (!user)
return res.status(400).json({ msg: "This email does not exist." });
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch)
return res.status(400).json({ msg: "Password is incorrect." });
const access_token = createAccessToken({ id: user._id });
const refresh_token = createRefreshToken({ id: user._id });
res.cookie("refreshtoken", refresh_token, {
httpOnly: true,
path: "http://localhost:5000/api/refresh_token",
maxAge: 30 * 24 * 60 * 60 * 1000, // 30days
});
res.json({
msg: "Login Success!",
access_token,
user: {
...user._doc,
password: "",
},
});
} catch (err) {
return res.status(500).json({ msg: err.message });
}
},
logout: async (req, res) => {
try {
res.clearCookie("refreshtoken", {
path: "http:/localhost:5000/api/refresh_token",
});
return res.json({ msg: "Logged out!" });
} catch (err) {
return res.status(500).json({ msg: err.message });
}
},
generateAccessToken: async (req, res) => {
try {
const rf_token = req.cookies.refreshtoken;
if (!rf_token)
return res.status(400).json({ msg: "Refresh token failed." });
jwt.verify(
rf_token,
process.env.REFRESH_TOKEN_SECRET,
async (err, result) => {
if (err) return res.status(400).json({ msg: "JWT Verify failed." });
const user = await Users.findById(result.id)
.select("-password")
.populate(
"followers following",
"avatar username fullname followers following"
);
if (!user)
return res.status(400).json({ msg: "This does not exist." });
const access_token = createAccessToken({ id: result.id });
res.json({
access_token,
user,
});
}
);
} catch (err) {
return res.status(500).json({ msg: err.message });
}
},
};
const createAccessToken = (payload) => {
return jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, {
expiresIn: "1d",
});
};
const createRefreshToken = (payload) => {
return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET, {
expiresIn: "30d",
});
};
module.exports = authCtrl;
Here's the util fetchData.js (where I use axios to fetch the data):
import axios from "axios";
export const getDataAPI = async (url, token) => {
const res = await axios.get(`http://localhost:5000/api/${url}`, {
headers: { Authorization: token },
});
return res;
};
export const postDataAPI = async (url, post, token) => {
const res = await axios.post(`http://localhost:5000/api/${url}`, post, {
headers: { Authorization: token },
});
return res;
};
export const putDataAPI = async (url, post, token) => {
const res = await axios.put(`http://localhost:5000/api/${url}`, post, {
headers: { Authorization: token },
});
return res;
};
export const patchDataAPI = async (url, post, token) => {
const res = await axios.patch(`http://localhost:5000/api/${url}`, post, {
headers: { Authorization: token },
});
return res;
};
export const deleteDataAPI = async (url, token) => {
const res = await axios.delete(`http://localhost:5000/api/${url}`, {
headers: { Authorization: token },
});
return res;
};
Here's the redux action authAction.js file:
import { GLOBALTYPES } from "./globalTypes";
import { postDataAPI } from "../../utils/fetchData";
export const login = (data) => async (dispatch) => {
try {
dispatch({ type: GLOBALTYPES.ALERT, payload: { loading: true } });
const res = await postDataAPI("login", data);
dispatch({
type: GLOBALTYPES.AUTH,
payload: {
token: res.data.access_token,
user: res.data.user,
},
});
localStorage.setItem("firstLogin", true);
dispatch({
type: GLOBALTYPES.ALERT,
payload: {
success: res.data.msg,
},
});
} catch (err) {
dispatch({
type: GLOBALTYPES.ALERT,
payload: {
error: err.response.data.msg,
},
});
}
};
export const refreshToken = () => async (dispatch) => {
const firstLogin = localStorage.getItem("firstLogin");
if (firstLogin) {
dispatch({ type: GLOBALTYPES.ALERT, payload: { loading: true } });
try {
const res = await postDataAPI("refresh_token");
dispatch({
type: GLOBALTYPES.AUTH,
payload: {
token: res.data.access_token,
user: res.data.user,
},
});
dispatch({ type: GLOBALTYPES.ALERT, payload: {} });
} catch (err) {
dispatch({
type: GLOBALTYPES.ALERT,
payload: {
error: err.response.data.msg,
},
});
}
}
};

What is the problem in the nodejs controller function?

exports.signupController = async (req, res) => {
const { phone, password } = req.body;
try {
const user = await User.findOne({ phone }).exec()
if (user) {
return res.status(400).json({
errorMessage: 'Phone Number already exists',
});
}
const newUser = new User();
newUser.phone = phone;
const salt = await bcrypt.genSalt(10);
newUser.password = await bcrypt.hash(password, salt);
await newUser.save();
return res.status(200).json({
successMessage: 'Registration success. Please login',
});
} catch (err) {
console.log('signupController error: ', err);
res.status(500).json({
errorMessage: 'Server error',
});
}};
**I upload a node application in shared hosting! **
*But an error was showing in this controller function. All the time the catch block is running on the json. The error is unhandled promise rejection. *
signup(data)
.then((response) => {
console.log('Axios signup success: ', response);
setFormData({
phone: '',
password: '',
password2: '',
loading: false,
successMsg: response.data.successMessage,
});
history.push('/signin');
})
.catch((err) => {
console.log('Axios signup error: ', err);
setFormData({
...formData,
loading: false,
errorMsg: err.response.data.errorMessage,
});
});
this is react front end event handler
import axios from 'axios';
export const signup = async (data) => {
const config = {
headers: {
'Content-Type': 'application/json',
},
};
const response = await axios.post('/api/auth/signup', data, config);
return response;
};
the signup api function
Mongoose queries are not promises. They have a .then() function for co and async/await as a convenience. If you need a fully-fledged promise, use the .exec() function. for example:
const query = Band.findOne({name: "Guns N' Roses"});
assert.ok(!(query instanceof Promise));
// A query is not a fully-fledged promise, but it does have a `.then()`.
query.then(function (doc) {
// use doc
});
// `.exec()` gives you a fully-fledged promise
const promise = query.exec();
assert.ok(promise instanceof Promise);
promise.then(function (doc) {
// use doc
});
If you are using exec() on your findOne query you should use:
exports.signupController = async (req, res) => {
const { phone, password } = req.body;
try {
const user = await User.findOne({ phone }).exec();
/// just a pseudo code
user.then('do your things').catch( 'log error')
const newUser = new User();
newUser.phone = phone;
const salt = await bcrypt.genSalt(10);
newUser.password = await bcrypt.hash(password, salt);
await newUser.save();
return res.status(200).json({
successMessage: 'Registration success. Please login',
});
} catch (err) {
console.log('signupController error: ', err);
res.status(500).json({
errorMessage: 'Server error',
});
}};
for more details check this out: https://mongoosejs.com/docs/promises.html#should-you-use-exec-with-await?

I am trying to create a doc to model with mongoose but model.create() does not return any promise

it seems that the create method does not return any promise that then can handle
I tried different things but nothing worked
this is my routes file
const express = require("express")
const router = express.Router();
const controller = require("./controller")
router.post("/signup", controller.create);
module.exports = router;
and this is my model file
const mongoose = require('mongoose');
const User = new mongoose.Schema(
{
firstName: {
type: String,
required: true
},
lastName: {
type: String,
required: true
},
picture: {
type: String
},
password: {
type: String,
select: false
},
email: {
required: true,
type: String,
unique: true
}
},
{
timestamps: true
}
);
User.index({
firstName: 'text',
lastName: 'text',
});
module.exports = mongoose.model('User', User);
and this is the controller file
const User = require('./model');
const { hash, compareHash } = require('../lib/util');
const { createToken, findUserByToken } = require('../lib/auth');
const cookieIsSecure = process.env.ENVIRONMENT === 'production';
exports.create = async (req, res) => {
const password = await hash(req.body.password);
const rawUser = {
...req.body,
password,
};
User.create(rawUser)
.then(async user => {
return user.save();
})
.then(async user => {
const newUser = user.toObject();
res.send(newUser);
})
.catch(err => {
if (err.code === 11000) {
res.status(400).send({ message: 'A user with this email address has already registered.' });
return;
}
res.status(500).send({ message: 'An unexpected error occurred' });
});
};
it always return the 500 error "an unexpected error occurred"
which is not really specific. and i do not know what is the problem exactly. but I am sure it has something to do with the model.create() it does not return any promise.
Here you are mixing methods. create doesn't want save in it as it's implicit:
https://mongoosejs.com/docs/api.html#model_Model.create
Please try this, I've refactored your code a bit and added much easier to read and use try/catch:
const rawUser = new User({ ...req.body, password});
try {
await rawUser.save();
res.status(201).send(newUser);
} catch(err) {
if (err.code === 11000) return res.status(400).send({ message: 'A user with this email address has already registered.' });
res.status(500).send({ message: 'An unexpected error occurred' });
}
You need to use async/await like this:
exports.create = async (req, res) => {
try {
const password = await hash(req.body.password);
const rawUser = {
...req.body,
password
};
const user = await User.create(rawUser);
const newUser = user.toObject();
res.send(newUser);
} catch (err) {
console.log("ERROR: ", err);
if (err.code === 11000) {
return res.status(400).send({
message: "A user with this email address has already registered."
});
}
res.status(500).send({ message: "An unexpected error occurred" });
}
};

Categories