Testing mongoose pre-save hook - node.js

I am quite new to testing nodejs. So my approach might be completely wrong. I try to test a mongoose models pre-save-hook without hitting the Database. Here is my model:
// models/user.js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
UserSchema = new Schema({
email: {type: String, required: true},
password: {type: String, required: true}
});
UserSchema.pre('save', function (next) {
const user = this;
user.email = user.email.toLowerCase();
// for testing purposes
console.log("Pre save hook called");
next();
});
module.exports = mongoose.model("User", UserSchema);
As I said, I do not want to hit the Database with my test, so I tried using a sinon stub of the Users save() method:
// test/models/user.js
const sinon = require("sinon");
const chai = require("chai");
const assert = chai.assert;
const User = require("../../models/user");
describe("User", function(){
it("should convert email to lower case before saving", (done) => {
const user = new User({email: "Valid#Email.com", password: "password123"});
const saveStub = sinon.stub(user, 'save').callsFake(function(cb){ cb(null,this) })
user.save((err,res) => {
if (err) return done(err);
assert.equal(res.email,"valid#email.com");
done();
})
})
});
However, If I do it like that the pre-save hook will not be called. Am I on the wrong path or am I missing something? Or is there maybe another way of triggering the pre-save hook and testing its outcome? Thanks very much in advance!

Before we start: I'm looking for the same thing as you do and I've yet to find a way to test the different hooks in Mongoose without a database. It's important that we distinguish between testing our code and testing mongoose.
Validation is middleware. Mongoose registers validation as a pre('save') hook on every schema by default. http://mongoosejs.com/docs/validation.html
Considering that validate will always be added to the model and I wish to test the automated fields in my model, I've switched from save to validate.
UserSchema = new Schema({
email: {type: String, required: true},
password: {type: String, required: true}
});
UserSchema.pre('validate', function(next) {
const user = this;
user.email = user.email.toLowerCase();
// for testing purposes
console.log("Pre validate hook called");
next();
});
The test will now look like:
it("should convert email to lower case before saving", (done) => {
const user = new User({email: "VALID#EMAIL.COM", password: "password123"});
assert.equal(res.email,"valid#email.com");
}
So What About the Pre Save Hook?
Because I've moved the business logic for automatic fields from 'save' to 'validate', I'll use 'save' for database specific operations. Logging, adding objects to other documents, and so on. And testing this only makes sense with integration with a database.

I just faced the same issue and managed to solve it by extracting the logic out of the hook, making it possible to test it in isolation. With isolation I mean without testing anything Mongoose related.
You can do so by creating a function, that enforces your logic, with the following structure:
function preSaveFunc(next, obj) {
// your logic
next();
}
You can then call it in your hook:
mySchema.pre('save', function (next) { preSaveFunc(next, this); });
This will make the reference to this available inside the function, so you can work with it.
The extracted part can then be unit tested by overwriting the next function to a function without a body.
Hope this will help anyone as it actually was a pain to solve this with my limited knowledge on Mongoose.

Related

mocha --watch and mongoose models

If I leave mocha watching for changes, every time I save a file mongoose throws the following error:
OverwriteModelError: Cannot overwrite Client model once compiled
I know that mongoose won't allow to define a model twice, but I don't know how to make it work with mocha --watch.
// client.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var clientSchema = new Schema({
secret: { type: String, required: true, unique: true },
name: String,
description: String,
grant_types: [String],
created_at: { type: Date, default: Date.now }
});
module.exports = mongoose.model('Client', clientSchema);
And here is the test
// client-test.js
var chai = require('chai');
var chaiHttp = require('chai-http');
var mongoose = require('mongoose');
var server = require('../../app');
var Client = require('../../auth/models').Client;
var should = chai.should();
chai.use(chaiHttp);
describe('client endpoints', function() {
after(function(done) {
mongoose.connection.close();
done();
});
it('should get a single client on /auth/client/{clientId} GET', function(done) {
var clt = new Client({
name: 'my app name',
description: 'super usefull and nice app',
grant_types: ['password', 'refresh_token']
});
clt.save(function(err) {
chai.request(server)
.get('/auth/client/' + clt._id.toString())
.end(function(err, res) {
res.should.have.status(200);
res.should.be.json;
res.body.should.have.property('client_id');
res.body.should.not.have.property('secret');
res.body.should.have.property('name');
res.body.should.have.property('description');
done();
});
});
});
});
I had the same issue. My solution was to check whether the model was created/compiled yet, and if not then do so, otherwise just retrieve the model.
using mongoose.modelNames() you can get an array of the names of your models. Then use .indexOf to check if the model you want to get is in the array or not. If it is not, then compile the model, for example: mongoose.model("User", UserSchema), but if it is already defined (as is the case with mocha --watch), simply retrieve the model (don't compile it again), which you can do with for example: mongoose.connection.model("User").
This is a function which returns a function to do this checking logic, which itself returns the model (either by compiling it or just retrieving it).
const mongoose = require("mongoose");
//returns a function which returns either a compiled model, or a precompiled model
//s is a String for the model name e.g. "User", and model is the mongoose Schema
function getModel(s, model) {
return function() {
return mongoose.modelNames().indexOf(s) === -1
? mongoose.model(s, model)
: mongoose.connection.model(s);
};
}
module.exports = getModel;
This means you have to require your model a bit differently, since you are likely replacing something like this:
module.exports = mongoose.model("User", UserSchema);
which returns the model itself,
with this:
module.exports = getModel("User", UserSchema);
which returns a function to return the model, either by compiling it or just retrieving it. This means when you require the 'User' model, you would want to call the function returned by getModel:
const UserModel = require("./models/UserModel")();
I hope this helps.
Here is a simpler code for the function getModel() that George is proposing
function getModel(modelName, modelSchema) {
return mongoose.models[modelName] // Check if the model exists
? mongoose.model(modelName) // If true, only retrieve it
: mongoose.model(modelName, modelSchema) // If false, define it
}
For a larger explanation on how to define and require the model, look here
Hope this helps :)
This worked for me,
place on the top of your test file:
const mongoose = require("mongoose");
mongoose.models = {};
mongoose.modelSchemas = {};

