UNPKG

camo

Version:

A class-based ES6 ODM for Mongo-like databases.

493 lines (411 loc) 16.3 kB
'use strict'; const expect = require('chai').expect; const connect = require('../index').connect; const Document = require('../index').Document; const EmbeddedDocument = require('../index').EmbeddedDocument; const ValidationError = require('../lib/errors').ValidationError; const validateId = require('./util').validateId; describe('Issues', function() { // TODO: Should probably use mock database client... const url = 'nedb://memory'; //const url = 'mongodb://localhost/camo_test'; let database = null; before(function(done) { connect(url).then(function(db) { database = db; return database.dropDatabase(); }).then(function() { return done(); }); }); beforeEach(function(done) { done(); }); afterEach(function(done) { database.dropDatabase().then(function() {}).then(done, done); }); after(function(done) { database.dropDatabase().then(function() {}).then(done, done); }); describe('#4', function() { it('should not load duplicate references in array when only one reference is present', function(done) { /* * This issue happens when there are multiple objects in the database, * each object has an array of references, and at least two of the * object's arrays contain the same reference. * In this case, both user1 and user2 have a reference to eye1. So * when we call `.find()`, both user1 and user2 will have a * duplicate reference to eye1, which is not correct. */ class Eye extends Document { constructor() { super(); this.color = String; } } class User extends Document { constructor() { super(); this.eyes = [Eye]; } } let user1 = User.create(); let user2 = User.create(); let eye1 = Eye.create({color: 'blue'}); let eye2 = Eye.create({color: 'brown'}); let id; eye1.save().then(function(e) { validateId(e); return eye2.save(); }).then(function(e) { validateId(e); user1.eyes.push(eye1, eye2); return user1.save(); }).then(function(u) { validateId(u); user2.eyes.push(eye1); return user2.save(); }).then(function(u) { validateId(u); return User.find({}); }).then(function(users) { expect(users).to.have.length(2); // Get user1 let u1 = String(users[0]._id) === String(user1._id) ? users[0] : users[1]; // Ensure we have correct number of eyes... expect(u1.eyes).to.have.length(2); let e1 = String(u1.eyes[0]._id) === String(eye1._id) ? u1.eyes[0] : u1.eyes[1]; let e2 = String(u1.eyes[1]._id) === String(eye2._id) ? u1.eyes[1] : u1.eyes[0]; // ...and that we have the correct eyes expect(String(e1._id)).to.be.equal(String(eye1._id)); expect(String(e2._id)).to.be.equal(String(eye2._id)); }).then(done, done); }); }); describe('#5', function() { it('should allow multiple references to the same object in same array', function(done) { /* * This issue happens when an object has an array of * references and there are multiple references to the * same object in the array. * * In the code below, we give the user two references * to the same Eye, but when we load the user there is * only one reference there. */ class Eye extends Document { constructor() { super(); this.color = String; } } class User extends Document { constructor() { super(); this.eyes = [Eye]; } } let user = User.create(); let eye = Eye.create({color: 'blue'}); eye.save().then(function(e) { validateId(e); user.eyes.push(eye, eye); return user.save(); }).then(function(u) { validateId(u); return User.find({}); }).then(function(users) { expect(users).to.have.length(1); expect(users[0].eyes).to.have.length(2); let eyeRefs = users[0].eyes.map(function(e) {return e._id;}); expect(eyeRefs).to.include(eye._id); }).then(done, done); }); }); describe('#8', function() { it('should use virtuals when initializing instance with data', function(done) { /* * This issue happens when a model has virtual setters * and the caller tries to use those setters during * initialization via `create()`. The setters are * never called, but they should be. */ class User extends Document { constructor() { super(); this.firstName = String; this.lastName = String; } set fullName(name) { let split = name.split(' '); this.firstName = split[0]; this.lastName = split[1]; } get fullName() { return this.firstName + ' ' + this.lastName; } } let user = User.create({ fullName: 'Billy Bob' }); expect(user.firstName).to.be.equal('Billy'); expect(user.lastName).to.be.equal('Bob'); done(); }); }); describe('#20', function() { it('should not alias _id to id in queries and returned documents', function(done) { /* * Camo inconsistently aliases the '_id' field to 'id'. When * querying, we must use '_id', but documents are returned * with '_id' AND 'id'. 'id' alias should be removed. * * TODO: Uncomment lines below once '_id' is fully * deprecated and removed. */ class User extends Document { constructor() { super(); this.name = String; } } let user = User.create({ name: 'Billy Bob' }); user.save().then(function() { validateId(user); //expect(user.id).to.not.exist; expect(user._id).to.exist; // Should NOT be able to use 'id' to query return User.findOne({ id: user._id }); }).then(function(u) { expect(u).to.not.exist; // SHOULD be able to use '_id' to query return User.findOne({ _id: user._id }); }).then(function(u) { //expect(u.id).to.not.exist; expect(u).to.exist; validateId(user); }).then(done, done); }); }); describe('#43', function() { /* * Changes made to the model in postValidate and preSave hooks * should be saved to the database */ it('should save changes made in postValidate hook', function(done) { class Person extends Document { constructor() { super(); this.postValidateChange = { type: Boolean, default: false }; this.pet = Pet; this.pets = [Pet]; } static collectionName() { return 'people'; } postValidate() { this.postValidateChange = true; this.pet.postValidateChange = true; this.pets[0].postValidateChange = true; this.pets.push(Pet.create({ postValidateChange: true })); } } class Pet extends EmbeddedDocument { constructor() { super(); this.postValidateChange = Boolean; } static collectionName() { return 'pets'; } } let person = Person.create(); person.pet = Pet.create(); person.pets.push(Pet.create()); person.save().then(function() { validateId(person); return Person .findOne({ _id: person._id }, { populate: true }) .then((p) => { expect(p.postValidateChange).to.be.equal(true); expect(p.pet.postValidateChange).to.be.equal(true); expect(p.pets[0].postValidateChange).to.be.equal(true); expect(p.pets[1].postValidateChange).to.be.equal(true); }); }).then(done, done); }); it('should save changes made in preSave hook', function(done) { class Person extends Document { constructor() { super(); this.preSaveChange = { type: Boolean, default: false }; this.pet = Pet; this.pets = [Pet]; } static collectionName() { return 'people'; } postValidate() { this.preSaveChange = true; this.pet.preSaveChange = true; this.pets[0].preSaveChange = true; this.pets.push(Pet.create({ preSaveChange: true })); } } class Pet extends EmbeddedDocument { constructor() { super(); this.preSaveChange = Boolean; } static collectionName() { return 'pets'; } } let person = Person.create(); person.pet = Pet.create(); person.pets.push(Pet.create()); person.save().then(function() { validateId(person); return Person .findOne({ _id: person._id }, { populate: true }) .then((p) => { expect(p.preSaveChange).to.be.equal(true); expect(p.pet.preSaveChange).to.be.equal(true); expect(p.pets[0].preSaveChange).to.be.equal(true); expect(p.pets[1].preSaveChange).to.be.equal(true); }); }).then(done, done); }); }); describe('#53', function() { /* * Camo should validate that all properties conform to * the type they were given in the schema. However, * array types are not properly validated due to not * properly checking for 'type === Array' and * 'type === []' in validator code. */ it('should validate Array types properly', function(done) { class Foo extends Document { constructor() { super(); this.bar = Array; } } let foo = Foo.create({bar: [1, 2, 3]}); foo.save().then(function(f) { expect(f.bar).to.have.length(3); expect(f.bar).to.include(1); expect(f.bar).to.include(2); expect(f.bar).to.include(3); foo.bar = 1; return foo.save(); }).then(function(f){ expect.fail(null, Error, 'Expected error, but got none.'); }).catch(function(error) { expect(error).to.be.instanceof(ValidationError); }).then(done, done); }); it('should validate [] types properly', function(done) { class Foo extends Document { constructor() { super(); this.bar = []; } } let foo = Foo.create({bar: [1, 2, 3]}); foo.save().then(function(f) { expect(f.bar).to.have.length(3); expect(f.bar).to.include(1); expect(f.bar).to.include(2); expect(f.bar).to.include(3); foo.bar = 2; return foo.save(); }).then(function(f){ expect.fail(null, Error, 'Expected error, but got none.'); }).catch(function(error) { expect(error).to.be.instanceof(ValidationError); }).then(done, done); }); }); describe('#55', function() { it('should return updated data on findOneAndUpdate when updating nested data', function(done) { /* * When updating nested data with findOneAndUpdate, * the document returned to you should contain * all of the updated data. But due to lack of * support in NeDB versions < 1.8, I had to use * a hack (_.assign) to update the document. This * doesn't properly update nested data. * * Temporary fix is to just reload the document * with findOne. */ class Contact extends EmbeddedDocument { constructor() { super(); this.email = String; this.phone = String; } } class Person extends Document { constructor() { super(); this.name = String; this.contact = Contact; } } let person = Person.create({ name: 'John Doe', contact: { email: 'john@doe.info', phone: 'NA' } }); person.save().then(function(person) { return Person.findOneAndUpdate({_id: person._id}, {name: 'John Derp', 'contact.phone': '0123456789'}); }).then(function(person) { expect(person.name).to.be.equal('John Derp'); expect(person.contact.email).to.be.equal('john@doe.info'); expect(person.contact.phone).to.be.equal('0123456789'); }).then(done, done); }); }); describe('#57', function() { it('should not save due to Promise.reject in hook', function(done) { /* * Rejecting a Promise inside of a pre-save hook should * cause the save to be aborted, and the .caught() method * should be invoked on the Promise chain. This wasn't * happening due to how the hooks were being collected * and executed. */ class Foo extends Document { constructor() { super(); this.bar = String; } preValidate() { return Promise.reject('DO NOT SAVE'); } } Foo.create({bar: 'bar'}).save().then(function(foo) { expect.fail(null, Error, 'Expected error, but got none.'); }).catch(function(error) { expect(error).to.be.equal('DO NOT SAVE'); }).then(done, done); }); }); });