I am trying to add test data for my test:
const chai = require("chai");
const expect = require("chai").expect;
chai.use(require("chai-http"));
const app = require("../server.js"); // Our app
const user = require("../app/controllers/user.controller.js");
describe("API endpoint /users", function() {
this.timeout(5000); // How long to wait for a response (ms)
before(function() {
const users = [
{
email: "ssss#ss.com",
givenName: "eee",
familyName: "www2"
},
{
email: "ssss#ss.com",
givenName: "eee",
familyName: "www2"
}
];
user.create(users);
done();
});
// GET - List all data
it("should return all users", function() {
return chai.request(app).get("/users").then(function(res) {
expect(res).to.have.status(200);
expect(res).to.be.json;
expect(res.body).to.be.an("array");
});
});
});
I get the error:
1) API endpoint /users
"before all" hook:
TypeError: Cannot destructure property email of 'undefined' or 'null'.
at Object.exports.create (app\controllers\user.controller.js:5:13)
How can I add test data?
Controller:
const user = require("../models/user.model.js");
const validator = require("email-validator");
// Create and Save a new user
exports.create = (req, res) => {
const { query: { email, givenName, familyName } } = req;
// Validate request
if (!validator.validate(email) || !givenName || !familyName) {
return res.status(400).send({
message:
"Please use a valid email address, given name and family name."
});
}
// Create a user
const User = new user({
email,
givenName,
familyName
});
// Save user in the database
User.save()
.then(data => {
res.send(data);
})
.catch(err => {
res.status(500).send({
message:
err.message || "Error occurred while creating the user."
});
});
};
// Retrieve and return all users from the database.
exports.findAll = (req, res) => {
user
.find()
.then(users => {
res.send(users);
})
.catch(err => {
res.status(500).send({
message:
err.message || "An error occurred while retrieving users."
});
});
};
// Find a single user with a userId
exports.findOne = (req, res) => {
user
.findById(req.params.userId)
.then(user => {
if (!user) {
return res.status(404).send({
message: "user not found with id " + req.params.userId
});
}
res.send(user);
})
.catch(err => {
if (err.kind === "ObjectId") {
return res.status(404).send({
message: "user not found with id " + req.params.userId
});
}
return res.status(500).send({
message: "Error retrieving user with id " + req.params.userId
});
});
};
// Update a user identified by the userId in the request
exports.update = (req, res) => {
// Validate Request
if (!req.body.content) {
return res.status(400).send({
message: "user content can not be empty"
});
}
// Find user and update it with the request body
user
.findByIdAndUpdate(
req.params.userId,
{
title: req.body.title || "Untitled user",
content: req.body.content
},
{ new: true }
)
.then(user => {
if (!user) {
return res.status(404).send({
message: "user not found with id " + req.params.userId
});
}
res.send(user);
})
.catch(err => {
if (err.kind === "ObjectId") {
return res.status(404).send({
message: "user not found with id " + req.params.userId
});
}
return res.status(500).send({
message: "Error updating user with id " + req.params.userId
});
});
};
// Delete a user with the specified userId in the request
exports.delete = (req, res) => {
user
.findByIdAndRemove(req.params.userId)
.then(user => {
if (!user) {
return res.status(404).send({
message: "user not found with id " + req.params.userId
});
}
res.send({ message: "user deleted successfully!" });
})
.catch(err => {
if (err.kind === "ObjectId" || err.name === "NotFound") {
return res.status(404).send({
message: "user not found with id " + req.params.userId
});
}
return res.status(500).send({
message: "Could not delete user with id " + req.params.userId
});
});
};
create function expects a single user while it receives an array of users as an argument. The problem with it is that it's a middleware, it doesn't return a promise, so it cannot be efficiently chained. It also causes side effects and calls res.send while this is not needed for what it's used here.
Mongoose model should be used directly here, its create accepts an array. The block should return a promise in order to not cause race conditions in tests:
const User = require(".../user.model.js");
...
before(function() {
const users = [...];
return User.create(users);
});
Related
I've been struggling with Bcrypt on my MERN project I'm trying to create an authentication system I'm trying to run tests on Postman and I'm not sure why do I keep getting the error: "Illegal arguments: undefined, string at Object.bcrypt.hashSync"
this is my postman request:
this is the Controller Code:
const config = require("../config/auth.config");
const db = require("../models");
const User = db.user;
const Role = db.role;
var jwt = require("jsonwebtoken");
var bcrypt = require("bcryptjs");
exports.signup = (req, res) => {
const user = new User({
username: req.body.username,
email: req.body.email,
password: bcrypt.hashSync(req.body.password, 8),
});
user.save((err, user) => {
if (err) {
res.status(500).send({ message: err });
return;
}
if (req.body.roles) {
Role.find(
{
name: { $in: req.body.roles },
},
(err, roles) => {
if (err) {
res.status(500).send({ message: err });
return;
}
user.roles = roles.map((role) => role._id);
user.save((err) => {
if (err) {
res.status(500).send({ message: err });
return;
}
res.send({ message: "User was registered successfully!" });
});
}
);
} else {
Role.findOne({ name: "user" }, (err, role) => {
if (err) {
res.status(500).send({ message: err });
return;
}
user.roles = [role._id];
user.save((err) => {
if (err) {
res.status(500).send({ message: err });
return;
}
res.send({ message: "User was registered successfully!" });
});
});
}
});
};
exports.signin = (req, res) => {
User.findOne({
username: req.body.username,
})
.populate("roles", "-__v")
.exec((err, user) => {
if (err) {
res.status(500).send({ message: err });
return;
}
if (!user) {
return res.status(404).send({ message: "User Not found." });
}
var passwordIsValid = bcrypt.compareSync(
req.body.password,
user.password
);
if (!passwordIsValid) {
return res.status(401).send({ message: "Invalid Password!" });
}
var token = jwt.sign({ id: user.id }, config.secret, {
expiresIn: 86400, // 24 hours
});
var authorities = [];
for (let i = 0; i < user.roles.length; i++) {
authorities.push("ROLE_" + user.roles[i].name.toUpperCase());
}
req.session.token = token;
res.status(200).send({
id: user._id,
username: user.username,
email: user.email,
roles: authorities,
});
});
};
exports.signout = async (req, res) => {
try {
req.session = null;
return res.status(200).send({ message: "You've been signed out!" });
} catch (err) {
this.next(err);
}
};
The error message:
Illegal arguments: undefined, string at Object.bcrypt.hashSync wants to say that you're passing undefined as an argument to the hashSync function. We need to fix this error.
Take a closer look at this line where the error occurs:
password: bcrypt.hashSync(req.body.password, 8),
req.body.password is undefined, you can verify it by console.log(req.body.password). What's wrong is that you are sending data as URL parameters. So req.body is an empty object and req.body.password is undefined.
In Postman, select the Body tab, choose JSON format, then type your data as a JSON object. Then, in your code, use express.json() middleware to parse requests in JSON format. You'll have the desired output.
You can see my example request in Postman below:
I am using SendGrid to send the user the reset password link that goes with two parameters (The user._id and token). I have another component that saves the user's changed the password but all I get is an error user. save is not a function
Email helper Code.
import sendGrid from "#sendgrid/mail";
export class sendGridEmail {
static async sendResetPasswordEmail(email, token, id) {
sendGrid.setApiKey(process.env.SENDGRID_API_KEY);
const msg = {
to: `${email}`,
from: `${process.env.VERIFIED_SENDER}`, // Change to your verified sender
subject: "RESET YOUR PASSWORD",
text: `Follow this link to reset your password: ${process.env.BASE_URL}/${id}/${token}`,
};
return sendGrid
.send(msg)
.then(() => {
console.log(`password rest link has been sent to: ${email}`);
})
.catch((err) => {
console.log(err);
});
}
sendLink Component
export const resetUserPassword = asynchandler(async (req, res) => {
const { email } = req.body;
const user = await userModel.findOne({ email });
if (!user) {
res.status(404);
res.json({ message: "the email provided was not found" });
} else if (user) {
const token = AuthToken(user._id);
try {
await sendGridEmail.sendResetPasswordEmail(user.email, token, user._id);
res.status(200);
res.json({
message: `a link to reset your password has been sent to: ${user.email}`,
});
} catch (error) {
res.status(500);
res.json({ message: error });
}
} else {
res.status(500);
res.json({ message: "Internal Server Error" });
}
});
The Component that tries to update the password in the Database but I get an error user.save() is not a function
export const saveResetPassword = asynchandler(async (req, res) => {
const { id, authorization } = req.params;
const user = userModel.findOne(req.params.id);
const private_key=process.env.PRIVATE_KEY
const payload = jwt.verify(authorization, private_key);
if (user._id === id || payload.id) {
try {
user.password = req.body.password;
await user.save();
} catch (error) {
res.status(404);
res.json({ message: `an error occured: ${error}` });
}
}else{
res.status(500)
res.json({message: "an error occured"})
}
});
My Routes
import { loginUser, registerUser, resetUserPassword, saveResetPassword } from "./controllers/user.controller.js";
export const Routes =(app)=>{
app.get("/health", (req,res) => {
res.send(200).json({message:"Server health check is Ok"});
});
// user api's
app.post('/api/registeruser', registerUser);
app.post('/api/loginuser', loginUser);
app.post('/api/password-reset', resetUserPassword);
app.post("/api/save-password/:id/:authorization", saveResetPassword);
}
const user = await userModel.findOne(req.params.id);
You forgot await, model.findOne() returns a Promise
Im trying to connect to an api I made with node.js but it looks like there isn't a clear way to do it if there requires a body in the request. Im able to connect to it with postman on localhost because there i can set the params in the body but when i use the heroku url it throws an error. I read that apple made it so swift can no longer make requests with a body anymore. In another project i was able to successfully connect to an api made with python but instead of passing the data to the body it was passed as params. How can I change the api/swift code to accept the data as params in swift instead of the body?
Heres the function call in swift:
func logoin() {
let params: Parameters = [
"email" : "test#email.com",
"password" : "1234"
]
AF.request("https://app_name.herokuapp.com/user/login", method: .post, parameters: params, encoding: JSONEncoding.default, headers: ["Content-Type": "application/json"]).responseJSON { response in
if let error = response.error {
print(error)
}
else{
let jsonData = try! JSON(data: response.data!)
print("json data", response)
}
}
}
Heres the api in node.js
const User = require('../models/user')
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
exports.get_user = (req, res, next) => {
const id = req.params.userId;
User.findById(id)
.exec()
.then(doc => {
console.log('From database', doc);
if (doc) {
res.status(200).json(doc);
}
else {
res.status(404).json({ message: 'No valid entry found for user ID.' });
}
})
.catch(err => {
console.log(err);
res.status(500).json({ error: err });
});
};
exports.user_login = (req, res, next) => {
User.find({ email: req.body.email }).exec().then(users => {
if (users.length < 1) {
return res.status(401).json({
message: 'Auth failed1'
});
}
bcrypt.compare(req.body.password, users[0].password, (err, result) => {
if (err) {
return res.status(401).json({
message: 'Auth failed2'
});
}
if (result) {
const token = jwt.sign({
email: users[0].email,
userId: users[0]._id
}, process.env.JWT_KEY)
return res.status(200).json({
message: 'Auth successful',
token: token
});
}
res.status(401).json({
message: 'Auth failed3'
});
});
}).catch(err => {
console.log(err);
res.status(500).json({
error: err
});
});
};
exports.delete_user = (req, res, next) => {
User.deleteOne({ _id: req.params.userId })
.exec()
.then(result => {
res.status(200).json({
message: 'User deleted'
});
})
.catch(err => {
console.log(err);
res.status(500).json({
error: err
});
});
};
exports.update_user = (req, res, next) => {
const id = req.params.userId;
const updateOps = {};
for (const ops of req.body) {
updateOps[ops.propName] = ops.value;
}
User.updateOne({ _id: id }, { $set: updateOps })
.exec()
.then(result => {
console.log(result);
res.status(200).json(result);
})
.catch(err => {
console.log(err);
res.status(500).json({
error: err
});
});
};
exports.signup = (req, res, next) => {
User.find({ email: req.body.email }).exec().then(user => {
if (user.length >= 1) {
return res.status(409).json({
message: 'User already exists'
});
}
else {
bcrypt.hash(req.body.password, 10, (err, hash) => {
if (err) {
return res.status(500).json({
error: err
});
}
else {
const user = new User({
firstname: req.body.firstname,
lastname: req.body.lastname,
email: req.body.email,
password: hash
});
user
.save()
.then(result => {
console.log(result)
res.status(201).json({
message: 'Successfully created user'
});
})
.catch(err => {
console.log(err);
res.status(500).json({
error: err
});
})
}
})
}
}).catch();
};
I don't understand it. I am not able to login. User is already in my database, and when I log in, it simply says:
POST http://localhost:3000/api/v1/users/login 402 (Payment Required)
When I register for the first time, and then login, login is successful. If I logout, and then try to log in with that same email and password, it's throwing me the above error. I'm not even using someone's API. It's my own created one. It's sending me a response of "incorrect password"
Here's the controller:
loginUser: (req, res, next) => {
const { email, password } = req.body
if (!email || !password) {
return res.status(400).json({ message: "Email and password are must" })
}
User.findOne({ email }, (err, user) => {
if (err) {
return next(err)
} else if (!validator.isEmail(email)) {
return res.status(400).json({ message: "Invalid email" })
} else if (!user) {
return res.status(402).json({ error: "User not found" })
} else if (!user.confirmPassword(password)) {
return res.status(402).json({ error: "incorrect password" })
}
})
}
User model
const mongoose = require("mongoose")
const bcrypt = require("bcrypt")
const Schema = mongoose.Schema
const userSchema = new Schema({
username: { type: String, required: true },
email: { type: String, reuired: true },
password: { type: String, required: true },
posts:[{ type: Schema.Types.ObjectId, ref: "Post" }]
}, { timestamps: true })
userSchema.pre("save", function (next) {
if (this.password) {
const salt = bcrypt.genSaltSync(10)
this.password = bcrypt.hashSync(this.password, salt)
}
next()
})
userSchema.methods.confirmPassword = function (password) {
return bcrypt.compareSync(password, this.password)
}
const User = mongoose.model("User", userSchema)
module.exports = User
registration controller
registerUser: (req, res) => {
const { username, email, password } = req.body
User.create(req.body, (err, createdUser) => {
if (err) {
return res.status(500).json({ error: "Server error occurred" })
} else if (!username || !email || !password) {
return res.status(400).json({ message: "Username, email and password are must" })
} else if (!validator.isEmail(email)) {
return res.status(400).json({ message: "Invaid email" })
} else if (password.length < 6) {
return res.status(400).json({ message: "Password should be of at least 6 characters" })
}
else {
return res.status(200).json({ user: createdUser })
}
})
}
Edit
loginUser: async (req, res, next) => {
const { email, password } = req.body
if (!email || !password) {
return res.status(400).json({ message: "Email and password are must" })
}
await User.findOne({ email }, (err, user) => {
if (err) {
return next(err)
} else if (!validator.isEmail(email)) {
return res.status(400).json({ message: "Invalid email" })
} else if (!user) {
return res.status(402).json({ error: "User not found" })
} else if (!user.confirmPassword(password)) {
return res.status(402).json({ error: "incorrect password" })
}
})
}
new post controller
newPost: (req, res) => {
const data = {
title: req.body.title,
content: req.body.content,
user: req.user.userId
}
Post.create(data, (err, newPost) => {
if (err) {
return res.status(500).json({ error: err })
} else if (!newPost) {
return res.status(400).json({ message: "No Post found" })
} else if (newPost) {
User.findById(req.user.userId, (err, user) => {
user.posts.push(newPost._id) //pushing posts documnet objectid to the post array of the user document
user
.save()
.then(() => {
return res.json(200).json({ user })
})
.catch(err => {
return res.status(500).json({ error: err })
})
})
}
})
}
You might want to refactor your code so that you do the bcrypt operations in controller not in the model. You are checking this.password after the user is updated (creating new posts) and since this is the user, the below code is being met each time you update the user object.
if (this.password) {
const salt = bcrypt.genSaltSync(10)
this.password = bcrypt.hashSync(this.password, salt)
}
So your hashing it every time you update the user (create a post). Instead, remove the above code from the userSchema.pre(...) and try doing the bcrypt hashing only when the user first registers.
registerUser: (req, res) => {
var { username, email, password } = req.body
if (password) {
const salt = bcrypt.genSaltSync(10)
password = bcrypt.hashSync(password, salt)
}
User.create(req.body, (err, createdUser) => {
if (err) {
return res.status(500).json({ error: "Server error occurred" })
} else if (!username || !email || !password) {
return res.status(400).json({ message: "Username, email and password are must" })
} else if (!validator.isEmail(email)) {
return res.status(400).json({ message: "Invaid email" })
} else if (password.length < 6) {
return res.status(400).json({ message: "Password should be of at least 6 characters" })
}
else {
return res.status(200).json({ user: createdUser })
}
})
}
This way the hashing occurs only once at the creation of the user and should remain consistent throughout other operations.
As for the Can't set headers after they are sent error, you might be sending a response twice, since the error appears to come from the posts controller. You are likely sending the user response and the post response. Maybe don't send the posts response since you will be sending it along in the user response.
More info on the error here.
I know this question gets asked a lot but I cannot tell where I am sending multiple headers. The data is getting stored in the database and then it crashes. I am fairly new to Node/Express and I think I might be missing something fundamental here.
I have tried reading what I could find on stackoverflow and figured out the reason I am getting this error is because it is sending multiple header requests. Tried updating the code with little tweaks but nothing has worked so far.
Thanks for the help.
Dashboard Controller -
exports.getGymOwnerMembersAdd = (req, res, next) => {
let message = req.flash('error');
if(message.length > 0) {
message = message[0];
} else {
message = null;
}
const oldInput = {
...
};
Membership
.find()
.then(memberships => {
res.render('gym-owner/members-add', {
memberships: memberships,
oldInput: oldInput,
errorMessage: message,
pageTitle: 'Add Members',
path: '/gym-owner-dashboard/members-add',
validationErrors: []
});
})
.catch(err => {
console.log(err);
});
}
exports.postGymOwnerMembersAdd = (req, res, next) => {
const membershipId = req.body.membershipLevel;
const errors = validationResult(req);
let message = req.flash('error');
if(message.length > 0) {
message = message[0];
} else {
message = null;
}
if(!errors.isEmpty()) {
Membership
.find()
.then(memberships => {
return res.status(422).render('gym-owner/members-add', {
pageTitle: 'Add Members',
path: '/gym-owner-dashboard/members-add',
errorMessage: errors.array()[0].msg,
message: message,
memberships: memberships,
oldInput: {
...
},
validationErrors: errors.array()
});
})
.catch(next);
}
bcrypt
.hash(password, 12)
.then(hashedPassword => {
const user = new User({
...
});
return user.save();
})
.then(result => {
res.redirect('/gym-owner-dashboard/members');
})
.catch(err=> {
console.log(err);
});
}
Dashboard Routes And Validation-
router.get('/gym-owner-dashboard/members-add', isAuth, isGymOwner, dashboardController.getGymOwnerMembersAdd);
router.post(
'/gym-owner-dashboard/members-add',
isAuth, isGymOwner,
[
check('name')
.isAlpha().withMessage('Names can only contain letters.')
.isLength({ min: 2 }).withMessage('Please enter a valid name')
.trim(),
check('email')
.isEmail().withMessage('Please enter a valid email.')
.custom((value, { req }) => {
return User.findOne({
email: value
}).then(userDoc => {
console.log('Made it here!');
if(userDoc) {
return Promise.reject('E-mail already exists, please pick a different one.');
};
});
})
.normalizeEmail(),
...
check(
'password',
'Please enter a password at least 5 characters.'
)
.isLength({ min: 5 })
.trim(),
check('confirmPassword')
.trim()
.custom((value, { req }) => {
if(value !== req.body.password) {
throw new Error('Passwords have to match!');
}
return true;
})
],
dashboardController.postGymOwnerMembersAdd
);
Expected Results
Create a new user while passing validation.
Actual Results
A new user is created and saved to Mongodb. The user gets redirected back to the user creation page with an error that the user is undefined. The server crashes with the error "Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client"
I understand you have a bug in "postGymOwnerMembersAdd".
if(!errors.isEmpty()) {
Membership
.find()
.then(memberships => {
return res.status(422).render('gym-owner/members-add', { // this return refers to cb but not to middleware
pageTitle: 'Add Members',
path: '/gym-owner-dashboard/members-add',
errorMessage: errors.array()[0].msg,
message: message,
memberships: memberships,
oldInput: {
...
},
validationErrors: errors.array()
});
})
.catch(next);
}
bcrypt
.hash(password, 12)
.then(hashedPassword => {
const user = new User({
...
});
return user.save();
})
.then(result => {
res.redirect('/gym-owner-dashboard/members');
})
.catch(err=> {
console.log(err);
});
Thus, both the "return res.status(422).render()" and the "res.redirect('/gym-owner-dashboard/members')" will be executed, and this trigger error (header set after they are sent).
I mean two solutions to the problem
First: use async/await
exports.postGymOwnerMembersAdd = async (req, res, next) => {
const membershipId = req.body.membershipLevel;
const errors = validationResult(req);
let message = req.flash('error');
if(message.length > 0) {
message = message[0];
} else {
message = null;
}
if(!errors.isEmpty()) {
try {
const memberships = await Membership.find();
return res.status(422).render('gym-owner/members-add', {
pageTitle: 'Add Members',
path: '/gym-owner-dashboard/members-add',
errorMessage: errors.array()[0].msg,
message: message,
memberships: memberships,
oldInput: {
...
},
validationErrors: errors.array()
};
} catch (err) {
next(err);
}
}
const hashedPassword = await bcrypt.hash(password, 12);
const user = new User({
...
});
await user.save();
return res.redirect('/gym-owner-dashboard/members');
};
Second: use else
exports.postGymOwnerMembersAdd = (req, res, next) => {
const membershipId = req.body.membershipLevel;
const errors = validationResult(req);
let message = req.flash('error');
if(message.length > 0) {
message = message[0];
} else {
message = null;
}
if(!errors.isEmpty()) {
Membership
.find()
.then(memberships => {
return res.status(422).render('gym-owner/members-add', {
pageTitle: 'Add Members',
path: '/gym-owner-dashboard/members-add',
errorMessage: errors.array()[0].msg,
message: message,
memberships: memberships,
oldInput: {
...
},
validationErrors: errors.array()
});
})
.catch(next);
} else {
bcrypt
.hash(password, 12)
.then(hashedPassword => {
const user = new User({
...
});
return user.save();
})
.then(result => {
res.redirect('/gym-owner-dashboard/members');
})
.catch(err=> {
console.log(err);
});
}
}