mongoose-id-validator
Version:
Mongoose plugin to validate that ObjectID references refer to objects that actually exist in the referenced collection
700 lines (629 loc) • 22.5 kB
JavaScript
var mongoose = require('mongoose')
var validator = require('../lib/id-validator')
var async = require('async')
var should = require('should')
var IdValidator = require('../lib/id-validator').getConstructor
var Schema = mongoose.Schema
var url = 'mongodb://localhost:27017/mongoose-id-validator'
if (process.env.MONGO_PORT_27017_TCP_PORT) {
url = 'mongodb://' + process.env.MONGO_PORT_27017_TCP_ADDR + ':' +
process.env.MONGO_PORT_27017_TCP_PORT + '/mongoose-id-validator'
}
var connection2
function validatorConcept(schema) {
var idvalidator = new IdValidator()
schema.plugin(IdValidator.prototype.validate.bind(idvalidator))
schema.statics.enableValidation = function () {
idvalidator.enable()
}
schema.statics.disableValidation = function () {
idvalidator.disable()
}
}
before(function (done) {
mongoose.connect(url, { useNewUrlParser: true }, done)
connection2 = mongoose.createConnection(url + '2', { useNewUrlParser: true })
})
describe('mongoose-id-validator Integration Tests', function () {
var ManufacturerSchema = new Schema({
name: String
})
var Manufacturer = mongoose.model('Manufacturer', ManufacturerSchema)
var ColourSchema = new Schema({
name: String
})
var Colour = mongoose.model('Colour', ColourSchema)
var colours = {}
var saveColours = []
'red green black blue silver'.split(' ').forEach(function (c) {
saveColours.push(function (cb) {
var newColour = new Colour({
name: c
})
colours[c] = newColour
newColour.save(cb)
})
})
var CarSchema = new Schema({
name: String,
manufacturer: {
type: Schema.Types.ObjectId,
ref: 'Manufacturer'
},
colours: [
{
type: Schema.Types.ObjectId,
ref: 'Colour'
}
]
})
CarSchema.plugin(validator, {
message: '{PATH} ID is bad'
})
var Car = mongoose.model('Car', CarSchema)
var BikeSchema = new Schema({
name: String,
manufacturer: {
type: Schema.Types.ObjectId,
ref: 'Manufacturer'
},
colours: [
{
type: Schema.Types.ObjectId,
ref: 'Colour'
}
]
})
validatorConcept(BikeSchema)
var Bike = mongoose.model('Bike', BikeSchema)
beforeEach(function (done) {
async.parallel([
Manufacturer.deleteMany.bind(Manufacturer, {}),
Colour.deleteMany.bind(Colour, {}),
Car.deleteMany.bind(Car, {}),
Bike.deleteMany.bind(Bike, {})
], function (err) {
if (err) {
return done(err)
}
colours = []
async.parallel(saveColours, done)
})
})
it('Should allow no manufacturer/colour IDs as developer can use '
+ 'mongoose required option to make these mandatory', function (done) {
var c = new Car({
name: 'Test Car'
})
c.save(done)
})
it('Should pass validation with explicit null ID', function (done) {
var c = new Car({
name: 'Test Car',
manufacturer: null
})
c.validate(done)
})
it('Should pass validation with explicit undefined ID', function (done) {
var c = new Car({
name: 'Test Car',
manufacturer: undefined
})
c.validate(done)
})
it('Should pass validation with explicit null array', function (done) {
var c = new Car({
name: 'Test Car',
colours: null
})
c.save(done)
})
it('Should pass validation with explicit undefined array', function (done) {
var c = new Car({
name: 'Test Car',
colours: undefined
})
c.save(done)
})
it('Should pass validation with existing ID', function (done) {
var m = new Manufacturer({
name: 'Car Maker'
})
var c = new Car({
name: 'Test Car',
manufacturer: m
})
async.series([
m.save.bind(m),
c.save.bind(c)
], done)
})
it('Should fail validation with custom message on bad ID', function (done) {
var c = new Car({
name: 'Test Car',
manufacturer: '50136e40c78c4b9403000001'
})
c.validate(function (err) {
err.name.should.eql('ValidationError')
err.errors.manufacturer.message.should.eql('manufacturer ID is bad')
done()
})
})
it('Should fail validation on bad ID with IdValidator instance',
function (done) {
var b = new Bike({
name: 'Test Bike',
manufacturer: '50136e40c78c4b9403000001'
})
b.validate(function (err) {
err.name.should.eql('ValidationError')
err.errors.manufacturer.message.should.eql(
'manufacturer references a non existing ID')
done()
})
})
it('Should ignore validation when it is disabled', function (done) {
Bike.disableValidation()
var b = new Bike({
name: 'Test Bike',
manufacturer: '50136e40c78c4b9403000001'
})
b.save(done)
})
it('Should fail validation if bad ID set after previously good ID value',
function (done) {
var savePassed = false
var m = new Manufacturer({
name: 'Car Maker'
})
var c = new Car({
name: 'Test Car',
manufacturer: m
})
async.series([
m.save.bind(m),
c.save.bind(c),
function (cb) {
savePassed = true
c.manufacturer = '50136e40c78c4b9403000001'
c.save(cb)
}
], function (err) {
should(savePassed).be.ok
err.name.should.eql('ValidationError')
err.errors.manufacturer.message.should.eql(
'manufacturer ID is bad')
done()
})
})
it(
'Should pass validation if no ID value changed (even when manufacturer subsequently removed)',
function (done) {
var m = new Manufacturer({
name: 'Car Maker'
})
var c = new Car({
name: 'Test Car',
manufacturer: m
})
async.series([
m.save.bind(m),
c.save.bind(c),
Manufacturer.deleteMany.bind(Manufacturer, {}),
c.save.bind(c)
], done)
})
it('Should validate correctly IDs in an array of ID references',
function (done) {
var c = new Car({
name: 'Test Car',
colours: [
colours['red'],
colours['blue'],
colours['black']
]
})
c.save(done)
})
it('Should fail ID validation in an array of ID references',
function (done) {
var c = new Car({
name: 'Test Car',
colours: [
colours['red'],
'50136e40c78c4b9403000001',
colours['black']
]
})
c.save(function (err) {
err.name.should.eql('ValidationError')
err.errors.colours.message.should.eql('colours ID is bad')
done()
})
})
it(
'Array of ID values should pass validation if not modified since last save',
function (done) {
var c = new Car({
type: Schema.Types.ObjectId,
colours: [
colours['red'],
colours['blue'],
colours['black']
]
})
async.series([
c.save.bind(c),
function (cb) {
colours['blue'].remove(cb)
},
c.validate.bind(c)
], done)
})
it('Should not trigger ref validation if path not modified',
function (done) {
var m = new Manufacturer({})
var c = new Car({
manufacturer: m._id,
name: 'c'
})
var called = 0
var tmp = Manufacturer.countDocuments
Manufacturer.countDocuments = function () {
called++
return tmp.apply(this, arguments)
}
async.waterfall([
function (cb) {
m.save(cb)
},
function (_, cb) {
c.save(cb)
},
function (_, cb) {
Car.findById(c._id, cb)
},
function (c, cb) {
c.name = 'd'
c.validate(cb)//must not trigger a count as manufacturerId not modified
},
function (cb) {
should(called).be.equal(1)
cb(null)
}
], function (err) {
Manufacturer.countDocuments = tmp
done(err)
})
})
describe('refConditions tests', function () {
var PersonSchema = new Schema({
name: String,
gender: {
type: String,
enum: ['m', 'f']
}
})
var Person = mongoose.model('Person', PersonSchema)
var InfoSchema = new Schema({
bestMaleFriend: {
type: Schema.Types.ObjectId,
ref: 'Person',
refConditions: {
gender: 'm'
}
},
femaleFriends: [
{
type: Schema.Types.ObjectId,
ref: 'Person',
refConditions: {
gender: 'f'
}
}
]
})
InfoSchema.plugin(validator)
var Info = mongoose.model('Info', InfoSchema)
var jack = new Person({ name: 'Jack', gender: 'm' })
var jill = new Person({ name: 'Jill', gender: 'f' })
var ann = new Person({ name: 'Ann', gender: 'f' })
before(function (done) {
async.series([
Person.deleteMany.bind(Person, {}),
Info.deleteMany.bind(Info, {}),
jack.save.bind(jack),
jill.save.bind(jill),
ann.save.bind(ann)
], done)
})
it('Should validate with single ID value that matches condition',
function (done) {
var i = new Info({ bestMaleFriend: jack })
i.validate(done)
})
it(
'Should fail to validate single ID value that exists but does not match conditions',
function (done) {
var i = new Info({ bestMaleFriend: jill })
i.validate(function (err) {
err.should.property('name', 'ValidationError')
err.errors.should.property('bestMaleFriend')
done()
})
})
it('Should validate array of ID values that match conditions',
function (done) {
var i = new Info({ femaleFriends: [ann, jill] })
i.validate(done)
})
it(
'Should not validate array of ID values containing value that exists but does not match conditions',
function (done) {
var i = new Info({ femaleFriends: [jill, jack] })
i.validate(function (err) {
err.should.property('name', 'ValidationError')
err.errors.should.property('femaleFriends')
done()
})
})
})
describe('refConditions with function tests', function () {
var PeopleSchema = new Schema({
name: String,
gender: {
type: String,
enum: ['m', 'f']
}
})
var People = mongoose.model('People', PeopleSchema)
var FriendSchema = new Schema({
mustBeFemale: Boolean,
bestFriend: {
type: Schema.Types.ObjectId,
ref: 'People',
refConditions: {
gender: function () {
return this.mustBeFemale ? 'f' : 'm'
}
}
},
friends: [
{
type: Schema.Types.ObjectId,
ref: 'People',
refConditions: {
gender: function () {
return this.mustBeFemale ? 'f' : 'm'
}
}
}
]
})
FriendSchema.plugin(validator)
var Friends = mongoose.model('Friends', FriendSchema)
var jack = new People({ name: 'Jack', gender: 'm' })
var jill = new People({ name: 'Jill', gender: 'f' })
var ann = new People({ name: 'Ann', gender: 'f' })
before(function (done) {
async.series([
People.deleteMany.bind(People, {}),
Friends.deleteMany.bind(Friends, {}),
jack.save.bind(jack),
jill.save.bind(jill),
ann.save.bind(ann)
], done)
})
it('Should validate with single ID value that matches condition',
function (done) {
var i = new Friends({ mustBeFemale: false, bestFriend: jack })
i.validate(done)
})
it(
'Should fail to validate single ID value that exists but does not match conditions',
function (done) {
var i = new Friends({ mustBeFemale: true, bestFriend: jack })
i.validate(function (err) {
err.should.property('name', 'ValidationError')
err.errors.should.property('bestFriend')
done()
})
})
it('Should validate array of ID values that match conditions',
function (done) {
var i = new Friends({ mustBeFemale: true, friends: [ann, jill] })
i.validate(done)
})
it(
'Should not validate array of ID values containing value that exists but does not match conditions',
function (done) {
var i = new Friends({
mustBeFemale: true,
friends: [jill, jack]
})
i.validate(function (err) {
err.should.property('name', 'ValidationError')
err.errors.should.property('friends')
done()
})
})
})
describe('Array Duplicate Tests', function () {
var InventoryItemSchema = new Schema({
name: String
})
function createInventorySchema(options) {
var s = new Schema({
items: [
{
type: Schema.Types.ObjectId,
ref: 'InventoryItem'
}
]
})
s.plugin(validator, options)
return s
}
var InventoryNoDuplicatesSchema = createInventorySchema()
var InventoryDuplicatesSchema = createInventorySchema({
allowDuplicates: true
})
var InventoryItem = mongoose.model('InventoryItem', InventoryItemSchema)
var InventoryNoDuplicates = mongoose.model('InventoryNoDuplicates',
InventoryNoDuplicatesSchema)
var InventoryDuplicates = mongoose.model('InventoryDuplicatesSchema',
InventoryDuplicatesSchema)
var item1 = new InventoryItem({ name: 'Widgets' })
before(function (done) {
async.series([
item1.save.bind(item1)
], done)
})
it('Should fail to validate duplicate entries with default option',
function (done) {
var i = new InventoryNoDuplicates({ items: [item1, item1] })
i.validate(function (err) {
err.should.property('name', 'ValidationError')
err.errors.should.property('items')
done()
})
})
it('Should pass validation of duplicate entries when allowDuplicates set',
function (done) {
var i = new InventoryDuplicates({ items: [item1, item1] })
i.validate(done)
})
})
describe('Recursion Tests', function () {
var contactSchema = new mongoose.Schema({})
var listSchema = new mongoose.Schema({
name: String,
contacts: [
{
reason: String,
contactId: {
type: Schema.Types.ObjectId,
ref: 'Contact'
}
}]
})
listSchema.plugin(validator)
var Contact = mongoose.model('Contact', contactSchema)
var List = mongoose.model('List', listSchema)
it('Should allow empty array', function (done) {
var obj = new List({ name: 'Test', contacts: [] })
obj.validate(done)
})
it('Should fail on invalid ID inside sub-schema', function (done) {
var obj = new List({
name: 'Test', contacts: [
{ reason: 'My friend', contactId: '50136e40c78c4b9403000001' }
]
})
obj.validate(function (err) {
err.should.property('name', 'ValidationError')
err.errors.should.property('contacts.0.contactId')
done()
})
})
it('Should pass on valid ID in sub-schema', function (done) {
var c = new Contact({})
async.series([
function (cb) {
c.save(cb)
},
function (cb) {
var obj = new List({
name: 'Test', contacts: [
{ reason: 'My friend', contactId: c }
]
})
obj.validate(cb)
}
], done)
})
})
describe('Self recursive schema', function () {
var Tasks = new mongoose.Schema()
Tasks.add({
title: String,
subtasks: [Tasks]
})
Tasks.plugin(validator)
var Task = mongoose.model('Tasks', Tasks)
it('Should validate recursive task', function (done) {
var t1 = new Task({ title: 'Task 1' })
var t2 = new Task({ title: 'Task 2', subtasks: [t1] })
async.series([
function (cb) {
t1.save(cb)
},
function (cb) {
t2.save(cb)
}
], done)
})
})
describe('Connection tests', function () {
it('Correct connection should be used when specified as option',
function (done) {
var UserSchema = new Schema({
name: String
})
var User1 = mongoose.model('User', UserSchema)
var User2 = connection2.model('User', UserSchema)
var ItemSchema1 = new Schema({
owner: {
type: Schema.Types.ObjectId,
ref: 'User'
}
})
ItemSchema1.plugin(validator)
var ItemSchema2 = new Schema({
owner: {
type: Schema.Types.ObjectId,
ref: 'User'
}
})
ItemSchema2.plugin(validator, {
connection: connection2
})
var Item1 = mongoose.model('Item', ItemSchema1)
var Item2 = connection2.model('Item', ItemSchema2)
var u1 = new User1({ _id: '50136e40c78c4b9403000001' })
var u2 = new User2({ _id: '50136e40c78c4b9403000002' })
var i1 = new Item1({ owner: '50136e40c78c4b9403000001' })
var i2 = new Item2({ owner: '50136e40c78c4b9403000002' })
var bad1 = new Item1({ owner: '50136e40c78c4b9403000002' })
var bad2 = new Item2({ owner: '50136e40c78c4b9403000001' })
async.series([
function (cb) {
async.parallel(mongoose.connections.map(function (c) {
return c.db.dropDatabase.bind(c.db)
}), cb)
},
function (cb) {
async.series([u1, u2, i1, i2].map(function (o) {
return o.save.bind(o)
}), cb)
},
function (cb) {
bad1.validate(function (err) {
should(!!err).eql(true)
err.should.property('name', 'ValidationError')
err.errors.should.property('owner')
cb()
})
},
function (cb) {
bad2.validate(function (err) {
should(!!err).eql(true)
err.should.property('name', 'ValidationError')
err.errors.should.property('owner')
cb()
})
}
], done)
})
})
})