'use strict'; const { expect } = require('chai'); const sinon = require('sinon'); const { DataTypes, Op, Sequelize } = require('@sequelize/core'); const { rand, sequelize } = require('../../support'); const dialect = sequelize.dialect; describe('InstanceValidator', () => { describe('validations', () => { const checks = { is: { spec: { args: ['[a-z]', 'i'] }, fail: '0', pass: 'a', }, not: { spec: { args: ['[a-z]', 'i'] }, fail: 'a', pass: '0', }, isEmail: { fail: 'a', pass: 'abc@abc.com', }, isUrl: { fail: 'abc', pass: 'http://abc.com', }, isIP: { fail: 'abc', pass: '129.89.23.1', }, isIPv4: { fail: 'abc', pass: '129.89.23.1', }, isIPv6: { fail: '1111:2222:3333::5555:', pass: 'fe80:0000:0000:0000:0204:61ff:fe9d:f156', }, isAlpha: { stringOrBoolean: true, spec: { args: 'en-GB' }, fail: '012', pass: 'abc', }, isAlphanumeric: { stringOrBoolean: true, spec: { args: 'en-GB' }, fail: '_abc019', pass: 'abc019', }, isNumeric: { fail: 'abc', pass: '019', }, isInt: { fail: '9.2', pass: '-9', }, isLowercase: { fail: 'AB', pass: 'ab', }, isUppercase: { fail: 'ab', pass: 'AB', }, isDecimal: { fail: 'a', pass: '0.2', }, isFloat: { fail: 'a', pass: '9.2', }, isNull: { fail: '0', pass: null, }, notEmpty: { fail: ' ', pass: 'a', }, equals: { spec: { args: 'bla bla bla' }, fail: 'bla', pass: 'bla bla bla', }, contains: { spec: { args: 'bla' }, fail: 'la', pass: '0bla23', }, notContains: { spec: { args: 'bla' }, fail: '0bla23', pass: 'la', }, regex: { spec: { args: ['[a-z]', 'i'] }, fail: '0', pass: 'a', }, notRegex: { spec: { args: ['[a-z]', 'i'] }, fail: 'a', pass: '0', }, len: { spec: { args: [2, 4] }, fail: ['1', '12345'], pass: ['12', '123', '1234'], raw: true, }, len$: { spec: [2, 4], fail: ['1', '12345'], pass: ['12', '123', '1234'], raw: true, }, isUUID: { spec: { args: 4 }, fail: 'f47ac10b-58cc-3372-a567-0e02b2c3d479', pass: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', }, isDate: { fail: 'not a date', pass: '2011-02-04', }, isAfter: { spec: { args: '2011-11-05' }, fail: '2011-11-04', pass: '2011-11-06', }, isBefore: { spec: { args: '2011-11-05' }, fail: '2011-11-06', pass: '2011-11-04', }, isIn: { spec: { args: 'abcdefghijk' }, fail: 'ghik', pass: 'ghij', }, notIn: { spec: { args: 'abcdefghijk' }, fail: 'ghij', pass: 'ghik', }, max: { spec: { args: 23 }, fail: '24', pass: '23', }, max$: { spec: 23, fail: '24', pass: '23', }, min: { spec: { args: 23 }, fail: '22', pass: '23', }, min$: { spec: 23, fail: '22', pass: '23', }, isCreditCard: { fail: '401288888888188f', pass: '4012888888881881', }, }; const applyFailTest = function applyFailTest(validatorDetails, i, validator) { const failingValue = validatorDetails.fail[i]; it(`correctly specifies an instance as invalid using a value of "${failingValue}" for the validation "${validator}"`, async function () { const validations = {}; const message = `${validator}(${failingValue})`; validations[validator] = validatorDetails.spec || {}; validations[validator].msg = message; const UserFail = this.sequelize.define(`User${rand()}`, { name: { type: DataTypes.STRING, validate: validations, }, }); const failingUser = UserFail.build({ name: failingValue }); const _errors = await expect(failingUser.validate()).to.be.rejected; expect(_errors.get('name')[0].message).to.equal(message); expect(_errors.get('name')[0].value).to.equal(failingValue); }); }; const applyPassTest = function applyPassTest(validatorDetails, j, validator, type) { const succeedingValue = validatorDetails.pass[j]; it(`correctly specifies an instance as valid using a value of "${succeedingValue}" for the validation "${validator}"`, async function () { const validations = {}; const message = `${validator}(${succeedingValue})`; validations[validator] = validatorDetails.spec || {}; switch (type) { case 'msg': { validations[validator].msg = message; break; } case 'args': { validations[validator].args = validations[validator].args || true; validations[validator].msg = message; break; } case 'true': { validations[validator] = true; break; } // No default } const UserSuccess = this.sequelize.define(`User${rand()}`, { name: { type: DataTypes.STRING, validate: validations, }, }); const successfulUser = UserSuccess.build({ name: succeedingValue }); await expect(successfulUser.validate()).not.to.be.rejected; }); }; for (let validator in checks) { if (checks.hasOwnProperty(validator)) { validator = validator.replace(/\$$/, ''); const validatorDetails = checks[validator]; if (!validatorDetails.raw) { validatorDetails.fail = Array.isArray(validatorDetails.fail) ? validatorDetails.fail : [validatorDetails.fail]; validatorDetails.pass = Array.isArray(validatorDetails.pass) ? validatorDetails.pass : [validatorDetails.pass]; } for (let i = 0; i < validatorDetails.fail.length; i++) { applyFailTest(validatorDetails, i, validator); } for (let i = 0; i < validatorDetails.pass.length; i++) { applyPassTest(validatorDetails, i, validator); applyPassTest(validatorDetails, i, validator, 'msg'); applyPassTest(validatorDetails, i, validator, 'args'); if (validatorDetails.stringOrBoolean || validatorDetails.spec === undefined) { applyPassTest(validatorDetails, i, validator, 'true'); } } } } }); if (dialect.supports.dataTypes.DECIMAL) { describe('DECIMAL validator', () => { let User; before(function () { User = sequelize.define('user', { decimal: DataTypes.DECIMAL(10, 2), }); this.stub = sinon.stub(sequelize, 'queryRaw').callsFake(async () => [User.build({}), 1]); }); after(function () { this.stub.restore(); }); it('should allow decimal as a string', async () => { await expect( User.create({ decimal: '12.6', }), ).not.to.be.rejected; }); it('should allow decimal big numbers as a string', async () => { await expect( User.create({ decimal: '2321312301230128391820831289123012', }), ).not.to.be.rejected; }); it('should allow decimal as scientific notation', async () => { await Promise.all([ expect( User.create({ decimal: '2321312301230128391820e219', }), ).not.to.be.rejected, expect( User.create({ decimal: '2321312301230128391820e+219', }), ).not.to.be.rejected, ]); }); }); } describe('datatype validations', () => { let User; before(function () { User = sequelize.define('user', { integer: DataTypes.INTEGER, name: DataTypes.STRING, awesome: DataTypes.BOOLEAN, uid: DataTypes.UUID, date: DataTypes.DATE, }); this.stub = sinon.stub(sequelize, 'queryRaw').callsFake(async () => [User.build({}), 1]); }); after(function () { this.stub.restore(); }); describe('should not throw', () => { describe('create', () => { it('should allow number as a string', async () => { await expect( User.create({ integer: '12', }), ).not.to.be.rejected; }); it('should allow dates as a string', async () => { await expect( User.findOne({ where: { date: '2000-12-16', }, }), ).not.to.be.rejected; }); }); describe('findAll', () => { it('should allow $in', async () => { if (!dialect.supports.dataTypes.ARRAY) { return; } await expect( User.findAll({ where: { name: { [Op.like]: { [Op.any]: ['foo%', 'bar%'], }, }, }, }), ).not.to.be.rejected; }); it('should allow $like for uuid if cast', async () => { await expect( User.findAll({ where: { 'uid::text': { [Op.like]: '12345678%', }, }, }), ).not.to.be.rejected; }); }); }); describe('should throw validationError', () => { describe('create', () => { it('should throw when passing string', async () => { const error = await expect( User.create({ integer: 'jan', }), ).to.be.rejectedWith(Sequelize.ValidationError, `'jan' is not a valid integer`); expect(error) .to.have.property('errors') .that.is.an('array') .with.lengthOf(1) .and.with.property(0) .that.is.an.instanceOf(Sequelize.ValidationErrorItem) .and.include({ type: 'Validation error', path: 'integer', value: 'jan', validatorKey: 'INTEGER validator', }); }); it('should throw when passing decimal', async () => { await expect( User.create({ integer: 4.5, }), ) .to.be.rejectedWith(Sequelize.ValidationError) .which.eventually.have.property('errors') .that.is.an('array') .with.lengthOf(1) .and.with.property(0) .that.is.an.instanceOf(Sequelize.ValidationErrorItem) .and.include({ type: 'Validation error', path: 'integer', value: 4.5, validatorKey: 'INTEGER validator', }); }); }); describe('update', () => { it('should throw when passing string', async () => { await expect(User.update({ integer: 'jan' }, { where: {} })) .to.be.rejectedWith(Sequelize.ValidationError) .which.eventually.have.property('errors') .that.is.an('array') .with.lengthOf(1) .and.with.property(0) .that.is.an.instanceOf(Sequelize.ValidationErrorItem) .and.include({ type: 'Validation error', path: 'integer', value: 'jan', validatorKey: 'INTEGER validator', }); }); it('should throw when passing decimal', async () => { await expect( User.update( { integer: 4.5, }, { where: {} }, ), ) .to.be.rejectedWith(Sequelize.ValidationError) .which.eventually.have.property('errors') .that.is.an('array') .with.lengthOf(1) .and.with.property(0) .that.is.an.instanceOf(Sequelize.ValidationErrorItem) .and.include({ type: 'Validation error', path: 'integer', value: 4.5, validatorKey: 'INTEGER validator', }); }); }); }); }); describe('custom validation functions', () => { let User; before(function () { User = sequelize.define( 'user', { integer: { type: DataTypes.INTEGER, validate: { customFn(val, next) { if (val < 0) { next('integer must be greater or equal zero'); } else { next(); } }, }, }, name: DataTypes.STRING, }, { validate: { customFn() { if (this.get('name') === 'error') { throw new Error('Error from model validation promise'); } }, }, }, ); this.stub = sinon.stub(sequelize, 'queryRaw').resolves([User.build(), 1]); }); after(function () { this.stub.restore(); }); describe('should not throw', () => { describe('create', () => { it('custom validation functions are successful', async () => { await expect( User.create({ integer: 1, name: 'noerror', }), ).not.to.be.rejected; }); }); describe('update', () => { it('custom validation functions are successful', async () => { await expect( User.update( { integer: 1, name: 'noerror', }, { where: {} }, ), ).not.to.be.rejected; }); }); }); describe('should throw validationerror', () => { describe('create', () => { it('custom attribute validation function fails', async () => { await expect( User.create({ integer: -1, }), ).to.be.rejectedWith(Sequelize.ValidationError); }); it('custom model validation function fails', async () => { await expect( User.create({ name: 'error', }), ).to.be.rejectedWith(Sequelize.ValidationError); }); }); describe('update', () => { it('custom attribute validation function fails', async () => { await expect( User.update( { integer: -1, }, { where: {} }, ), ).to.be.rejectedWith(Sequelize.ValidationError); }); it('when custom model validation function fails', async () => { await expect( User.update( { name: 'error', }, { where: {} }, ), ).to.be.rejectedWith(Sequelize.ValidationError); }); }); }); }); describe('custom validation functions returning promises', () => { let User; before(function () { User = sequelize.define( 'user', { name: DataTypes.STRING, }, { validate: { async customFn() { if (this.get('name') === 'error') { throw new Error('Error from model validation promise'); } }, }, }, ); this.stub = sinon.stub(sequelize, 'queryRaw').resolves([User.build(), 1]); }); after(function () { this.stub.restore(); }); describe('should not throw', () => { describe('create', () => { it('custom model validation functions are successful', async () => { await expect( User.create({ name: 'noerror', }), ).not.to.be.rejected; }); }); describe('update', () => { it('custom model validation functions are successful', async () => { await expect( User.update( { name: 'noerror', }, { where: {} }, ), ).not.to.be.rejected; }); }); }); describe('should throw validationerror', () => { describe('create', () => { it('custom model validation function fails', async () => { await expect( User.create({ name: 'error', }), ).to.be.rejectedWith(Sequelize.ValidationError); }); }); describe('update', () => { it('when custom model validation function fails', async () => { await expect( User.update( { name: 'error', }, { where: {} }, ), ).to.be.rejectedWith(Sequelize.ValidationError); }); }); }); }); describe('custom validation functions and null values', () => { before(function () { this.customValidator = sinon.fake(function (value) { if (value === null && this.integer !== 10) { throw new Error("name can't be null unless integer is 10"); } }); }); describe('with allowNull set to true', () => { before(function () { this.User = sequelize.define('user', { integer: DataTypes.INTEGER, name: { type: DataTypes.STRING, allowNull: true, validate: { customValidator: this.customValidator, }, }, }); this.stub = sinon.stub(sequelize, 'queryRaw').resolves([this.User.build(), 1]); }); after(function () { this.stub.restore(); }); describe('should call validator and not throw', () => { beforeEach(function () { this.customValidator.resetHistory(); }); it('on create', async function () { await expect( this.User.create({ integer: 10, name: null, }), ).not.to.be.rejected; await expect(this.customValidator).to.have.been.calledOnce; }); it('on update', async function () { await expect( this.User.update( { integer: 10, name: null, }, { where: {} }, ), ).not.to.be.rejected; await expect(this.customValidator).to.have.been.calledOnce; }); }); describe('should call validator and throw ValidationError', () => { beforeEach(function () { this.customValidator.resetHistory(); }); it('on create', async function () { await expect( this.User.create({ integer: 11, name: null, }), ).to.be.rejectedWith(Sequelize.ValidationError); await expect(this.customValidator).to.have.been.calledOnce; }); it('on update', async function () { await expect( this.User.update( { integer: 11, name: null, }, { where: {} }, ), ).to.be.rejectedWith(Sequelize.ValidationError); await expect(this.customValidator).to.have.been.calledOnce; }); }); }); describe('with allowNull set to false', () => { before(function () { this.User = sequelize.define('user', { integer: DataTypes.INTEGER, name: { type: DataTypes.STRING, allowNull: false, validate: { customValidator: this.customValidator, }, }, }); this.stub = sinon.stub(sequelize, 'queryRaw').resolves([this.User.build(), 1]); }); after(function () { this.stub.restore(); }); describe('should not call validator and throw ValidationError', () => { beforeEach(function () { this.customValidator.resetHistory(); }); it('on create', async function () { await expect( this.User.create({ integer: 99, name: null, }), ).to.be.rejectedWith(Sequelize.ValidationError); await expect(this.customValidator).to.have.not.been.called; }); it('on update', async function () { await expect( this.User.update( { integer: 99, name: null, }, { where: {} }, ), ).to.be.rejectedWith(Sequelize.ValidationError); await expect(this.customValidator).to.have.not.been.called; }); }); describe('should call validator and not throw', () => { beforeEach(function () { this.customValidator.resetHistory(); }); it('on create', async function () { await expect( this.User.create({ integer: 99, name: 'foo', }), ).not.to.be.rejected; await expect(this.customValidator).to.have.been.calledOnce; }); it('on update', async function () { await expect( this.User.update( { integer: 99, name: 'foo', }, { where: {} }, ), ).not.to.be.rejected; await expect(this.customValidator).to.have.been.calledOnce; }); }); }); }); });