How to cascade delete using Mongoose remove middleware?

I'm trying to delete all dependencies of a schema when a DELETE request is sent to my API. Deleting goes ok, but the remove middleware, which is supposed to clean the dependencies, seems like is not even getting called.
This is my Customer schema:
var mongoose = require("mongoose"),
Schema = mongoose.Schema,
passportLocalMongoose = require('passport-local-mongoose');
var Order = require('./order');
var Customer = new Schema({
name: String,
telephone: Number,
address: String,
email: String,
seller: String
});
Customer.post('remove', function(next) {
Order.remove({ customer: this._id }).exec();
next();
});
Customer.plugin(passportLocalMongoose);
module.exports = mongoose.model("Customer", Customer);
And this is my customer route:
var express = require('express');
var router = express.Router();
var passport = require('passport');
var isAuthenticated = require('./isAuthenticated');
var Customer = require('../models/customer');
var Order = require('../models/order');
// (...)
router.delete('/:customer_id', function(req, res) {
Customer.remove({ _id: req.params.customer_id }, function(err) {
if (err)
res.json({ SERVER_RESPONSE: 0, SERVER_MESSAGE: "Error deleting", ERR: err });
else res.json({ SERVER_RESPONSE: 1, SERVER_MESSAGE: "Customer deleted" });
});
});
// (...)
I did look this question and Mongoose Docs (Mongoose Middleware) but it's still unclear to me. I don't know what I'm missing or doing wrong.
Thanks in advance!
EDIT
This is my project's repository. Please, feel free to look into.
I finally found the solution to this. Middleware wasn't firing because you must use remove(), save(), etc on model instances, not the model itself.
Example:
Customer.remove({...}); won't work.
Customer.findOne({...}, function(err, customer) {
customer.remove();
});
will work and will do whatever is in Customer.post('remove').
Seems like this is the part you're focusing on:
Customer.post('remove', function(next) {
Order.remove({ customer: this._id }).exec();
next();
});
What you're doing wrong here is that the post hook is not given any flow control, so the next parameter is not actually a function but the document itself.
Change it up to this and you should get what you want:
Customer.post('remove', function(doc) {
Order.remove({ customer: doc._id }).exec();
});
From the docs:
post middleware are executed after the hooked method and all of its
pre middleware have completed. post middleware do not directly receive
flow control, e.g. no next or done callbacks are passed to it. post
hooks are a way to register traditional event listeners for these
methods.

Save mongoose document again after deleting it

