Trying to test a nodejs backend using jest.
this test is passing 70% of the time but sometimes fails.
my test:
both userInput and copycatUserInput are saved to the database and create a duplicate email.
test("Register with the same email twice shoud throw an error.", async () => {
const userInput: ICreateUserInput = {
email: "same-email#gmail.com",
username: "username",
password: "tesT$1234",
};
const copycatUserInput: ICreateUserInput = {
email: "same-email#gmail.com",
username: "differentUsername",
password: "tesT$1234",
};
> await registerUser(userInput);
> await expect(registerUser(copycatUserInput)).rejects.toThrow(/(Email address is already exists)/);
});
the fail reason:
expect(received).rejects.toThrow()
Received promise resolved instead of rejected
Resolved to value: {"token": "eyJhb...
here is my mongoose schema:
both username and email fields are unique
const schemaOptions: SchemaOptions = { timestamps: true };
const userSchema = new Schema(
{
username: {
type: String,
required: true,
unique: true,
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
},
password: {
type: String,
required: true,
},
status: String,
},
schemaOptions,
);
export default model<IUser>("User", userSchema);
and registerUser function:
export default async (registerInput: ICreateUserInput): Promise<ILoginUserResult> => {
/**
* validate props.
*/
if (!isEmail(registerInput.email)) throw new Error("Email address is not valid.");
if (!isValidUsername(registerInput.username)) throw new Error("Username is not valid.");
if (!isStrongPassword(registerInput.password)) throw new Error("Password is not valid.");
/**
* gnerate hashed password.
*/
try {
const hashPassword = await createHashedPassword(registerInput.password);
/**
* create new user.
*/
const doc: ICreateUserInput = {
email: registerInput.email,
username: registerInput.username,
password: hashPassword,
status: registerInput.status ? registerInput.status : "",
};
/**
* save the user in the database.
*/
const user = await new User(doc).save();
return {
user,
token: getAuthToken(user),
};
} catch (error) {
/**
* throws a duplicate email error.
*/
if (error.message && `${error.message}`.includes("email_1 dup key:")) {
throw new Error("Email address is already exists");
}
/**
* throws a duplicate username error.
*/
if (error.message && `${error.message}`.includes("username_1 dup key:")) {
throw new Error("Username is already exists");
}
/**
* may be that mongoose or bcrypt are throwing..
*/
Logger.error(`auth.registerUser => ${error}`);
throw new Error(`Error: Failed to register user: ${error.message}`);
}
i also got useCreateIndex: true in the connection options
and tried to wait for the indexes to be created:
user.once("index", () => {
user = await new User(doc).save();
});
Thank you for your help
EDIT:
ended up running tests separately, make a new connection on each testsuit (beforeAll) can knock things out :(
"scripts": {
"test": "jest --runInBand",
},
Related
My problem is to find the user with the resetPassword in the database and the hashToken does not match or passwordResetExpires the time has expired. How can I fix this?
How this should work find the user and set the password, reset the passwordReset and passwordResetExpires.
authController.js
exports.forgotPassword = catchAsync(async (req, res, next) => {
//1) Get user based on POSTed email
const user = await User.findOne({ email: req.body.email });
if (!user) {
return next(
new AppError('There is no user with email ' + req.body.email, 404)
);
}
//2) Generate the random reset token
const resetToken = user.createPasswordResetToken();
await user.save({ validateBeforeSave: false });
//3) Send it to user's email
const resetURL = `${req.protocol}://${req.get(
'host'
)}/api/v1/users/resetPassword:${resetToken}`;
const message = `Forgot your password? Submit a PATCH request with your new password and passwordConfirm to: ${resetURL}.\nIf you didn't forget your password, please ignore this email!`;
try {
await sendEmail({
email: user.email,
subject: 'You password on Account reset token (valid for 10 min)',
message,
});
res.status(200).json({
status: 'success',
message: 'Token sent to email',
token: resetToken
});
} catch (err) {
user.passwordResetToken = undefined;
user.passwordResetExpires = undefined;
await user.save({ validateBeforeSave: false });
return next(
new AppError(
'There was an error sending the email. Try again later!!!',
500
)
);
}
});
/**
*
* #param {*} req
* #param {*} res
* #param {*} next
*/
exports.resetPassword = catchAsync(async (req, res, next) => {
//!) Get user based on the token
const hashedToken = crypto
.createHash('sha256')
.update(req.params.token)
.digest('hex');
console.log(hashedToken);
const user = await User.findOne({
passwordResetToken: hashedToken,
passwordResetExpires: { $gt: Date.now() },
});
console.log(user);
//2) IF token has not expired, and there is user, set the new password
if (!user) {
return next(new AppError('Token is invalid or has expired', 400));
}
user.password = req.body.password;
user.passwordConfirm = req.body.passwordConfirm;
user.passwordResetToken = undefined;
user.passwordResetExpires = undefined;
await user.save();
//3) Update changedPasswordAt property for the user
//4) Log the user in , send JWT
const token = signToken(user._id);
res.status(200).json({
statusbar: 'success',
token,
});
userModel
/* eslint-disable no-console */
const crypto = require('crypto');
const mongoose = require('mongoose');
const validator = require('validator');
const bcrypt = require('bcrypt');
// const validator = require('validator');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Please tell us your full name'],
trim: true,
},
email: {
type: String,
required: [true, 'Please tell us your email address'],
unique: true,
lowercase: true,
validate: [validator.isEmail, 'Please enter a valid email address'],
},
role: {
type: String,
required: [true, 'A tour must have a role'],
enum: {
values: ['admin', 'user', 'lead-guide', 'guide'],
message: 'Role is either: admin, user, lead-guide or guide',
},
default: 'user',
},
photo: String,
password: {
type: String,
required: [true, 'A use must have a password'],
minlength: 8,
select: false,
},
passwordConfirm: {
type: String,
required: [true, 'A use must have a password'],
validate: {
// This only work on CREATE and SAVE!!!
validator: function (el) {
return el === this.password;
},
message: 'Password is not the same',
},
},
active: {
type: Boolean,
default: true,
},
passwordChangeAt: Date,
passwordResetToken: String,
passwordResetExpires: Date,
});
userSchema.pre('save', async function (next) {
//Only run this function if password was actually modified
if (!this.isModified('password')) return next();
// hash the password with cost of 14
this.password = await bcrypt.hash(this.password, 14);
//Delete the passwordConfirm because it only need as a input not as output to the database
this.passwordConfirm = undefined;
next();
});
userSchema.methods.correctPassword = function (
candidatePassword,
userPassword
) {
return bcrypt.compare(candidatePassword, userPassword);
};
userSchema.methods.changePasswordAfter = function (JWTTimestamp) {
if (this.passwordChangeAt) {
const changeTimestamp = parseInt(
this.passwordChangeAt.getTime() / 1000,
10
);
return JWTTimestamp < changeTimestamp;
}
// False means that it is not changed
return false;
};
userSchema.methods.createPasswordResetToken = function () {
const resetToken = crypto.randomBytes(32).toString('hex');
this.passwordResetToken = crypto.createHash('sha256').update(resetToken).digest('hex');
console.log({resetToken}, this.passwordResetToken);
this.passwordResetExpires = Date.now() + 10 * 60 * 1000;
return resetToken;
};
const User = mongoose.model('User', userSchema);
module.exports = User;
Initially, the project was set up with promise support, and all queries used promise like method.then().catch() later some were converted to try-catch with async await. All worked fine until a few weeks ago when all of a sudden some methods stopped working, I have tried converting the methods to many different variations from promise to callback and to try-catch. await new Model(object).save() does not save the record. I am using mongoose.createConnection because I need to connect to two databases.
Here is how I init my DB
const mongoose = require("mongoose");
mongoose.Promise = require('bluebird');
function makeNewConnection(uri, id) {
const db = mongoose.createConnection(uri);
db.on("error", function(error) {
console.log(
`MongoDB :: connection ${this.name} :: ${id} ${JSON.stringify(error)}`
);
db.close().catch(() =>
console.log(`MongoDB :: failed to close connection ${this.name}`)
);
});
db.on("connected", async function() {
mongoose.set("debug", function(col, method, query, doc) {
console.log(
`MongoDB :: ${
this.conn.name
} :: ${id} ${col}.${method}(${JSON.stringify(query)},${JSON.stringify(
doc
)})`
);
});
console.log(`MongoDB :: connected ${this.name} :: ${id}`);
require("../models/notification.model");
if (process.env.DATABASE_ENV === "local" && id === "cloud") {
require("../helpers/data.sync.helper");
}
});
db.on("disconnected", function() {
console.log(`MongoDB :: disconnected ${this.name} :: ${id}`);
});
return db;
}
// Use
let local, cloud;
if (process.env?.DATABASE_ENV === "local") {
// Connect to local database
local = makeNewConnection(
`mongodb://${process.env.DATABASE_USER}:${process.env.DATABASE_PASS}#127.0.0.1:27017/Eyemasters?retryWrites=true&authSource=admin&useNewUrlParser=true&useUnifiedTopology=true&w=majority`,
"local"
);
// Connect to cloud database
cloud = makeNewConnection(
`mongodb://${process.env.DATABASE_USER}:${process.env.DATABASE_PASS}#64.227.44.132:27017/Eyemasters?retryWrites=true&w=majority`,
"cloud"
);
// Start Database sync helper
} else {
// Connect to cloud local database
local = makeNewConnection(
`mongodb://${process.env.DATABASE_USER}:${process.env.DATABASE_PASS}#localhost:27017/Eyemasters?retryWrites=true&w=majority`,
"local"
);
}
module.exports = {
local,
cloud
};
And here is one of my models having the issue.
const mongoose = require("mongoose");
mongoose.Promise = require('bluebird');
const { local, cloud } = require("../config/database.config");
const { genId } = require("../helpers/doc.id.generator");
const validator = require("validator");
const UserSchema = mongoose.Schema(
{
_id: mongoose.Schema.Types.ObjectId,
email: {
type: String,
required: true,
unique: true,
validate: {
validator: validator.isEmail,
message: "{VALUE} is not a valid email",
isAsync: false
}
},
hash: { type: String, bcrypt: true, rounds: 10 },
firstname: { type: String, required: true },
lastname: { type: String, required: true },
phone: { type: String },
dateOfBirth: { type: Date },
designation: { type: String },
role: { type: mongoose.Schema.Types.ObjectId, ref: "Role" },
passport: { type: String },
accountDetails: {
name: String,
number: Number,
bank: String
},
defaultBranch: {
type: mongoose.Schema.Types.ObjectId,
ref: "Branch"
},
branches: [{ type: mongoose.Schema.Types.ObjectId, ref: "Branch" }],
createdBy: {
type: mongoose.Schema.Types.ObjectId,
ref: "User"
},
lastModifiedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: "User"
},
webpush: { type: Object },
inactive: { type: Boolean, default: true },
approved: { type: Boolean, default: false },
activationCode: { type: String, unique: true },
activationExpiresIn: { type: Date }
},
{ toJSON: { virtuals: true }, timestamps: true }
);
UserSchema.plugin(require("mongoose-bcrypt"));
genId(UserSchema);
UserSchema.pre("save", function(next) {
if (!this.createdBy) this.createdBy = this._id;
if (!this.lastModifiedBy) this.lastModifiedBy = this._id;
});
exports.User = exports.User || local.model("User", UserSchema);
exports.OnlineUser = exports.OnlineUser || cloud.model("User", UserSchema);
And Lastly my controller setup;
exports.create = async (req, res) => {
// Validating entered data
if (
!req.body.firstname ||
!req.body.lastname ||
req.body.firstname.length < 3 ||
req.body.lastname.length < 3 ||
!req.body.email ||
!req.body.role ||
req.body.email.length < 3
) {
return res.status(400).send({
message: "Please fill in all required fields"
});
}
try {
const user = await User.findOne({
email: req.body.email.toLowerCase()
});
if (user) {
throw new Error("User with email " + req.body.email + " already exist");
}
console.log("Before create");
let newUser = new User({
...req.body,
activationCode: randtoken.uid(16),
activationExpiresIn: moment.utc().add(30, "minutes"),
email: req.body.email.toLowerCase()
});
console.log(newUser.save);
const userData = await newUser.save();
console.log("Saved");
let transaction = new DbTransaction({
transactionType: "insert",
modelName: "User",
data: userData,
clients: [process.env.DATABASE_CLIENT_ID],
isProcessed: false
});
await transaction
.save()
.then(d => console.log("Transaction updated successfully"))
await User.populate(userData, populateQuery, (err, data) => {
if (err) throw new Error(err);
return res
.status(201)
.send({ message: "User created successfully", user: data });
});
} catch (err) {
console.log(err);
console.log(err.kind);
return res.status(500).send({
message: err.message
});
}
};
I have tried different variants of javascript promise based work flow. Like Model.method().then().catch(), async try-await Model.method()-catch and lastly callback Model.method((err, data)=>{ //do something }).
None of the above conbination has worked. My observation is that mongoose just logs "done" into the console for this method but never action is never actually performed.
Your help is greatly appreciated, I have absolutely no idea why this is not working.
Thank you.
To all who made effort to assist, Thank you for the help.
I don't know why I am seeing the problem after posting here.
The issue was coming from not calling next in the middleware inside the model setup;
UserSchema.pre("save", function(next) {
if (!this.createdBy) this.createdBy = this._id;
if (!this.lastModifiedBy) this.lastModifiedBy = this._id;
});
Replace with;
UserSchema.pre("save", function(next) {
if (!this.createdBy) this.createdBy = this._id;
if (!this.lastModifiedBy) this.lastModifiedBy = this._id;
next();
});
Thank you all once again for your support.
I have been trying to initialize new users with a balance of 0 when they create a new banking account. However, I keep getting an empty array on Postman instead. On submit, "fill in all the fields balance" from the transaction controller is displayed.
I assume the error includes the User model affecting the Transaction model, but I am unsure how to fix this. Do I just need to move this block
balance: {
type: Number,
default: 0,
},
into the User model for the creation of a user? If so, should it be nested in the transactions reference somewhere? How should I keep track of the balance if I need to remove "balance" from the controller.
USER MODEL
const mongoose = require("mongoose");
const bcrypt = require("bcrypt");
const validator = require("validator");
const Schema = mongoose.Schema;
const Transaction = require("./transactionModel");
const userSchema = new Schema({
fname: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
transactions: {
// type: Array,
transactions: {
// type: Array,
type: mongoose.ObjectId,
ref: Transaction,
},
},
});
// static signup method
userSchema.statics.signup = async function (fname, email, password) {
// validation
if (!fname || !email || !password) {
throw Error("All fields must be filled");
}
if (!validator.isEmail(email)) {
throw Error("Email not valid");
}
if (!validator.isStrongPassword(password)) {
throw Error(
"Password not strong enough. Must be at least 8 characters and contain: uppercase, lowercase, number, and a special character."
);
}
const exists = await this.findOne({ email });
if (exists) {
throw Error("Email already in use");
}
const salt = await bcrypt.genSalt(10);
const hash = await bcrypt.hash(password, salt);
const user = await this.create({ fname, email, password: hash });
return user;
};
// static login method
userSchema.statics.login = async function (email, password) {
if (!email || !password) {
throw Error("All fields must be filled");
}
const user = await this.findOne({ email });
if (!user) {
throw Error("Incorrect email");
}
const match = await bcrypt.compare(password, user.password);
if (!match) {
throw Error("Incorrect password");
}
return user;
};
module.exports = mongoose.model("User", userSchema);
TRANSACTION MODEL
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const transactionSchema = new Schema(
{
transactionType: {
type: String,
required: true,
},
balance: {
type: Number,
default: 0,
},
amount: {
type: Number,
required: true,
},
user_id: {
type: String,
required: true,
},
},
{ timestamps: true }
);
module.exports = mongoose.model("Transaction", transactionSchema);
new transaction controller
// create new transaction
const createTransaction = async (req, res) => {
const { transactionType, amount, balance } = req.body;
let emptyFields = [];
if (!transactionType) {
emptyFields.push("transactionType");
}
if (!amount) {
emptyFields.push("amount");
}
if (!balance) {
emptyFields.push("balance");
}
if (emptyFields.length > 0) {
return res.status(400).json({
error: "Please fill in all the fields " + emptyFields + " ",
emptyFields,
});
}
I tried removing balance from the transaction controller/ nesting the balance in the User model/ having the default balance be 0 in the transactions model. I can't figure out what or what combination of things needs to change.
how is everything ? I really need your help!
I'm building an API to register authenticated users, with storage in the mongo atlas database (cloud). I'm currently experiencing the following error: TypeError subscription error: User.hashPassword is not a function. I've done several researches, in several questions here on stackoverflow and on other sites, after testing all the solutions the error persists.
my user.model.js file looks like this:
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const bcrypt = require('bcrypt');
const userSchema = new Schema(
{
userName: { type: String, unique: true, required: true },
email: { type: String, required: true, unique: true },
emailToken: { type: String, default: null },
emailTokenExpires: { type: Date, default: null },
active: { type: Boolean, default: false},
password: { type: String, required: true},
resetPasswordToken: { type: String, default: null },
resetPasswordExpires: { type: Date, default: null },
emailToken: {type: String, default: null},
emailTokenExpires: {type: Date, default: null},
},
{
timestamps: {
createdAt: "createdAt",
updatedAt: "updatedAt",
},
}
);
const User = mongoose.model("user", userSchema);
module.exports.hashPassword = async (password) => {
try {
const salt = await bcrypt.genSalt(10); // 10 rounds
return await bcrypt.hash(password, salt);
} catch (error) {
throw new Error("Hashing failed", error);
}
};
module.exports = User;
and my user.controller.js file looks like this:
const Joi = require("joi");
require("dotenv").config();
const { v4: uuid } = require("uuid");
const { sendEmail } = require("./helpers/mailer");
const User = require("./user.model");
//Validate user schema
const userSchema = Joi.object().keys({
email: Joi.string().email({ minDomainSegments: 2 }),
password: Joi.string().required().min(4),
confirmPassword: Joi.string().valid(Joi.ref("password")).required(),
});
exports.Signup = async (req, res) => {
try {
const result = userSchema.validate(req.body);
if (result.error) {
console.log(result.error.message);
return res.json({
error: true,
status: 400,
message: result.error.message,
});
}
//Check if the email has been already registered.
var user = await User.findOne({
email: result.value.email,
});
if (user) {
return res.json({
error: true,
message: "Email is already in use",
});
}
const hashPassword = await User.hashPassword(result.value.password);
const id = uuid(); //Generate unique id for the user.
result.value.userId = id;
//remove the confirmPassword field from the result as we dont need to save this in the db.
delete result.value.confirmPassword;
result.value.password = hashPassword;
let code = Math.floor(100000 + Math.random() * 900000); //Generate random 6 digit code.
let expiry = Date.now() + 60 * 1000 * 15; //Set expiry 15 mins ahead from now
const sendCode = await sendEmail(result.value.email, code);
if (sendCode.error) {
return res.status(500).json({
error: true,
message: "Couldn't send verification email.",
});
}
result.value.emailToken = code;
result.value.emailTokenExpires = new Date(expiry);
const newUser = new User(result.value);
await newUser.save();
return res.status(200).json({
success: true,
message: "Registration Success",
});
} catch (error) {
console.error("signup-error", error);
return res.status(500).json({
error: true,
message: "Cannot Register",
});
}
};
Error displayed in terminal:
Danilo#DANILO-PC D:\Meus Documentos\Área de Trabalho\api-auth-pokestore
$ node app.js
Server started listening on PORT : 5000
Database connection Sucess.
signup-error TypeError: User.hashPassword is not a function
at exports.Signup (D:\Meus Documentos\Área de Trabalho\api-auth-pokestore\src\users\user.controller.js:39:37)
at processTicksAndRejections (internal/process/task_queues.js:94:5)
image terminal
Look at this part of your code:
module.exports.hashPassword = async (password) => { ... };
module.exports = User;
You're setting hashPassword in your exports, then completely replacing your exports with User. You probably wanted to do something like this instead:
User.hashPassword = async (password) => { ... };
module.exports = User;
or move your module.exports.hashPassword = ... so it's after the module.exports = ....
I'm tryng to register a user in my database.
const sequelize = require('sequelize')
const { Model, DataTypes } = sequelize
const bcrypt = require('bcrypt')
class User extends Model {
isPasswordValid(encondedPassword, password) {
return bcrypt.compareSync(password, encondedPassword)
}
static init(sequelize) {
super.init({
email: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notEmpty: true,
},
},
password: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notEmpty: true,
},
}
}, {
hooks: {
beforeCreate: (user, options) => {
const salt = bcrypt.genSaltSync()
user.setAttributes('password', bcrypt.hashSync(user.password, salt))
}
},
sequelize
})
}
}
module.exports = User
But when I call User.create({email, password}) it gives me and error:
UnhandledPromiseRejectionWarning: SequelizeDatabaseError: null value in column "password" violates not-null constraint .
If a delete my hook the code works fine but the password will not be encrypted.
setAttributes receive 3 arguments or an object
Try this
user.setAttributes('setAttributes', 'password', bcrypt.hashSync(user.password, salt))
//OR
user.setAttributes({password: bcrypt.hashSync(user.password, salt)})
Simpler use setDataValue
user.setDataValue('password', bcrypt.hashSync(user.password, salt))