Mongoose validation not working properly in mocha test - node.js

I am building a REST api with nodejs, using mongoose and mochajs to run some tests. I have the following scheme:
var subscriptionTypeSchema = new mongoose.Schema({
typeId : { type: Number, required: true, unique: true },
name : { type: String, required: true},
active : { type: Boolean, required: true }
});
Express route:
app.post('/1.0/subscriptiontype', subscriptiontype.create);
Controller:
exports.create = function(req, res) {
validation.subscriptionTypeValidator(req);
var errors = req.validationErrors();
if (errors) {
res.status(400).json({
errors: errors
});
} else {
var subscriptionType = new SubscriptionType();
subscriptionType.typeId = parseInt(req.body.typeId);
subscriptionType.name = req.body.name;
subscriptionType.active = req.body.active;
subscriptionType.save(function(err) {
if (err) {
var parsedError = mongooseutility.parseMongooseError(err);
res.status(400).json({
errors: [parsedError]
});
} else {
res.json({identifier: subscriptionType._id});
}
});
}
};
The mongoose utility maps the error codes to a more API friendly output (error codes 11001 and 11000 are mapped to a 'duplicate' error, as can be seen in the test).
Mocha before method:
before(function(done) {
db.connection.on('open', function() {
db.connection.db.dropDatabase(function(err) {
done();
});
});
});
I've verified that the database is dropped successfully.
The test itself makes a request using supertest. Before this test, I have a test that creates a subscription type with typeId 4 successfully, so this one should fail:
it('Should not create subscription with taken type id', function (done) {
request(app.privateapi)
.post('/1.0/subscriptiontype')
.set('Authorization', authorizationHeader)
.send({
typeId: 4,
name: 'New package',
active: 1
})
.expect(function (res) {
if (res.status !== 400) {
throw new Error('Status code was not 400');
}
var expectedResponse = { errors: [ { param: 'typeId', msg: 'duplicate' } ] };
if (JSON.stringify(res.body) !== JSON.stringify(expectedResponse)) {
throw new Error('Output was not was as expected');
}
})
.end(done);
});
Tests are invoked using grunt-simple-mocha.
This test works the first time, however when I run it a 2nd time it fails on the unique validation. A third time it works again. I've done some searching and found that it probably has something to do with a race condition while recreating indexes, so I've tried restarting mongodb before running the tests again, but that doesn't work. I've found a solution here: http://grokbase.com/t/gg/mongoose-orm/138qe75dvr/mongoose-unique-index-test-fail but I am not sure how to implement this. Any ideas?
Edit: for now I fixed it by dropping the database in an 'after' method (instead of 'before'). All the tests run fine, but it would be nice to keep the test data after the tests are done, for inspection etc...

You are not testing the creation of your tables so you can just empty your collections instead of creating the db.
Something along those lines (not tested):
beforeEach(function(done){
var models = Object.keys(mongoose.models);
var expects = models.length;
if(expects == 0) return done();
var removeCount = 1;
//maybe use async or something else but whatever
models.forEach(function(model){
model.remove({}, function(){
if(removeCount == expects){
done();
}
removeCount++;
})
});
});

Related

Why it only returns the first id from my mock up database?

In short I am trying to create a simple api that would return the user with the matching id. I use postman to send requests to my localhost created using node.js with express. It works fine when I request the first user but throws in an error when requesting "John". I am coding along a udemy course and can't figure out what the issue is other than the material is outdated. The error is "Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client"
users: [
{
id: "123",
name: "Sally",
email: "sally#gmail.com",
password: "bananas",
entries: 0,
joined: new Date(),
},
{
id: "124",
name: "John",
email: "john#gmail.com",
password: "apples",
entries: 0,
joined: new Date(),
},
],
};
app.get("/profile/:id", (req, res) => {
const { id } = req.params;
let found = false;
database.users.forEach((user) => {
if (user.id === id) {
found = true;
return res.json(user);
}
if (!found) {
res.json("User not found");
}
});
});
From the MDN Web Docs:
There is no way to stop or break a forEach() loop other than by throwing an exception. If you need such behavior, the forEach() method is the wrong tool.
Early termination may be accomplished with:
A simple loop
A for...of
loop
[Array.prototype.every()][every]
[Array.prototype.some()][some]
[Array.prototype.find()][find]
[Array.prototype.findIndex()][findIndex]
This means that your loop will run through all elements and in fact call res.json multiple times resulting in the ERR_HTTP_HEADERS_SENT error . There are many ways to fix this, here's an one example:
app.get("/profile/:id", (req, res) => {
const {id} = req.params;
for (const user of database.users) {
if (user.id === id) {
return res.json(user);
}
}
res.json("User not found");
});