I try to unit test my restify node.js-app using mocha and without mocking out the mongodb database. As some tests will alter the database, I'd like to reset its contents before each test.
In my tests I also need to access the mongoose documents I am creating. Thus I have to define them outside of the beforeEach hook (see the user document below).
However, it seems like it's not possible to save a document a second time after emptying the database.
Below is a minimal example I've come up with. The second test will fail in that case, because user won't get saved a second time. If I delete the first test, beforeEach only gets called once and everything works nicely.
Also if I define user inside the beforeEach hook, it works as well.
So my actual question: Is it possible to work around this issue and save a document a second time after deleting it? Or do you have any other idea on how I can reset the database inside the beforeEach hook? What's the proper way to have the same database setup before each test case?
var mongoose = require('mongoose')
var Schema = mongoose.Schema
var should = require('should')
var flow = require('async')
var UserSchema = new Schema({
username: {type: String, required: true, unique: true},
password: {type: String, required: true},
name: {type: String, default: ''}
})
mongoose.model('User', UserSchema)
var User = mongoose.model('User')
describe('test mocha', function() {
var user = new User({
username: 'max',
password: 'asdf'
})
before(function(done) {
var options = {server: {socketOptions: {keepAlive: 1}}}
mongoose.connect('mongodb://localhost/unittest', options, done)
})
beforeEach(function(done) {
flow.series([
function(callback) {
User.collection.remove(callback)
}, function(callback) {
user.save(callback)
}
], function(err, res) {
done()
})
})
it('should pass', function(done) {
true.should.equal(true)
// also access some elements of user here
done()
})
it('should have a user', function(done) {
User.find().exec(function(err, res) {
res.should.not.be.empty
})
done()
})
after(function(done) {
mongoose.disconnect()
done()
})
})
I faced same problem,I generated a copy of the document to save. When need to save the document after deleting it I saved the copy, and it worked. Like
var user = new User({
username: 'max',
password: 'asdf'
});
var userCopy = new User({
username: 'max',
password: 'asdf'
});
And in test cases.
user.remove(callback)
}, function(callback) {
userCopy.save(callback){
// should.not.exist(err)
}
}
It might not be good solution ,but it worked for me.

Mongoose save callback doesn't fire

I'm new to mongoose and I'm having a hard time finding the issue within my code. I'm building a REST server using Sails.js and Mongoose. I have a node module (e.g. "sails-mongoose") for exporting mongoose, where I also connect to my database:
var mongoose = require('mongoose');
mongoose.connect('mongodb://#localhost:27017/fooria');
module.exports = mongoose;
And in my model.js:
var adapter = require('sails-mongoose');
var schema = new adapter.Schema({
firstname: {
type: String,
required: true,
trim: true
}
});
module.exports = {
schema: schema,
model: adapter.model('Collection', schema)
}
In my controller's create method I have:
create: function(req, res, next) {
var userData = {firstname: 'Test'};
var users = new Users.model(userData);
users.save(function(err, data){
if (err) return res.json(err, 400);
res.json(data, 201);
});
}
When running create method, the entry is saved to the Mongodb collection but the callback is never reached. Can someone please help me on this track, as I found similar questions but none helped me though. Thanks!
I suppose your are using Express. According Express docs you are calling res.json using incorrect parameters (wrong order).
Correct format:
res.json(code, data)
Example:
res.json(500, { error: 'message' })

Mongoose ODM, change variables before saving

I want to create a model layer with Mongoose for my user documents, which does:
validation (unique, length)
canonicalisation (username and email are converted to lowercase to check uniqueness)
salt generation
password hashing
(logging)
All of these actions are required to be executed before persisting to the db. Fortunately mongoose supports validation, plugins and middleware.
The bad thing is that I cannot find any good material on the subject.
The official docs on mongoosejs.com are too short...
Does anyone have an example about pre actions with Mongoose (or a complete plugin which does all, if it exists)?
Regards
In your Schema.pre('save', callback) function, this is the document being saved, and modifications made to it before calling next() alter what's saved.
Another option is to use Getters. Here's an example from the website:
function toLower (v) {
return v.toLowerCase();
}
var UserSchema = new Schema({
email: { type: String, set: toLower }
});
https://mongoosejs.com/docs/tutorials/getters-setters.html
var db = require('mongoose');
var schema = new db.Schema({
foo: { type: String }
});
schema.pre('save', function(next) {
this.foo = 'bar';
next();
});
db.model('Thing', schema);

Resources