// Copyright IBM Corp. 2013,2016. All Rights Reserved. // Node module: loopback-datasource-juggler // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT // This test written in mocha+should.js 'use strict'; var should = require('./init.js'); var async = require('async'); var j = require('../'), db, User; var ValidationError = j.ValidationError; function getValidAttributes() { return { name: 'Anatoliy', email: 'email@example.com', state: '', age: 26, gender: 'male', createdByAdmin: false, createdByScript: true, }; } describe('validations', function() { var User, Entry; before(function(done) { db = getSchema(); User = db.define('User', { email: String, name: String, password: String, state: String, age: Number, gender: String, domain: String, pendingPeriod: Number, createdByAdmin: Boolean, createdByScript: Boolean, updatedAt: Date, }); Entry = db.define('Entry', { id: {type: 'string', id: true, generated: false}, name: {type: 'string'}, }); Entry.validatesUniquenessOf('id'); db.automigrate(done); }); beforeEach(function(done) { User.destroyAll(function() { delete User.validations; done(); }); }); after(function() { // db.disconnect(); }); describe('commons', function() { describe('skipping', function() { it('should NOT skip when `if` is fulfilled', function() { User.validatesPresenceOf('pendingPeriod', {if: 'createdByAdmin'}); var user = new User; user.createdByAdmin = true; user.isValid().should.be.false; user.errors.pendingPeriod.should.eql(['can\'t be blank']); user.pendingPeriod = 1; user.isValid().should.be.true; }); it('should skip when `if` is NOT fulfilled', function() { User.validatesPresenceOf('pendingPeriod', {if: 'createdByAdmin'}); var user = new User; user.createdByAdmin = false; user.isValid().should.be.true; user.errors.should.be.false; user.pendingPeriod = 1; user.isValid().should.be.true; }); it('should NOT skip when `unless` is fulfilled', function() { User.validatesPresenceOf('pendingPeriod', {unless: 'createdByAdmin'}); var user = new User; user.createdByAdmin = false; user.isValid().should.be.false; user.errors.pendingPeriod.should.eql(['can\'t be blank']); user.pendingPeriod = 1; user.isValid().should.be.true; }); it('should skip when `unless` is NOT fulfilled', function() { User.validatesPresenceOf('pendingPeriod', {unless: 'createdByAdmin'}); var user = new User; user.createdByAdmin = true; user.isValid().should.be.true; user.errors.should.be.false; user.pendingPeriod = 1; user.isValid().should.be.true; }); }); describe('skipping in async validation', function() { it('should skip when `if` is NOT fulfilled', function(done) { User.validateAsync('pendingPeriod', function(err, done) { if (!this.pendingPeriod) err(); done(); }, {if: 'createdByAdmin', code: 'presence', message: 'can\'t be blank'}); var user = new User; user.createdByAdmin = false; user.isValid(function(valid) { valid.should.be.true; user.errors.should.be.false; done(); }); }); it('should NOT skip when `if` is fulfilled', function(done) { User.validateAsync('pendingPeriod', function(err, done) { if (!this.pendingPeriod) err(); done(); }, {if: 'createdByAdmin', code: 'presence', message: 'can\'t be blank'}); var user = new User; user.createdByAdmin = true; user.isValid(function(valid) { valid.should.be.false; user.errors.pendingPeriod.should.eql(['can\'t be blank']); done(); }); }); it('should skip when `unless` is NOT fulfilled', function(done) { User.validateAsync('pendingPeriod', function(err, done) { if (!this.pendingPeriod) err(); done(); }, {unless: 'createdByAdmin', code: 'presence', message: 'can\'t be blank'}); var user = new User; user.createdByAdmin = true; user.isValid(function(valid) { valid.should.be.true; user.errors.should.be.false; done(); }); }); it('should NOT skip when `unless` is fulfilled', function(done) { User.validateAsync('pendingPeriod', function(err, done) { if (!this.pendingPeriod) err(); done(); }, {unless: 'createdByAdmin', code: 'presence', message: 'can\'t be blank'}); var user = new User; user.createdByAdmin = false; user.isValid(function(valid) { valid.should.be.false; user.errors.pendingPeriod.should.eql(['can\'t be blank']); done(); }); }); }); describe('lifecycle', function() { it('should work on create', function(done) { delete User.validations; User.validatesPresenceOf('name'); User.create(function(e, u) { should.exist(e); User.create({name: 'Valid'}, function(e, d) { should.not.exist(e); done(); }); }); }); it('should work on update', function(done) { delete User.validations; User.validatesPresenceOf('name'); User.create({name: 'Valid'}, function(e, d) { d.updateAttribute('name', null, function(e) { should.exist(e); e.should.be.instanceOf(Error); e.should.be.instanceOf(ValidationError); d.updateAttribute('name', 'Vasiliy', function(e) { should.not.exist(e); done(); }); }); }); }); it('should ignore errors on upsert by default', function(done) { delete User.validations; User.validatesPresenceOf('name'); // It's important to pass an id value, otherwise DAO falls back // to regular create() User.updateOrCreate({id: 999}, done); }); it('should be skipped by upsert when disabled via settings', function(done) { var Customer = User.extend('Customer'); Customer.attachTo(db); db.autoupdate(function(err) { if (err) return done(err); Customer.prototype.isValid = function() { throw new Error('isValid() should not be called at all'); }; Customer.settings.validateUpsert = false; // It's important to pass an id value, otherwise DAO falls back // to regular create() Customer.updateOrCreate({id: 999}, done); }); }); it('should work on upsert when enabled via settings', function(done) { delete User.validations; User.validatesPresenceOf('name'); User.settings.validateUpsert = true; // It's important to pass an id value, otherwise DAO falls back // to regular create() User.upsert({id: 999}, function(err, u) { if (!err) return done(new Error('Validation should have failed.')); err.should.be.instanceOf(ValidationError); done(); }); }); it('should return error code', function(done) { delete User.validations; User.validatesPresenceOf('name'); User.create(function(e, u) { should.exist(e); e.details.codes.name.should.eql(['presence']); done(); }); }); it('should allow to modify error after validation', function(done) { User.afterValidate = function(next) { next(); }; done(); }); it('should include validation messages in err.message', function(done) { delete User.validations; User.validatesPresenceOf('name'); User.create(function(e, u) { should.exist(e); e.message.should.match(/`name` can't be blank/); done(); }); }); it('should include property value in err.message', function(done) { delete User.validations; User.validatesPresenceOf('name'); User.create(function(e, u) { should.exist(e); e.message.should.match(/`name` can't be blank \(value: undefined\)/); done(); }); }); it('should include model name in err.message', function(done) { delete User.validations; User.validatesPresenceOf('name'); User.create(function(e, u) { should.exist(e); e.message.should.match(/`User` instance/i); done(); }); }); it('should return validation metadata', function() { var expected = {name: [{validation: 'presence', options: {}}]}; delete User.validations; User.validatesPresenceOf('name'); var validations = User.validations; validations.should.eql(expected); }); }); }); describe('validation with or without options', function() { it('should work on update with options', function(done) { delete User.validations; User.validatesPresenceOf('name'); User.create({name: 'Valid'}, function(e, d) { d.updateAttribute('name', null, {options: 'options'}, function(e) { should.exist(e); e.should.be.instanceOf(Error); e.should.be.instanceOf(ValidationError); d.updateAttribute('name', 'Vasiliy', {options: 'options'}, function(e) { should.not.exist(e); done(); }); }); }); }); it('should work on update without options', function(done) { delete User.validations; User.validatesPresenceOf('name'); User.create({name: 'Valid'}, function(e, d) { d.updateAttribute('name', null, function(e) { should.exist(e); e.should.be.instanceOf(Error); e.should.be.instanceOf(ValidationError); d.updateAttribute('name', 'Vasiliy', function(e) { should.not.exist(e); done(); }); }); }); }); it('should work on create with options', function(done) { delete User.validations; User.validatesPresenceOf('name'); User.create(function(e, u) { should.exist(e); User.create({name: 'Valid'}, {options: 'options'}, function(e, d) { should.not.exist(e); done(); }); }); }); it('should work on create without options', function(done) { delete User.validations; User.validatesPresenceOf('name'); User.create(function(e, u) { should.exist(e); User.create({name: 'Valid'}, function(e, d) { should.not.exist(e); done(); }); }); }); }); describe('presence', function() { it('should validate presence', function() { User.validatesPresenceOf('name', 'email'); var validations = User.validations; validations.name.should.eql([{validation: 'presence', options: {}}]); validations.email.should.eql([{validation: 'presence', options: {}}]); var u = new User; u.isValid().should.not.be.true; u.name = 1; u.isValid().should.not.be.true; u.email = 2; u.isValid().should.be.true; }); it('should reject NaN value as a number', function() { User.validatesPresenceOf('age'); var u = new User(); u.isValid().should.be.false; u.age = NaN; u.isValid().should.be.false; u.age = 1; u.isValid().should.be.true; }); it('should allow "NaN" value as a string', function() { User.validatesPresenceOf('name'); var u = new User(); u.isValid().should.be.false; u.name = 'NaN'; u.isValid().should.be.true; }); it('should skip validation by property (if/unless)', function() { User.validatesPresenceOf('domain', {unless: 'createdByScript'}); var user = new User(getValidAttributes()); user.isValid().should.be.true; user.createdByScript = false; user.isValid().should.be.false; user.errors.domain.should.eql(['can\'t be blank']); user.domain = 'domain'; user.isValid().should.be.true; }); }); describe('absence', function() { it('should validate absence', function() { User.validatesAbsenceOf('reserved', {if: 'locked'}); var u = new User({reserved: 'foo', locked: true}); u.isValid().should.not.be.true; u.reserved = null; u.isValid().should.be.true; var u = new User({reserved: 'foo', locked: false}); u.isValid().should.be.true; }); }); describe('uniqueness', function() { it('should validate uniqueness', function(done) { User.validatesUniquenessOf('email'); var u = new User({email: 'hey'}); Boolean(u.isValid(function(valid) { valid.should.be.true; u.save(function() { var u2 = new User({email: 'hey'}); u2.isValid(function(valid) { valid.should.be.false; done(); }); }); })).should.be.false; }); it('should handle same object modification', function(done) { User.validatesUniquenessOf('email'); var u = new User({email: 'hey'}); Boolean(u.isValid(function(valid) { valid.should.be.true; u.save(function() { u.name = 'Goghi'; u.isValid(function(valid) { valid.should.be.true; u.save(done); }); }); // async validations always falsy when called as sync })).should.not.be.ok; }); it('should support multi-key constraint', function(done) { var EMAIL = 'user@xample.com'; var SiteUser = db.define('SiteUser', { siteId: String, email: String, }); SiteUser.validatesUniquenessOf('email', {scopedTo: ['siteId']}); async.waterfall([ function automigrate(next) { db.automigrate(next); }, function createSite1User(next) { SiteUser.create( {siteId: 1, email: EMAIL}, next); }, function createSite2User(user1, next) { SiteUser.create( {siteId: 2, email: EMAIL}, next); }, function validateDuplicateUser(user2, next) { var user3 = new SiteUser({siteId: 1, email: EMAIL}); user3.isValid(function(valid) { valid.should.be.false; next(); }); }, ], function(err) { if (err && err.name == 'ValidationError') { console.error('ValidationError:', err.details.messages); } done(err); }); }); it('should skip blank values', function(done) { User.validatesUniquenessOf('email'); var u = new User({email: ' '}); Boolean(u.isValid(function(valid) { valid.should.be.true; u.save(function() { var u2 = new User({email: null}); u2.isValid(function(valid) { valid.should.be.true; done(); }); }); })).should.be.false; }); it('should work with if/unless', function(done) { User.validatesUniquenessOf('email', { if: function() { return true; }, unless: function() { return false; }, }); var u = new User({email: 'hello'}); Boolean(u.isValid(function(valid) { valid.should.be.true; done(); })).should.be.false; }); it('should work with id property on create', function(done) { Entry.create({id: 'entry'}, function(err, entry) { var e = new Entry({id: 'entry'}); Boolean(e.isValid(function(valid) { valid.should.be.false; done(); })).should.be.false; }); }); it('should work with id property after create', function(done) { Entry.findById('entry', function(err, e) { Boolean(e.isValid(function(valid) { valid.should.be.true; done(); })).should.be.false; }); }); }); describe('format', function() { it('should validate format'); it('should overwrite default blank message with custom format message'); it('should skip missing values when allowing null', function() { User.validatesFormatOf('email', {with: /^\S+@\S+\.\S+$/, allowNull: true}); var u = new User({}); u.isValid().should.be.true; }); it('should skip null values when allowing null', function() { User.validatesFormatOf('email', {with: /^\S+@\S+\.\S+$/, allowNull: true}); var u = new User({email: null}); u.isValid().should.be.true; }); it('should not skip missing values', function() { User.validatesFormatOf('email', {with: /^\S+@\S+\.\S+$/}); var u = new User({}); u.isValid().should.be.false; }); it('should not skip null values', function() { User.validatesFormatOf('email', {with: /^\S+@\S+\.\S+$/}); var u = new User({email: null}); u.isValid().should.be.false; }); }); describe('numericality', function() { it('passes when given numeric values', function() { User.validatesNumericalityOf('age'); var user = new User({age: 10}); user.isValid().should.be.true(); }); it('fails when given non-numeric values', function() { User.validatesNumericalityOf('age'); var user = new User({age: 'notanumber'}); user.isValid().should.be.false(); user.errors.should.eql({age: ['is not a number']}); }); it('fails when given undefined values', function() { User.validatesNumericalityOf('age'); var user = new User({}); user.isValid().should.be.false(); user.errors.should.eql({age: ['is blank']}); }); it('skips undefined values when allowBlank option is true', function() { User.validatesNumericalityOf('age', {allowBlank: true}); var user = new User({}); user.isValid().should.be.true(); }); it('fails when given non-numeric values when allowBlank option is true', function() { User.validatesNumericalityOf('age', {allowBlank: true}); var user = new User({age: 'test'}); user.isValid().should.be.false(); user.errors.should.eql({age: ['is not a number']}); }); it('fails when given null values', function() { User.validatesNumericalityOf('age'); var user = new User({age: null}); user.isValid().should.be.false(); user.errors.should.eql({age: ['is null']}); }); it('passes when given null values when allowNull option is true', function() { User.validatesNumericalityOf('age', {allowNull: true}); var user = new User({age: null}); user.isValid().should.be.true(); }); it('passes when given float values', function() { User.validatesNumericalityOf('age'); var user = new User({age: 13.37}); user.isValid().should.be.true(); }); it('fails when given non-integer values when int option is true', function() { User.validatesNumericalityOf('age', {int: true}); var user = new User({age: 13.37}); user.isValid().should.be.false(); user.errors.should.eql({age: ['is not an integer']}); }); }); describe('inclusion', function() { it('should validate inclusion'); }); describe('exclusion', function() { it('should validate exclusion'); }); describe('length', function() { it('should validate length'); }); describe('custom', function() { it('should validate using custom sync validation', function() { User.validate('email', function(err) { if (this.email === 'hello') err(); }, {code: 'invalid-email'}); var u = new User({email: 'hello'}); Boolean(u.isValid()).should.be.false; u.errors.codes.should.eql({email: ['invalid-email']}); }); it('should validate and return detailed error messages', function() { User.validate('global', function(err) { if (this.email === 'hello' || this.email === 'hey') { this.errors.add('email', 'Cannot be `' + this.email + '`', 'invalid-email'); err(false); // false: prevent global error message } }); var u = new User({email: 'hello'}); Boolean(u.isValid()).should.be.false; u.errors.should.eql({email: ['Cannot be `hello`']}); u.errors.codes.should.eql({email: ['invalid-email']}); }); it('should validate using custom async validation', function(done) { User.validateAsync('email', function(err, next) { process.nextTick(next); }, { if: function() { return true; }, unless: function() { return false; }, }); var u = new User({email: 'hello'}); Boolean(u.isValid(function(valid) { valid.should.be.true; done(); })).should.be.false; }); }); describe('invalid value formatting', function() { var origMaxLen; beforeEach(function saveAndSetMaxLen() { origMaxLen = ValidationError.maxPropertyStringLength; }); afterEach(function restoreMaxLen() { ValidationError.maxPropertyStringLength = origMaxLen; }); it('should truncate long strings', function() { ValidationError.maxPropertyStringLength = 9; var err = givenValidationError('prop', '1234567890abc', 'is invalid'); getErrorDetails(err) .should.equal('`prop` is invalid (value: "12...abc").'); }); it('should truncate long objects', function() { ValidationError.maxPropertyStringLength = 12; var err = givenValidationError('prop', {foo: 'bar'}, 'is invalid'); getErrorDetails(err) .should.equal('`prop` is invalid (value: { foo:... }).'); }); it('should truncate long arrays', function() { ValidationError.maxPropertyStringLength = 12; var err = givenValidationError('prop', [{a: 1, b: 2}], 'is invalid'); getErrorDetails(err) .should.equal('`prop` is invalid (value: [ { a...} ]).'); }); it('should print only top-level object properties', function() { var err = givenValidationError('prop', {a: {b: 'c'}}, 'is invalid'); getErrorDetails(err) .should.equal('`prop` is invalid (value: { a: [Object] }).'); }); it('should print only top-level props of objects in array', function() { var err = givenValidationError('prop', [{a: {b: 'c'}}], 'is invalid'); getErrorDetails(err) .should.equal('`prop` is invalid (value: [ { a: [Object] } ]).'); }); it('should exclude colors from Model values', function() { var obj = new User(); obj.email = 'test@example.com'; var err = givenValidationError('user', obj, 'is invalid'); getErrorDetails(err).should.equal( '`user` is invalid (value: { email: \'test@example.com\' }).'); }); function givenValidationError(propertyName, propertyValue, errorMessage) { var jsonVal = {}; jsonVal[propertyName] = propertyValue; var errorVal = {}; errorVal[propertyName] = [errorMessage]; var obj = { errors: errorVal, toJSON: function() { return jsonVal; }, }; return new ValidationError(obj); } function getErrorDetails(err) { return err.message.replace(/^.*Details: /, ''); } }); });