jest.spyOn "Number of calls: 0", but actual implementation still called - jestjs

I want to spy on the hashPassword function in authController to see if it's called with the correct arguments from the user model, while keeping it's implementation.
number of calls 0
The mock function is being set but is not being called, meanwhile the original hashPassword function is being called. It seems that it can be tricky to make jest.spyOn work, but I think it should work as I am using 2 separate files.
logged controller and user
test
const request = require('supertest')
const app = require('../src/app.js')
const db = require('./db.js')()
const authController = require('../src/controllers/authController')
//in memory database
beforeAll(async () => await db.connect())
afterEach(async () => await db.clear())
afterAll(async () => await db.disconnect())
it('hashes the password', async () => {
const hashPassword = jest.spyOn(authController, 'hashPassword')
console.log(require('../src/controllers/authController'))
const user = await request(app)
.post('/auth/signup')
.send({ name: 'User', password: 'password', confirmPassword: 'password' })
console.log(user.body)
expect(hashPassword).toHaveBeenCalledWith('password')
})
controller
const bcrypt = require('bcrypt')
async function hashPassword(password) {
return await bcrypt.hash(password, 12)
}
function test() {
return 'not mocked'
}
module.exports = { hashPassword, test }
model
const mongoose = require('mongoose')
const { isEmail } = require('validator')
const { hashPassword } = require('../controllers/authController')
const userSchema = mongoose.Schema({
name: {
type: String,
unique: true,
required: [true, 'Username required'],
minLength: [4, 'Username should have at least 4 characters'],
},
email: {
type: String,
unique: true,
sparse: true, //ignore documents without the email field when determining uniqueness
validate: [isEmail, 'Please enter a valid email.'],
},
password: {
type: String,
required: [true, 'Password required'],
minLength: [6, 'Password should have at least 6 characters'],
},
confirmPassword: {
type: String,
required: true,
validate: [matches, 'The passwords do not match'],
},
hashedPassword: {
type: String,
select: false,
},
})
function matches() {
const { password, confirmPassword } = this
return password === confirmPassword
}
userSchema.post('validate', async function (doc) {
//hash password
this.hashedPassword = await hashPassword(this.password)
//remove plain password before saving
doc.password = undefined
doc.confirmPassword = undefined
})
const User = mongoose.model('User', userSchema)
module.exports = User
I am guessing the model file isn't using the mock version of hashPassword for some reason. However, if I simply use mock, it works as expected.
const request = require('supertest')
const app = require('../src/app.js')
const db = require('./db.js')()
const authController = require('../src/controllers/authController')
//in memory database
beforeAll(async () => await db.connect())
afterEach(async () => await db.clear())
afterAll(async () => await db.disconnect())
jest.mock('../src/controllers/authController')
it('hashes the password', async () => {
console.log(require('../src/controllers/authController'))
const user = await request(app)
.post('/auth/signup')
.send({ name: 'User', password: 'password', confirmPassword: 'password' })
console.log(user.body)
expect(authController.hashPassword).toHaveBeenCalled()
})
(Is it relevant that it says "[Function: hashPassword]" instead of "[Function: mockConstructor]")
logged controller
(no hashed password, meaning the model is using the mocked version of the function)
logged response
I am confused by why jest.mock works while jest.spyOn doesn't. I found many similar questions, but I haven't really found an answer.

Related

why func2 works without ValidateBeforeSave:false but func1 doesn't?

