UNPKG

mongoose

Version:

Mongoose MongoDB ORM

1,750 lines (1,411 loc) 120 kB
/** * Test dependencies. */ var start = require('./common') , should = require('should') , mongoose = start.mongoose , random = require('../lib/utils').random , Query = require('../lib/query') , Schema = mongoose.Schema , SchemaType = mongoose.SchemaType , CastError = SchemaType.CastError , ValidatorError = SchemaType.ValidatorError , ValidationError = mongoose.Document.ValidationError , ObjectId = Schema.ObjectId , DocumentObjectId = mongoose.Types.ObjectId , DocumentArray = mongoose.Types.DocumentArray , EmbeddedDocument = mongoose.Types.Document , MongooseNumber = mongoose.Types.Number , MongooseArray = mongoose.Types.Array , MongooseError = mongoose.Error; /** * Setup. */ var Comments = new Schema(); Comments.add({ title : String , date : Date , body : String , comments : [Comments] }); var BlogPost = new Schema({ title : String , author : String , slug : String , date : Date , meta : { date : Date , visitors : Number } , published : Boolean , mixed : {} , numbers : [Number] , owners : [ObjectId] , comments : [Comments] }); BlogPost.virtual('titleWithAuthor') .get(function () { return this.get('title') + ' by ' + this.get('author'); }) .set(function (val) { var split = val.split(' by '); this.set('title', split[0]); this.set('author', split[1]); }); BlogPost.method('cool', function(){ return this; }); BlogPost.static('woot', function(){ return this; }); mongoose.model('BlogPost', BlogPost); var collection = 'blogposts_' + random(); module.exports = { 'test a model isNew flag when instantiating': function(){ var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost(); post.isNew.should.be.true; db.close(); }, 'modified getter should not throw': function () { var db = start(); var BlogPost = db.model('BlogPost', collection); var post = new BlogPost; db.close(); var threw = false; try { post.modified; } catch (err) { threw = true; } threw.should.be.false; }, 'test presence of model schema': function(){ var db = start() , BlogPost = db.model('BlogPost', collection); BlogPost.schema.should.be.an.instanceof(Schema); BlogPost.prototype.schema.should.be.an.instanceof(Schema); db.close(); }, 'test a model default structure when instantiated': function(){ var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost(); post.isNew.should.be.true; post.db.model('BlogPost').modelName.should.equal('BlogPost'); post.constructor.modelName.should.equal('BlogPost'); post.get('_id').should.be.an.instanceof(DocumentObjectId); should.equal(undefined, post.get('title')); should.equal(undefined, post.get('slug')); should.equal(undefined, post.get('date')); post.get('meta').should.be.a('object'); post.get('meta').should.eql({}); should.equal(undefined, post.get('meta.date')); should.equal(undefined, post.get('meta.visitors')); should.equal(undefined, post.get('published')); post.get('numbers').should.be.an.instanceof(MongooseArray); post.get('owners').should.be.an.instanceof(MongooseArray); post.get('comments').should.be.an.instanceof(DocumentArray); db.close(); }, 'mongoose.model returns the model at creation': function () { var Named = mongoose.model('Named', new Schema({ name: String })); var n1 = new Named(); should.equal(n1.name, null); var n2 = new Named({ name: 'Peter Bjorn' }); should.equal(n2.name, 'Peter Bjorn'); var schema = new Schema({ number: Number }); var Numbered = mongoose.model('Numbered', schema, collection); var n3 = new Numbered({ number: 1234 }); n3.number.valueOf().should.equal(1234); }, 'test default Array type casts to Mixed': function () { var db = start() , DefaultArraySchema = new Schema({ num1: Array , num2: [] }) mongoose.model('DefaultArraySchema', DefaultArraySchema); var DefaultArray = db.model('DefaultArraySchema', collection); var arr = new DefaultArray(); arr.get('num1').should.have.length(0); arr.get('num2').should.have.length(0); var threw1 = false , threw2 = false; try { arr.num1.push({ x: 1 }) arr.num1.push(9) arr.num1.push("woah") } catch (err) { threw1 = true; } threw1.should.equal(false); try { arr.num2.push({ x: 1 }) arr.num2.push(9) arr.num2.push("woah") } catch (err) { threw2 = true; } threw2.should.equal(false); db.close(); }, 'test a model default structure that has a nested Array when instantiated': function () { var db = start() , NestedSchema = new Schema({ nested: { array: [Number] } }); mongoose.model('NestedNumbers', NestedSchema); var NestedNumbers = db.model('NestedNumbers', collection); var nested = new NestedNumbers(); nested.get('nested.array').should.be.an.instanceof(MongooseArray); db.close(); }, 'test a model that defaults an Array attribute to a default non-empty array': function () { var db = start() , DefaultArraySchema = new Schema({ arr: {type: Array, cast: String, default: ['a', 'b', 'c']} }); mongoose.model('DefaultArray', DefaultArraySchema); var DefaultArray = db.model('DefaultArray', collection); var arr = new DefaultArray(); arr.get('arr').should.have.length(3); arr.get('arr')[0].should.equal('a'); arr.get('arr')[1].should.equal('b'); arr.get('arr')[2].should.equal('c'); db.close(); }, 'test a model that defaults an Array attribute to a default single member array': function () { var db = start() , DefaultOneCardArraySchema = new Schema({ arr: {type: Array, cast: String, default: ['a']} }); mongoose.model('DefaultOneCardArray', DefaultOneCardArraySchema); var DefaultOneCardArray = db.model('DefaultOneCardArray', collection); var arr = new DefaultOneCardArray(); arr.get('arr').should.have.length(1); arr.get('arr')[0].should.equal('a'); db.close(); }, 'test a model that defaults an Array attributes to an empty array': function () { var db = start() , DefaultZeroCardArraySchema = new Schema({ arr: {type: Array, cast: String, default: []} }); mongoose.model('DefaultZeroCardArray', DefaultZeroCardArraySchema); var DefaultZeroCardArray = db.model('DefaultZeroCardArray', collection); var arr = new DefaultZeroCardArray(); arr.get('arr').should.have.length(0); arr.arr.should.have.length(0); db.close(); }, 'test that arrays auto-default to the empty array': function () { var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost(); post.numbers.should.have.length(0); db.close(); }, 'test instantiating a model with a hash that maps to at least 1 null value': function () { var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost({ title: null }); should.strictEqual(null, post.title); db.close(); }, 'saving a model with a null value should perpetuate that null value to the db': function () { var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost({ title: null }); should.strictEqual(null, post.title); post.save( function (err) { should.strictEqual(err, null); BlogPost.findById(post.id, function (err, found) { db.close(); should.strictEqual(err, null); should.strictEqual(found.title, null); }); }); }, 'test instantiating a model with a hash that maps to at least 1 undefined value': function () { var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost({ title: undefined }); should.strictEqual(undefined, post.title); post.save( function (err) { should.strictEqual(null, err); BlogPost.findById(post.id, function (err, found) { db.close(); should.strictEqual(err, null); should.strictEqual(found.title, undefined); }); }); }, 'test a model structure when saved': function(){ var db = start() , BlogPost = db.model('BlogPost', collection) , pending = 2; function done () { if (!--pending) db.close(); } var post = new BlogPost(); post.on('save', function (post) { post.get('_id').should.be.an.instanceof(DocumentObjectId); should.equal(undefined, post.get('title')); should.equal(undefined, post.get('slug')); should.equal(undefined, post.get('date')); should.equal(undefined, post.get('published')); post.get('meta').should.be.a('object'); post.get('meta').should.eql({}); should.equal(undefined, post.get('meta.date')); should.equal(undefined, post.get('meta.visitors')); post.get('owners').should.be.an.instanceof(MongooseArray); post.get('comments').should.be.an.instanceof(DocumentArray); done(); }); post.save(function(err, post){ should.strictEqual(err, null); post.get('_id').should.be.an.instanceof(DocumentObjectId); should.equal(undefined, post.get('title')); should.equal(undefined, post.get('slug')); should.equal(undefined, post.get('date')); should.equal(undefined, post.get('published')); post.get('meta').should.be.a('object'); post.get('meta').should.eql({}); should.equal(undefined, post.get('meta.date')); should.equal(undefined, post.get('meta.visitors')); post.get('owners').should.be.an.instanceof(MongooseArray); post.get('comments').should.be.an.instanceof(DocumentArray); done(); }); }, 'test a model structures when created': function () { var db = start() , BlogPost = db.model('BlogPost', collection); BlogPost.create({ title: 'hi there'}, function (err, post) { should.strictEqual(err, null); post.get('_id').should.be.an.instanceof(DocumentObjectId); should.strictEqual(post.get('title'), 'hi there'); should.equal(undefined, post.get('slug')); should.equal(undefined, post.get('date')); post.get('meta').should.be.a('object'); post.get('meta').should.eql({}); should.equal(undefined, post.get('meta.date')); should.equal(undefined, post.get('meta.visitors')); should.strictEqual(undefined, post.get('published')); post.get('owners').should.be.an.instanceof(MongooseArray); post.get('comments').should.be.an.instanceof(DocumentArray); db.close(); }); }, 'test a model structure when initd': function(){ var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost() post.init({ title : 'Test' , slug : 'test' , date : new Date , meta : { date : new Date , visitors : 5 } , published : true , owners : [new DocumentObjectId, new DocumentObjectId] , comments : [ { title: 'Test', date: new Date, body: 'Test' } , { title: 'Super', date: new Date, body: 'Cool' } ] }); post.get('title').should.eql('Test'); post.get('slug').should.eql('test'); post.get('date').should.be.an.instanceof(Date); post.get('meta').should.be.a('object'); post.get('meta').date.should.be.an.instanceof(Date); post.get('meta').visitors.should.be.an.instanceof(MongooseNumber); post.get('published').should.be.true; post.title.should.eql('Test'); post.slug.should.eql('test'); post.date.should.be.an.instanceof(Date); post.meta.should.be.a('object'); post.meta.date.should.be.an.instanceof(Date); post.meta.visitors.should.be.an.instanceof(MongooseNumber); post.published.should.be.true; post.get('owners').should.be.an.instanceof(MongooseArray); post.get('owners')[0].should.be.an.instanceof(DocumentObjectId); post.get('owners')[1].should.be.an.instanceof(DocumentObjectId); post.owners.should.be.an.instanceof(MongooseArray); post.owners[0].should.be.an.instanceof(DocumentObjectId); post.owners[1].should.be.an.instanceof(DocumentObjectId); post.get('comments').should.be.an.instanceof(DocumentArray); post.get('comments')[0].should.be.an.instanceof(EmbeddedDocument); post.get('comments')[1].should.be.an.instanceof(EmbeddedDocument); post.comments.should.be.an.instanceof(DocumentArray); post.comments[0].should.be.an.instanceof(EmbeddedDocument); post.comments[1].should.be.an.instanceof(EmbeddedDocument); db.close(); }, 'test a model structure when partially initd': function(){ var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost() post.init({ title : 'Test' , slug : 'test' , date : new Date }); post.get('title').should.eql('Test'); post.get('slug').should.eql('test'); post.get('date').should.be.an.instanceof(Date); post.get('meta').should.be.a('object'); post.get('meta').should.eql({}); should.equal(undefined, post.get('meta.date')); should.equal(undefined, post.get('meta.visitors')); should.equal(undefined, post.get('published')); post.get('owners').should.be.an.instanceof(MongooseArray); post.get('comments').should.be.an.instanceof(DocumentArray); db.close(); }, 'test initializing with a nested hash': function () { var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost({ meta: { date : new Date , visitors : 5 } }); post.get('meta.visitors').valueOf().should.equal(5); db.close(); }, 'test isNew on embedded documents after initing': function(){ var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost() post.init({ title : 'Test' , slug : 'test' , comments : [ { title: 'Test', date: new Date, body: 'Test' } ] }); post.get('comments')[0].isNew.should.be.false; db.close(); }, 'test isModified when modifying keys': function(){ var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost() post.init({ title : 'Test' , slug : 'test' , date : new Date }); post.isModified('title').should.be.false; post.set('title', 'test'); post.isModified('title').should.be.true; post.isModified('date').should.be.false; post.set('date', new Date(post.date + 1)); post.isModified('date').should.be.true; post.isModified('meta.date').should.be.false; db.close(); }, 'setting a key identically to its current value should not dirty the key': function () { var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost() post.init({ title : 'Test' , slug : 'test' , date : new Date }); post.isModified('title').should.be.false; post.set('title', 'Test'); post.isModified('title').should.be.false; db.close(); }, 'test isModified and isDirectModified on a DocumentArray': function(){ var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost() post.init({ title : 'Test' , slug : 'test' , comments : [ { title: 'Test', date: new Date, body: 'Test' } ] }); post.isModified('comments.0.title').should.be.false; post.get('comments')[0].set('title', 'Woot'); post.isModified('comments').should.be.true; post.isDirectModified('comments').should.be.false; post.isModified('comments.0.title').should.be.true; post.isDirectModified('comments.0.title').should.be.true; db.close(); }, 'test isModified on a DocumentArray with accessors': function(){ var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost() post.init({ title : 'Test' , slug : 'test' , comments : [ { title: 'Test', date: new Date, body: 'Test' } ] }); post.isModified('comments.0.body').should.be.false; post.get('comments')[0].body = 'Woot'; post.isModified('comments').should.be.true; post.isDirectModified('comments').should.be.false; post.isModified('comments.0.body').should.be.true; post.isDirectModified('comments.0.body').should.be.true; db.close(); }, 'test isModified on a MongooseArray with atomics methods': function(){ // COMPLETEME var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost() post.isModified('owners').should.be.false; post.get('owners').$push(new DocumentObjectId); post.isModified('owners').should.be.true; db.close(); }, 'test isModified on a MongooseArray with native methods': function(){ // COMPLETEME var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost() post.isModified('owners').should.be.false; post.get('owners').push(new DocumentObjectId); db.close(); }, // GH-342 'over-writing a number should persist to the db': function () { var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost({ meta: { date : new Date , visitors : 10 } }); post.save( function (err) { should.strictEqual(null, err); post.set('meta.visitors', 20); post.save( function (err) { should.strictEqual(null, err); BlogPost.findById(post.id, function (err, found) { should.strictEqual(null, err); found.get('meta.visitors').valueOf().should.equal(20); db.close(); }); }); }); }, 'test defining a new method': function(){ var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost(); post.cool().should.eql(post); db.close(); }, 'test defining a static': function(){ var db = start() , BlogPost = db.model('BlogPost', collection); BlogPost.woot().should.eql(BlogPost); db.close(); }, 'test casting error': function(){ var db = start() , BlogPost = db.model('BlogPost', collection) , threw = false; var post = new BlogPost(); try { post.init({ date: 'Test' }); } catch(e){ threw = true; } threw.should.be.false; try { post.set('title', 'Test'); } catch(e){ threw = true; } threw.should.be.false; post.save(function(err){ err.should.be.an.instanceof(MongooseError); err.should.be.an.instanceof(CastError); db.close(); }); }, 'test nested casting error': function(){ var db = start() , BlogPost = db.model('BlogPost', collection) , threw = false; var post = new BlogPost(); try { post.init({ meta: { date: 'Test' } }); } catch(e){ threw = true; } threw.should.be.false; try { post.set('meta.date', 'Test'); } catch(e){ threw = true; } threw.should.be.false; post.save(function(err){ err.should.be.an.instanceof(MongooseError); err.should.be.an.instanceof(CastError); db.close(); }); }, 'test casting error in subdocuments': function(){ var db = start() , BlogPost = db.model('BlogPost', collection) , threw = false; var post = new BlogPost() post.init({ title : 'Test' , slug : 'test' , comments : [ { title: 'Test', date: new Date, body: 'Test' } ] }); post.get('comments')[0].set('date', 'invalid'); post.save(function(err){ err.should.be.an.instanceof(MongooseError); err.should.be.an.instanceof(CastError); db.close(); }); }, 'test casting error when adding a subdocument': function(){ var db = start() , BlogPost = db.model('BlogPost', collection) , threw = false; var post = new BlogPost() try { post.get('comments').push({ date: 'Bad date' }); } catch (e) { threw = true; } threw.should.be.false; post.save(function(err){ err.should.be.an.instanceof(MongooseError); err.should.be.an.instanceof(CastError); db.close(); }); }, 'test validation': function(){ function dovalidate (val) { this.asyncScope.should.equal('correct'); return true; } function dovalidateAsync (val, callback) { this.scope.should.equal('correct'); process.nextTick(function () { callback(true); }); } mongoose.model('TestValidation', new Schema({ simple: { type: String, required: true } , scope: { type: String, validate: [dovalidate, 'scope failed'], required: true } , asyncScope: { type: String, validate: [dovalidateAsync, 'async scope failed'], required: true } })); var db = start() , TestValidation = db.model('TestValidation'); var post = new TestValidation(); post.set('simple', ''); post.set('scope', 'correct'); post.set('asyncScope', 'correct'); post.save(function(err){ err.should.be.an.instanceof(MongooseError); err.should.be.an.instanceof(ValidationError); post.set('simple', 'here'); post.save(function(err){ should.strictEqual(err, null); db.close(); }); }); }, 'test validation with custom message': function () { function validate (val) { return val === 'abc'; } mongoose.model('TestValidationMessage', new Schema({ simple: { type: String, validate: [validate, 'must be abc'] } })); var db = start() , TestValidationMessage = db.model('TestValidationMessage'); var post = new TestValidationMessage(); post.set('simple', ''); post.save(function(err){ err.should.be.an.instanceof(MongooseError); err.should.be.an.instanceof(ValidationError); err.errors.simple.should.be.an.instanceof(ValidatorError); err.errors.simple.message.should.equal('Validator "must be abc" failed for path simple'); post.errors.simple.message.should.equal('Validator "must be abc" failed for path simple'); post.set('simple', 'abc'); post.save(function(err){ should.strictEqual(err, null); db.close(); }); }); }, // GH-272 'test validation with Model.schema.path introspection': function () { var db = start(); var IntrospectionValidationSchema = new Schema({ name: String }); var IntrospectionValidation = db.model('IntrospectionValidation', IntrospectionValidationSchema, 'introspections_' + random()); IntrospectionValidation.schema.path('name').validate(function (value) { return value.length < 2; }, 'Name cannot be greater than 1 character'); var doc = new IntrospectionValidation({name: 'hi'}); doc.save( function (err) { err.errors.name.message.should.equal("Validator \"Name cannot be greater than 1 character\" failed for path name"); err.name.should.equal("ValidationError"); err.message.should.equal("Validation failed"); db.close(); }); }, 'test required validation for undefined values': function () { mongoose.model('TestUndefinedValidation', new Schema({ simple: { type: String, required: true } })); var db = start() , TestUndefinedValidation = db.model('TestUndefinedValidation'); var post = new TestUndefinedValidation(); post.save(function(err){ err.should.be.an.instanceof(MongooseError); err.should.be.an.instanceof(ValidationError); post.set('simple', 'here'); post.save(function(err){ should.strictEqual(err, null); db.close(); }); }); }, // GH-319 'save callback should only execute once regardless of number of failed validations': function () { mongoose.model('CallbackFiresOnceValidation', new Schema({ username: { type: String, validate: /^[a-z]{6}$/i } , email: { type: String, validate: /^[a-z]{6}$/i } , password: { type: String, validate: /^[a-z]{6}$/i } })); var db = start() , CallbackFiresOnceValidation = db.model('CallbackFiresOnceValidation'); var post = new CallbackFiresOnceValidation({ username: "nope" , email: "too" , password: "short" }); var timesCalled = 0; post.save(function (err) { err.should.be.an.instanceof(MongooseError); err.should.be.an.instanceof(ValidationError); (++timesCalled).should.eql(1); (Object.keys(err.errors).length).should.eql(3); err.errors.password.should.be.an.instanceof(ValidatorError); err.errors.email.should.be.an.instanceof(ValidatorError); err.errors.username.should.be.an.instanceof(ValidatorError); err.errors.password.message.should.eql('Validator failed for path password'); err.errors.email.message.should.eql('Validator failed for path email'); err.errors.username.message.should.eql('Validator failed for path username'); (Object.keys(post.errors).length).should.eql(3); post.errors.password.should.be.an.instanceof(ValidatorError); post.errors.email.should.be.an.instanceof(ValidatorError); post.errors.username.should.be.an.instanceof(ValidatorError); post.errors.password.message.should.eql('Validator failed for path password'); post.errors.email.message.should.eql('Validator failed for path email'); post.errors.username.message.should.eql('Validator failed for path username'); db.close(); }); }, 'test validation on a query result': function () { mongoose.model('TestValidationOnResult', new Schema({ resultv: { type: String, required: true } })); var db = start() , TestV = db.model('TestValidationOnResult'); var post = new TestV; post.validate(function (err) { err.should.be.an.instanceof(MongooseError); err.should.be.an.instanceof(ValidationError); post.resultv = 'yeah'; post.save(function (err) { should.strictEqual(err, null); TestV.findOne({ _id: post.id }, function (err, found) { should.strictEqual(err, null); found.resultv.should.eql('yeah'); found.save(function(err){ should.strictEqual(err, null); db.close(); }) }); }); }) }, 'test required validation for previously existing null values': function () { mongoose.model('TestPreviousNullValidation', new Schema({ previous: { type: String, required: true } , a: String })); var db = start() , TestP = db.model('TestPreviousNullValidation') TestP.collection.insert({ a: null, previous: null}, {}, function (err, f) { should.strictEqual(err, null); TestP.findOne({_id: f[0]._id}, function (err, found) { should.strictEqual(err, null); found.isNew.should.be.false; should.strictEqual(found.get('previous'), null); found.validate(function(err){ err.should.be.an.instanceof(MongooseError); err.should.be.an.instanceof(ValidationError); found.set('previous', 'yoyo'); found.save(function (err) { should.strictEqual(err, null); db.close(); }); }) }) }); }, 'test nested validation': function(){ mongoose.model('TestNestedValidation', new Schema({ nested: { required: { type: String, required: true } } })); var db = start() , TestNestedValidation = db.model('TestNestedValidation'); var post = new TestNestedValidation(); post.set('nested.required', null); post.save(function(err){ err.should.be.an.instanceof(MongooseError); err.should.be.an.instanceof(ValidationError); post.set('nested.required', 'here'); post.save(function(err){ should.strictEqual(err, null); db.close(); }); }); }, 'test validation in subdocuments': function(){ var Subdocs = new Schema({ required: { type: String, required: true } }); mongoose.model('TestSubdocumentsValidation', new Schema({ items: [Subdocs] })); var db = start() , TestSubdocumentsValidation = db.model('TestSubdocumentsValidation'); var post = new TestSubdocumentsValidation(); post.get('items').push({ required: '' }); post.save(function(err){ err.should.be.an.instanceof(MongooseError); err.should.be.an.instanceof(ValidationError); err.errors.required.should.be.an.instanceof(ValidatorError); err.errors.required.message.should.eql('Validator "required" failed for path required'); post.get('items')[0].set('required', true); post.save(function(err){ should.strictEqual(err, null); db.close(); }); }); }, 'test async validation': function(){ var executed = false; function validator(v, fn){ setTimeout(function () { executed = true; fn(v !== 'test'); }, 50); }; mongoose.model('TestAsyncValidation', new Schema({ async: { type: String, validate: [validator, 'async validator'] } })); var db = start() , TestAsyncValidation = db.model('TestAsyncValidation'); var post = new TestAsyncValidation(); post.set('async', 'test'); post.save(function(err){ err.should.be.an.instanceof(MongooseError); err.should.be.an.instanceof(ValidationError); err.errors.async.should.be.an.instanceof(ValidatorError); err.errors.async.message.should.eql('Validator "async validator" failed for path async'); executed.should.be.true; executed = false; post.set('async', 'woot'); post.save(function(err){ executed.should.be.true; should.strictEqual(err, null); db.close(); }); }); }, 'test nested async validation': function(){ var executed = false; function validator(v, fn){ setTimeout(function () { executed = true; fn(v !== 'test'); }, 50); }; mongoose.model('TestNestedAsyncValidation', new Schema({ nested: { async: { type: String, validate: [validator, 'async validator'] } } })); var db = start() , TestNestedAsyncValidation = db.model('TestNestedAsyncValidation'); var post = new TestNestedAsyncValidation(); post.set('nested.async', 'test'); post.save(function(err){ err.should.be.an.instanceof(MongooseError); err.should.be.an.instanceof(ValidationError); executed.should.be.true; executed = false; post.validate(function(err){ err.should.be.an.instanceof(MongooseError); err.should.be.an.instanceof(ValidationError); executed.should.be.true; executed = false; post.set('nested.async', 'woot'); post.validate(function(err){ executed.should.be.true; should.equal(err, null); executed = false; post.save(function(err){ executed.should.be.true; should.strictEqual(err, null); db.close(); }); }); }) }); }, 'test async validation in subdocuments': function(){ var executed = false; function validator (v, fn) { setTimeout(function(){ executed = true; fn(v !== ''); }, 50); }; var Subdocs = new Schema({ required: { type: String, validate: [validator, 'async in subdocs'] } }); mongoose.model('TestSubdocumentsAsyncValidation', new Schema({ items: [Subdocs] })); var db = start() , Test = db.model('TestSubdocumentsAsyncValidation'); var post = new Test(); post.get('items').push({ required: '' }); post.save(function(err){ err.should.be.an.instanceof(MongooseError); err.should.be.an.instanceof(ValidationError); executed.should.be.true; executed = false; post.get('items')[0].set({ required: 'here' }); post.save(function(err){ executed.should.be.true; should.strictEqual(err, null); db.close(); }); }); }, 'test validation without saving': function(){ mongoose.model('TestCallingValidation', new Schema({ item: { type: String, required: true } })); var db = start() , TestCallingValidation = db.model('TestCallingValidation'); var post = new TestCallingValidation; post.schema.path('item').isRequired.should.be.true; should.strictEqual(post.isNew, true); post.validate(function(err){ err.should.be.an.instanceof(MongooseError); err.should.be.an.instanceof(ValidationError); should.strictEqual(post.isNew, true); post.item = 'yo'; post.validate(function(err){ should.equal(err, null); should.strictEqual(post.isNew, true); db.close(); }); }); }, 'test setting required to false': function () { function validator () { return true; } mongoose.model('TestRequiredFalse', new Schema({ result: { type: String, validate: [validator, 'chump validator'], required: false } })); var db = start() , TestV = db.model('TestRequiredFalse'); var post = new TestV; post.schema.path('result').isRequired.should.be.false; db.close(); }, 'test defaults application': function(){ var now = Date.now(); mongoose.model('TestDefaults', new Schema({ date: { type: Date, default: now } })); var db = start() , TestDefaults = db.model('TestDefaults'); var post = new TestDefaults(); post.get('date').should.be.an.instanceof(Date); (+post.get('date')).should.eql(now); db.close(); }, 'test "type" is allowed as a key': function(){ mongoose.model('TestTypeDefaults', new Schema({ type: { type: String, default: 'YES!' } })); var db = start() , TestDefaults = db.model('TestTypeDefaults'); var post = new TestDefaults(); post.get('type').should.be.a('string'); post.get('type').should.eql('YES!'); // GH-402 var TestDefaults2 = db.model('TestTypeDefaults2', new Schema({ x: { y: { type: { type: String }, owner: String } } })); var post = new TestDefaults2; post.x.y.type = "#402"; post.x.y.owner= "me"; post.save(function (err) { db.close(); should.strictEqual(null, err); }); }, 'test nested defaults application': function(){ var now = Date.now(); mongoose.model('TestNestedDefaults', new Schema({ nested: { date: { type: Date, default: now } } })); var db = start() , TestDefaults = db.model('TestNestedDefaults'); var post = new TestDefaults(); post.get('nested.date').should.be.an.instanceof(Date); (+post.get('nested.date')).should.eql(now); db.close(); }, 'test defaults application in subdocuments': function(){ var now = Date.now(); var Items = new Schema({ date: { type: Date, default: now } }); mongoose.model('TestSubdocumentsDefaults', new Schema({ items: [Items] })); var db = start() , TestSubdocumentsDefaults = db.model('TestSubdocumentsDefaults'); var post = new TestSubdocumentsDefaults(); post.get('items').push({}); post.get('items')[0].get('date').should.be.an.instanceof(Date); (+post.get('items')[0].get('date')).should.eql(now); db.close(); }, // TODO: adapt this text to handle a getIndexes callback that's not unique to // the mongodb-native driver. 'test that indexes are ensured when the model is compiled': function(){ var Indexed = new Schema({ name : { type: String, index: true } , last : String , email : String }); Indexed.index({ last: 1, email: 1 }, { unique: true }); var db = start() , IndexedModel = db.model('IndexedModel', Indexed, 'indexedmodel' + random()) , assertions = 0; IndexedModel.on('index', function(){ IndexedModel.collection.getIndexes(function(err, indexes){ should.strictEqual(err, null); for (var i in indexes) indexes[i].forEach(function(index){ if (index[0] == 'name') assertions++; if (index[0] == 'last') assertions++; if (index[0] == 'email') assertions++; }); assertions.should.eql(3); db.close(); }); }); }, 'test indexes on embedded documents': function () { var BlogPosts = new Schema({ _id : { type: ObjectId, index: true } , title : { type: String, index: true } , desc : String }); var User = new Schema({ name : { type: String, index: true } , blogposts : [BlogPosts] }); var db = start() , UserModel = db.model('DeepIndexedModel', User, 'deepindexedmodel' + random()) , assertions = 0; UserModel.on('index', function () { UserModel.collection.getIndexes(function (err, indexes) { should.strictEqual(err, null); for (var i in indexes) indexes[i].forEach(function(index){ if (index[0] == 'name') assertions++; if (index[0] == 'blogposts._id') assertions++; if (index[0] == 'blogposts.title') assertions++; }); assertions.should.eql(3); db.close(); }); }); }, 'compound indexes on embedded documents should be created': function () { var BlogPosts = new Schema({ title : String , desc : String }); BlogPosts.index({ title: 1, desc: 1 }); var User = new Schema({ name : { type: String, index: true } , blogposts : [BlogPosts] }); var db = start() , UserModel = db.model('DeepCompoundIndexModel', User, 'deepcompoundindexmodel' + random()) , found = 0; UserModel.on('index', function () { UserModel.collection.getIndexes(function (err, indexes) { should.strictEqual(err, null); for (var index in indexes) { switch (index) { case 'name_1': case 'blogposts.title_1_blogposts.desc_1': ++found; break; } } db.close(); found.should.eql(2); }); }); }, 'test getters with same name on embedded documents not clashing': function() { var Post = new Schema({ title : String , author : { name : String } , subject : { name : String } }); mongoose.model('PostWithClashGetters', Post); var db = start() , PostModel = db.model('PostWithClashGetters', 'postwithclash' + random()); var post = new PostModel({ title: 'Test' , author: { name: 'A' } , subject: { name: 'B' } }); post.author.name.should.eql('A'); post.subject.name.should.eql('B'); post.author.name.should.eql('A'); db.close(); }, 'test post save middleware': function () { var schema = new Schema({ title: String }); var called = 0; schema.post('save', function (obj) { obj.title.should.eql('Little Green Running Hood'); called.should.equal(0); called++; }); schema.post('save', function (obj) { obj.title.should.eql('Little Green Running Hood'); called.should.equal(1); called++; }); schema.post('save', function (obj) { obj.title.should.eql('Little Green Running Hood'); called.should.equal(2); db.close(); }); var db = start() , TestMiddleware = db.model('TestPostSaveMiddleware', schema); var test = new TestMiddleware({ title: 'Little Green Running Hood'}); test.save(function(err){ should.strictEqual(err, null); }); }, 'test middleware': function () { var schema = new Schema({ title: String }); var called = 0; schema.pre('init', function (next) { called++; next(); }); schema.pre('save', function (next) { called++; next(new Error('Error 101')); }); schema.pre('remove', function (next) { called++; next(); }); mongoose.model('TestMiddleware', schema); var db = start() , TestMiddleware = db.model('TestMiddleware'); var test = new TestMiddleware(); test.init({ title: 'Test' }); called.should.eql(1); test.save(function(err){ err.should.be.an.instanceof(Error); err.message.should.eql('Error 101'); called.should.eql(2); test.remove(function(err){ should.strictEqual(err, null); called.should.eql(3); db.close(); }); }); }, 'test post init middleware': function () { var schema = new Schema({ title: String }); var preinit = 0 , postinit = 0 schema.pre('init', function (next) { ++preinit; next(); }); schema.post('init', function () { ++postinit; }); mongoose.model('TestPostInitMiddleware', schema); var db = start() , Test = db.model('TestPostInitMiddleware'); var test = new Test({ title: "banana" }); test.save(function(err){ should.strictEqual(err, null); Test.findById(test._id, function (err, test) { should.strictEqual(err, null); preinit.should.eql(1); postinit.should.eql(1); test.remove(function(err){ db.close(); }); }); }); }, 'test updating documents': function () { var db = start() , BlogPost = db.model('BlogPost', collection) , title = 'Tobi ' + random() , author = 'Brian ' + random() , newTitle = 'Woot ' + random() , id0 = new DocumentObjectId , id1 = new DocumentObjectId var post = new BlogPost(); post.set('title', title); post.author = author; post.meta.visitors = 0; post.date = new Date; post.published = true; post.mixed = { x: 'ex' }; post.numbers = [4,5,6,7]; post.owners = [id0, id1]; post.comments = [{ body: 'been there' }, { body: 'done that' }]; post.save(function (err) { should.strictEqual(err, null); BlogPost.findById(post._id, function (err, cf) { should.strictEqual(err, null); cf.title.should.equal(title); cf.author.should.equal(author); cf.meta.visitors.valueOf().should.eql(0); cf.date.should.eql(post.date); cf.published.should.be.true; cf.mixed.x.should.equal('ex'); cf.numbers.toObject().should.eql([4,5,6,7]); cf.owners.length.should.equal(2); cf.owners[0].toString().should.equal(id0.toString()); cf.owners[1].toString().should.equal(id1.toString()); cf.comments.length.should.equal(2); cf.comments[0].body.should.eql('been there'); cf.comments[1].body.should.eql('done that'); var update = { title: newTitle // becomes $set , $inc: { 'meta.visitors': 2 } , $set: { date: new Date } , published: false // becomes $set , 'mixed': { x: 'ECKS', y: 'why' } // $set , $pullAll: { 'numbers': [4, 6] } , $pull: { 'owners': id0 } , 'comments.1.body': 8 } BlogPost.update({ title: title }, update, function (err) { should.strictEqual(err, null); BlogPost.findById(post._id, function (err, up) { should.strictEqual(err, null); up.title.should.equal(newTitle); up.author.should.equal(author); up.meta.visitors.valueOf().should.equal(2); up.date.toString().should.equal(update.$set.date.toString()); up.published.should.eql(false); up.mixed.x.should.equal('ECKS'); up.mixed.y.should.equal('why'); up.numbers.toObject().should.eql([5,7]); up.owners.length.should.equal(1); up.owners[0].toString().should.eql(id1.toString()); up.comments[0].body.should.equal('been there'); up.comments[1].body.should.equal('8'); var update2 = { 'comments.body': 'fail' } BlogPost.update({ _id: post._id }, update2, function (err) { should.strictEqual(!!err, true); ;/^can't append to array using string field name \[body\]/.test(err.message).should.be.true; var update3 = { $pull: 'fail' } BlogPost.update({ _id: post._id }, update3, function (err) { should.strictEqual(!!err, true); ;/Invalid atomic update value/.test(err.message).should.be.true; var update4 = { $inc: { idontexist: 1 } } // should not overwrite doc when no valid paths are submitted BlogPost.update({ _id: post._id }, update4, function (err) { should.strictEqual(err, null); BlogPost.findById(post._id, function (err, up) { should.strictEqual(err, null); up.title.should.equal(newTitle); up.author.should.equal(author); up.meta.visitors.valueOf().should.equal(2); up.date.toString().should.equal(update.$set.date.toString()); up.published.should.eql(false); up.mixed.x.should.equal('ECKS'); up.mixed.y.should.equal('why'); up.numbers.toObject().should.eql([5,7]); up.owners.length.should.equal(1); up.owners[0].toString().should.eql(id1.toString()); up.comments[0].body.should.equal('been there'); up.comments[1].body.should.equal('8'); var update5 = { comments: [{ body: 'worked great' }] , $set: {'numbers.1': 100} , $inc: { idontexist: 1 } } BlogPost.update({ _id: post._id }, update5, function (err) { should.strictEqual(err, null); // get the underlying doc BlogPost.collection.findOne({ _id: post._id }, function (err, doc) { db.close(); should.strictEqual(err, null); var up = new BlogPost; up.init(doc); up.comments.length.should.equal(1); up.comments[0].body.should.equal('worked great'); up.meta.visitors.valueOf().should.equal(2); up.mixed.x.should.equal('ECKS'); up.numbers.toObject().should.eql([5,100]); up.numbers[1].valueOf().should.eql(100); should.strictEqual(undefined, doc.idontexist); doc.numbers[1].should.eql(100); }); }); }); }); }); }); }); }); }); }); }, 'test update doc casting': function () { var db = start() , BlogPost = db.model('BlogPost', collection); var post = new BlogPost(); post.set('title', '1'); var id = post.get('_id'); post.save(function (err) { should.strictEqual(err, null); BlogPost.update({ title: 1, _id: id }, { title: 2 }, function (err) { should.strictEqual(err, null); BlogPost.findOne({ _id: post.get('_id') }, function (err, doc) { should.strictEqual(err, null); doc.get('title').should.eql('2'); db.close(); }); }); }); }, 'test $push casting': function () { var db = start() , BlogPost = db.model('BlogPost', collection) , post = new BlogPost(); post.get('numbers').push('3'); post.get('numbers')[0].should.equal(3); db.close(); }, 'test $pull casting': function () { var db = start() , BlogPost = db.model('BlogPost', collection) , post = new BlogPost(); post.get('numbers').push(1, 2, 3, 4); post.save( function (err) { BlogPost.findById( post.get('_id'), function (err, found) { found.get('numbers').length.should.equal(4); found.get('numbers').$pull('3'); found.save( function (err) { BlogPost.findById( found.get('_id'), function (err, found2) { found2.get('numbers').length.should.equal(3); db.close(); }); }); }); }); }, 'test updating numbers atomically': function () { var db = start() , BlogPost = db.model('BlogPost', collection) , totalDocs = 4 , saveQueue = []; var post = new BlogPost(); post.set('meta.visitors', 5); post.save(function(err){ if (err) throw err; BlogPost.findOne({ _id: post.get('_id') }, function(err, doc){ if (err) throw err; doc.get('meta.visitors').increment(); doc.get('meta.visitors').valueOf().should.be.equal(6); save(doc); }