one query inside another MongoDB hangs

I am trying to get only get Notes [from the notes collection] that come from meetings [from the meetings collection] that don't contain the word 'test' in them:
function getNotes(done) {
noteSchema.find({}).exec((err, notes) => {
var numNotes = 0;
async.each(notes, (n, next) => {
userSchema.findById(n.userId, (err, user) => {
if (err || !user) { next(); return; }
var emailsStr = utils.getEmailsString(user.emails);
if (!utils.toSkipEmail(emailsStr)) {
meetingSchema.findById(n.meetingId, (err, meeting) => {
if (err || !meeting) { next(); return; }
if (meeting.name.displayValue.indexOf('test', 'Test') == -1) {
numNotes++;
}
next();
});
}
})
}, (err, result) => {
console.log(util.format('Total Number of Notes: %d', numNotes));
done(null);
});
});
}
The code works fine without adding in the lines to find the meetings by ID. It hangs at that point.
For reference, here is the start of a function that comes later to filter out any 'test' or 'Test' containing meetings.
function getMeetings(done) {
meetingSchema.find({
'name.displayValue': { '$regex' : '(?!.*test)^.*$' , '$options' : 'i' }
}).exec((err, meetings) => {
Relevant lines of Notes Schema:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var noteSchema = mongoose.Schema({
meetingId: {type: String, default: ''},
});
exports.Note = mongoose.model('Note', noteSchema);
The meeting schema has no notes field.
So, if I were going to go after a solution like this, where I wanted to get all notes that weren't part of a meeting w/ the word 'test' in the name, I'd probably go the other way with it, unless there's a whole ton of notes you could just get all notes and populate them with their meetings, and then do the filtering. assuming your NoteSchema defines something like:
meeting : { type: ObjectId, ref: 'Meeting'}
then in your query you could do (given the notATest function that returns true or false appropriately:
Note.find({}).populate('meeting').exec((e, n) => {
_.omit(n, (note) => { return notATest(note.meeting.name); });
});
Alternatively, you could search for all meetings that are not a test first, and call .populate('notes') on them, if the 'ref' goes the other way.

Don't run validation on schema under certain conditions

I have a schema with several required fields. When I save a document with a published:false prop, I want to not run any validation and just save the document as is. Later, when published:true, I want to run all the validation.
I thought this would work:
MySchema.pre('validate', function(next) {
if(this._doc.published === false) {
//don't run validation
next();
}
else {
this.validate(next);
}
});
But this isn't working, it returns validation errors on required properties.
So how do I not run validation in some scenarios and run it in others? What's the most elegant way to do this?
Please try this one,
TagSchema.pre('validate', function(next) {
if (!this.published)
next();
else {
var error = new mongoose.Error.ValidationError(this);
next(error);
}
});
Test schema
var TagSchema = new mongoose.Schema({
name: {type: String, require: true},
published: Boolean,
tags: [String]
});
With published is true
var t = new Tag({
published: true,
tags: ['t1']
});
t.save(function(err) {
if (err)
console.log(err);
else
console.log('save tag successfully...');
});
Result:
{ [ValidationError: Tag validation failed]
message: 'Tag validation failed',
name: 'ValidationError',
errors: {} }
With published is false, result is
save tag successfully...

Mongoose.js: isModified flag for attribute with default value

I have a model with a default generated value that doesn't change throughout the document lifetime except in one special case.
A document may get marked as deleted using doc.update({_id: doc._id, deleted_at: new Date()}, {overwrite: true})
In a very special case the document may be "revived" - looked up by it's id and being worked with again afterwards.
In a pre-save hook I need to perform some action (for example store a document in another collection) whenever the document is created or revived.
Consider following simplified code:
'use strict';
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');
var someSchema = mongoose.Schema({
immutable: {
type: String,
default: function () {
return 'SomeVeryRandomValue';
}
}
});
someSchema.pre('save', function (next) {
if (this.isNew || this.isModified('immutable')) {
console.log('Processing pre-save hook!');
}
next();
});
var SomeModel = mongoose.model('SomeModel', someSchema, 'test');
mongoose.connection.once('open', function (err) {
var testDoc = new SomeModel({});
console.log('New: %j', testDoc.toObject());
testDoc.save(function(err) {
console.log('Initial saved: %j', testDoc.toObject());
testDoc.update({_id: testDoc._id}, {overwrite: true}, function (err) {
// at this point using mongo console:
// > db.test.findOne()
// { "_id" : ObjectId("5617b028bf84f0a93687cf67") }
SomeModel.findById(testDoc.id, function(err, reloadedDoc) {
console.log('Reloaded: %j', reloadedDoc.toObject());
console.log('reloaded isModified(\'immutable\'): %j', reloadedDoc.isModified('immutable'));
reloadedDoc.save(function(err) {
console.log('Re-saved: %j', reloadedDoc);
mongoose.connection.close();
});
});
});
});
});
And the script runtime output:
$ node mongoose-modified-test.js
New: {"_id":"5617b64c5376737b46f6bb98","immutable":"SomeVeryRandomValue"}
Processing pre-save hook!
Initial saved: {"__v":0,"_id":"5617b64c5376737b46f6bb98","immutable":"SomeVeryRandomValue"}
Reloaded: {"_id":"5617b64c5376737b46f6bb98","immutable":"SomeVeryRandomValue"}
reloaded isModified('immutable'): false
Re-saved: {"_id":"5617b64c5376737b46f6bb98","immutable":"SomeVeryRandomValue"}
The immutable is not marked as modified and IMHO it should - original document had no value for that attribute.
A work-around solution is to remove the default value for immutable attribute and define pre-validate hook like this one:
someSchema.pre('validate', function (next) {
if (this.isNew || !this.immutable) {
this.immutable = 'SomeVeryRandomValue';
}
next();
});
This is not exactly what I need because the value won't be generated until I try to validate/save the document. The pre/post-init hooks are not executed on new SomeModel({}) so I can't use those.
Should I open an issue for mongoose.js?
this.$isDefault('immutable') can be used instead.
someSchema.pre('save', function (next) {
if (this.isNew || this.$isDefault('immutable')) {
console.log('Processing pre-save hook!');
}
next();
});
Output of the script with updated pre-save hook:
$ node --harmony mongoose-modified-test.js
New: {"_id":"56276f0c1a2f17ae7e0a03f7","immutable":"SomeVeryRandomValue"}
Processing pre-save hook!
Initial saved: {"__v":0,"_id":"56276f0c1a2f17ae7e0a03f7","immutable":"SomeVeryRandomValue"}
Reloaded: {"_id":"56276f0c1a2f17ae7e0a03f7","immutable":"SomeVeryRandomValue"}
Processing pre-save hook!
Re-saved: {"_id":"56276f0c1a2f17ae7e0a03f7","immutable":"SomeVeryRandomValue"}
Thanks to #vkarpov15 for clarification.

Mongoose helper method has no findOne method?

How can I add helper methods to find and save objects in Mongoose. A friend told me to use helper methods but I cannot get them to work after a day. I always receive errors saying that either findOne() or save() does not exist OR that next callback is undefined (when node compiles ... before I execute it):
I've tried _schema.methods, _schema.statics... nothing works...
var email = require('email-addresses'),
mongoose = require('mongoose'),
strings = require('../../utilities/common/strings'),
uuid = require('node-uuid'),
validator = require('validator');
var _schema = new mongoose.Schema({
_id: {
type: String,
trim: true,
lowercase: true,
default: uuid.v4
},
n: { // Name
type: String,
required: true,
trim: true,
lowercase: true,
unique: true,
index: true
}
});
//_schema.index({
// d: 1,
// n: 1
//}, { unique: true });
_schema.pre('save', function (next) {
if (!this.n || strings.isNullOrWhitespace(this.n)){
self.invalidate('n', 'Domain name required but not supplied');
return next(new Error('Domain name required but not supplied'));
}
var a = email.parseOneAddress('test#' + this.n);
if (!a || !a.local || !a.domain){
self.invalidate('n', 'Name is not valid domain name');
return next(new Error('Name is not valid domain name'));
}
next();
});
_schema.statics.validateForSave = function (next) {
if (!this.n || strings.isNullOrWhitespace(this.n)){
return next(new Error('Domain name required but not supplied'));
}
var a = email.parseOneAddress('test#' + this.n);
if (!a || !a.local || !a.domain){
return next(new Error('Name is not valid domain name'));
}
next();
}
_schema.statics.findUnique = function (next) {
this.validateForSave(function(err){
if (err){ return next(err); }
mongoose.model('Domain').findOne({ n: this.n }, next);
//this.findOne({ n: this.n }, next);
});
}
_schema.statics.init = function (next) {
this.findUnique(function(err){
if (err){ return next(err); }
this.save(next);
});
}
var _model = mongoose.model('Domain', _schema);
module.exports = _model;
I believe you are running into issues because of your usage with this. Every time you enter a new function this's context is changing. You can read more about this at mdn.
Additionally your callbacks aren't allowing anything to be passed into the mongoose method. For example if I was to create the most basic "save" method I I would do the following:
_schema.statics.basicCreate = function(newDocData, next) {
new _model(newDocData).save(next);
}
Now if I wanted to search the Domain collection for a unique document I would use the following:
_schema.statics.basicSearch = function(uniqueName, next) {
var query = {n: uniqueName};
_model.findOne(query, function(err, myUniqueDoc) {
if (err) return next(err);
if (!myUniqueDoc) return next(new Error("No Domain with " + uniqueName + " found"));
next(null, myNewDoc);
});
}
Mongoose has built in validations for what you are doing:
_schema.path("n").validate(function(name) {
return name.length;
}, "Domain name is required");
_schema.path("n").validate(function(name) {
var a = email.parseOneAddress("test#" + name);
if (!a || !a.local || !a.domain) {
return false;
}
return true;
}, "Name is not a valid domain name");
It returns a boolean. If false, it passes an error to the .save() callback with the stated message. For validating uniqueness:
_schema.path("n").validate(function(name, next) {
var self = this;
this.model("Domain").findOne({n: name}, function(err, domain) {
if (err) return next(err);
if (domain) {
if (self._id === domain._id) {
return next(true);
}
return next(false);
}
return next(true);
});
}, "This domain is already taken");
You're using self = this here so that you can access the document inside the findOne() callback. false is being passed to the callback if the name exists and the result that is found isn't the document itself.
I've tried _schema.methods, _schema.statics
To clarify, .statics operate on the Model, .methods operate on the document. Zane gave a good example of statics, so here is an example of methods:
_schema.methods.isDotCom = function() {
return (/.com/).test(this.n);
}
var org = new Domain({n: "stuff.org"});
var com = new Domain({n: "things.com"});
org.isDotCom(); // false
com.isDotCom(); // true
Opinion: It's neat to have mongoose do validations, but it's very easy to forget it's happening. You also may want to have some validation in one area of your app while using different validations elsewhere. I'd avoid using most of it unless you definitively know you will have to do the same thing every time and will never NOT have to do it.
Methods/statics are a different story. It's very convenient to call isDotCom() instead of writing out a regex test every time you need to check. It performs a single and simple task that saves you some typing and makes your code more readable. Using methods for boolean checks can add a ton of readability. Defining statics like findByName (Zane's basicSearch) is great when you know you're going to do a simple query like that repeatedly.
Treat Mongoose as a utility, not as core functionality.

Resources