const userSchema = new mongoose.Schema({
name: {
required: [true, 'A user must have a name'],
},,
password: {
required: [true, 'A password must be set'],
},
passwordConfirm: {
required: [true, 'password confimr must be set'],
validate: {
validator: function (val) {
return val === this.password;
},
},
},
passwordResetToken: String,
});
const User = mongoose.model('User', userSchema);
My schema with custom and built-in validators.
userSchema.methods.createResetToken = async function() {
const plainToken = "some string"
this.passwordResetToken = "some strange looking string"
return plainToken;
};
The function above sets values of some properties in the Schema and returns a string to func1.
const func1 = async (req, res, next) => {
const user = await User.findOne({ //getUser });
const plainToken = await user.createResetToken();
await user.save({ validateBeforeSave: false });
//more code
};
The function above runs only when ValidateBeforeSave is set to false.
const func2 = async function (req, res, next) {
let user = await User.findOne({
// get user
});
user.password = req.body.password;
user.passwordConfirm = req.body.passwordConfirm;
user.passwordResetExpiresAt = undefined;
user.passwordResetToken = undefined;
user = await user.save();
//more code
};
The function above runs without need of ValidateBeforeSave set to false.

Circular Dependency Error for deleteMany MongoDB

I am writing the Model for my Web App API, and am getting the following circular dependency error:
Warning: Accessing non-existent property 'deleteMany' of module exports inside circular dependency
(Use node --trace-warnings ... to show where the warning was created)
.
Here is my code:
const validator = require('validator')
const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')
const Task = require('./task')
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const userSchema = new Schema({
email: {
type: String,
unique: true,
required: true,
trim: true,
lowercase: true,
validate(value) {
if (!validator.isEmail(value)) {
throw new Error('Email is invalid.')
}
}
},
password: {
type: String,
required: true,
trim: true,
minLength: 8
},
name: {
type: String,
unique: true,
required: true,
trim: true
},
tokens: [{
token: {
type: String,
required: true
}
}]
})
userSchema.pre('save', async function(next) {
const user = this
if (user.isModified('password')) {
user.password = await bcrypt.hash(user.password, 8)
}
next() // run the save() method
})
userSchema.pre('deleteOne', {document: true, query: false}, async function(next) {
const user = this
await Task.deleteMany({owner: user._id})
next()
})
userSchema.methods.toJSON = function() {
const user = this
const userObject = user.toObject()
delete userObject.password
delete userObject.__v
delete userObject.tokens
return userObject
}
userSchema.methods.generateAuthToken = async function () {
const user = this
const token = jwt.sign({ _id: user._id.toString() }, process.env.JSON_WEB_TOKEN_SECRET)
user.tokens = user.tokens.concat({ token })
await user.save()
return token
}
userSchema.statics.findByCredentials = async (email, password) => {
const user = await User.findOne({email})
if (!user) {
throw new Error('Unable to login')
}
const isMatch = await bcrypt.compare(password, user.password)
if (!isMatch) {
throw new Error('Unable to login')
}
return user
}
userSchema.virtual('tasks', {
localField: '_id',
foreignField: 'owner',
ref: 'Task'
})
const User = mongoose.model('User', userSchema);
module.exports = User
Any idea what could be going wrong? I have checked my Node.js and MongoDB versions and updated them, but continue to get this same error when I try to delete. I can provide further details of my code if necessary. The problem area in question is the one leading with userScheme.pre('deleteOne'....

Bcryptjs Compare method always returns false?

Bit stumped.. my code is identical to a tutorial I'm following for this section. However, bcryptjs.compare is always returning false.
Database is mongodb and string length limit is set to 16mb from what I read so I dont think it has to do with that.
userModel.js
const mongoose = require('mongoose')
const bcrypt = require('bcryptjs')
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
isAdmin: {
type: Boolean,
required: true,
default: false
}
},{
timestamps: true
})
userSchema.methods.comparePW = async function(password) {
console.log(await bcrypt.compare(password, this.password))
return await bcrypt.compare(password, this.password)
}
module.exports = mongoose.model('User', userSchema)
userController.js
const userModel = require('../models/userModel')
const asyncHandler = require('express-async-handler')
const userAuth = asyncHandler(async(req, res) => {
const { email, password } = req.body
// check if reqbody pw and email matches userModel pw/email
const user = await userModel.findOne({ email })
if (user && (await user.comparePW(password))) {
res.send('match')
} else {
res.send('no match')
}
})
module.exports = { userAuth }
dummy user filler data in the database
const bcrypt = require('bcryptjs')
const users = [
{
name: 'Admin',
email: 'admin#test.com',
password: bcrypt.hashSync('admin123, 10'),
isAdmin: 'true',
},
{
name: 'Max Smith',
email: 'Max#test.com',
password: bcrypt.hashSync('admin123, 10'),
},
{
name: 'Jennifer Garnett',
email: 'Jen#test.com',
password: bcrypt.hashSync('admin123, 10'),
},
]
module.exports = users
using console.log, the bcrypt.compare method always returns false.
Strange as this is how the tutorial has it and it seems to be working for the instructor.
Using Postman when I run a post request with email "admin#test.com" and password: "admin123" it is return false every time.
I tried reimporting the dummy data and also reloading data on mongodb compass.
Not sure what to do at this point to fix this issue? Thoughts?
While hashing the password you are combining the password and salt strength into a string like this bcrypt.hashSync('admin123, 10') which should be like this bcrypt.hashSync('admin123', 10). If you want to work with the current situation then u need to enter password "admin123, 10" instead of "admin123".

Unable to add properties in a MongoDB document

I am trying to implement password reset functionality in a MERN application. Whenever a user enters their email (for which they want to reset the password) and clicks on the "Send Password Reset Link" button, a POST request is made to the route "/account/forgot".
In the route handler function, I check whether any user with the given email exists or not. If a user exists, then I add resetPasswordLink and resetPasswordExpires properties to the user object and send a message "You have been emailed a password link" to the client.
The problem I am facing is I get the message at the frontend.
However, whenever I check the database, I don't see resetPasswordLink and resetPassworExpires properties being added to the user.
Where is the problem?
The code snippets are given below:
server/routes/passwordResetRoutes.js
const express = require("express");
const crypto = require("crypto");
const asyncHandler = require("express-async-handler");
const User = require("../models/userModel");
const router = express.Router();
router.post(
"/forgot",
asyncHandler(async (req, res, next) => {
const user = await User.findOne({ email: req.body.email });
if (user) {
user.passwordResetToken = crypto.randomBytes(20).toString("hex");
user.passwordResetExpires = Date.now() + 3600000;
await user.save();
res.json({
message: "You have been emailed a password reset link",
});
} else {
const err = new Error("No account with that email exists");
err.status = 404;
next(err);
}
})
);
module.exports = router;
server/models/userModel.js
const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");
const userSchema = new mongoose.Schema({
firstName: {
type: String,
required: true,
},
lastName: {
type: String,
required: true,
},
email: {
type: String,
unique: true,
required: true,
},
password: {
type: String,
required: true,
},
resetPasswordToken: {
type: String,
},
resetPasswordExpires: {
type: Date,
},
});
userSchema.methods.matchPassword = async function (incomingPassword) {
return await bcrypt.compare(incomingPassword, this.password);
};
userSchema.pre("save", async function (next) {
if (!this.isModified("password")) {
next();
}
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
});
const User = mongoose.model("User", userSchema);
module.exports = User;
You're trying to update the passwordResetToken and passwordResetExpires fields but they are not present in the User model. That's why the user.save() call does nothing. They should be resetPasswordToken and resetPasswordExpires respectively.
user.resetPasswordToken = crypto.randomBytes(20).toString('hex')
user.resetPasswordExpires = Date.now() + 3600000
await user.save()

How to show Errors from Mongoose?

I have a user I can save in MongoDB, when I enter correct data, the save works.
But when I enter wrong data, I can't catch the errors to be seen for the user. All I can see is this on the code editor:
...UnhandledPromiseRejectionWarning: ValidationError: User validation
failed: username: username is not there!...
This error "kills" the server, and so I'm not rendering home-guest template.
The question is how I can catch the errors and show them to the user?
Schema:
const mongoose = require("mongoose")
const userSchema = new mongoose.Schema({
username: {
type: String,
required: [true, "username is not there!"],
minlength: 3,
maxlength: 20,
},
email: {
type: String,
required: true,
},
password: {
type: String,
required: true,
minlength: 6,
maxlength: 20,
},
})
module.exports = mongoose.model("User", userSchema)
Controller:
const mongoose = require("mongoose")
const userModel = require("../models/userModel")
exports.signUp = async (req, res) => {
const { username, email, password } = req.body
try {
const user = await new userModel({
username,
email,
password,
})
user.save()
} catch (error) {
res.render("home-guest", { error })
}
}
You just need to add an await to the save operation, since that's also async:
const mongoose = require("mongoose")
const userModel = require("../models/userModel")
exports.signUp = async (req, res) => {
const { username, email, password } = req.body
try {
const user = await new userModel({
username,
email,
password,
})
// Wait for the save to complete, also allowing you to catch errors
await user.save()
} catch (error) {
res.render("home-guest", { error })
}
}
EDIT: And note that you do not need an async in front of new userModel(). new cannot return a promise, it is always synchronous.

Resources