VValeri Karpovfix tests from #16480
b4cb6818创建于 7 天前历史提交
'use strict';

/**
 * Module dependencies.
 */

const start = require('./common');

const assert = require('assert');
const random = require('./util').random;
const { v4: uuidv4 } = require('uuid');

const mongoose = start.mongoose;
const Schema = mongoose.Schema;
const ValidatorError = mongoose.Error.ValidatorError;
const SchemaTypes = Schema.Types;
const ObjectId = SchemaTypes.ObjectId;
const Mixed = SchemaTypes.Mixed;

describe('schema', function() {
  describe('validation', function() {
    it('invalid arguments are rejected (1044)', function(done) {
      assert.throws(function() {
        new Schema({
          simple: { type: String, validate: 'nope' }
        });
      }, /Invalid validator/);

      assert.throws(function() {
        new Schema({
          simple: { type: String, validate: ['nope'] }
        });
      }, /Invalid validator/);

      assert.throws(function() {
        new Schema({
          simple: { type: String, validate: { nope: 1, msg: 'nope' } }
        });
      }, /Invalid validator/);

      assert.throws(function() {
        new Schema({
          simple: { type: String, validate: [{ nope: 1, msg: 'nope' }, 'nope'] }
        });
      }, /Invalid validator/);

      done();
    });

    it('string enum', async function() {
      const Test = new Schema({
        complex: { type: String, enum: ['a', 'b', undefined, 'c', null] },
        state: { type: String }
      });

      assert.ok(Test.path('complex') instanceof SchemaTypes.String);
      assert.deepEqual(Test.path('complex').enumValues, ['a', 'b', 'c', null]);
      assert.equal(Test.path('complex').validators.length, 1);

      Test.path('complex').enum('d', 'e');

      assert.deepEqual(Test.path('complex').enumValues, ['a', 'b', 'c', null, 'd', 'e']);

      // with SchemaTypes validate method
      Test.path('state').enum({
        values: 'opening open closing closed'.split(' '),
        message: 'enum validator failed for path `{PATH}`: test'
      });

      assert.equal(Test.path('state').validators.length, 1);
      assert.deepEqual(Test.path('state').enumValues, ['opening', 'open', 'closing', 'closed']);

      await assert.rejects(Test.path('complex').doValidate('x'), ValidatorError);

      // allow unsetting enums
      await Test.path('complex').doValidate(undefined);

      await Test.path('complex').doValidate(null);

      await assert.rejects(
        Test.path('complex').doValidate('da'),
        ValidatorError
      );

      await assert.rejects(
        Test.path('state').doValidate('x'),
        err => {
          assert.ok(err instanceof ValidatorError);
          assert.equal(err.message,
            'enum validator failed for path `state`: test');
          return true;
        }
      );

      await Test.path('state').doValidate('opening');

      await Test.path('state').doValidate('open');
    });

    it('number enum', async function() {
      const Test = new Schema({
        status: { type: Number, enum: [1, 2, 3, null] },
        priority: { type: Number }
      });

      assert.ok(Test.path('status') instanceof SchemaTypes.Number);
      assert.deepEqual(Test.path('status').enumValues, [1, 2, 3, null]);
      assert.equal(Test.path('status').validators.length, 1);

      Test.path('status').enum(4, 5);

      assert.deepEqual(Test.path('status').enumValues, [1, 2, 3, null, 4, 5]);

      // with SchemaTypes validate method
      Test.path('priority').enum({
        values: [10, 20, 30],
        message: 'enum validator failed for path `{PATH}`: test'
      });

      assert.equal(Test.path('priority').validators.length, 1);
      assert.deepEqual(Test.path('priority').enumValues, [10, 20, 30]);

      await assert.rejects(Test.path('status').doValidate(6), ValidatorError);

      // allow unsetting enums
      await Test.path('status').doValidate(undefined);

      await Test.path('status').doValidate(null);

      await assert.rejects(
        Test.path('status').doValidate(99),
        ValidatorError
      );

      await assert.rejects(
        Test.path('priority').doValidate(40),
        err => {
          assert.ok(err instanceof ValidatorError);
          assert.equal(err.message,
            'enum validator failed for path `priority`: test');
          return true;
        }
      );

      await Test.path('status').doValidate(1);

      await Test.path('status').doValidate(2);
    });

    it('string regexp', async function() {
      const Test = new Schema({
        simple: { type: String, match: /[a-z]/ }
      });

      assert.equal(Test.path('simple').validators.length, 1);

      await Test.path('simple').doValidate('az');

      Test.path('simple').match(/[0-9]/);
      assert.equal(Test.path('simple').validators.length, 2);

      await assert.rejects(Test.path('simple').doValidate('12'), ValidatorError);

      await Test.path('simple').doValidate('a12');

      await Test.path('simple').doValidate('');
      await Test.path('simple').doValidate(null);
      await Test.path('simple').doValidate(undefined);
      Test.path('simple').validators = [];
      Test.path('simple').match(/[1-9]/);
      await assert.rejects(Test.path('simple').doValidate(0), ValidatorError);

      Test.path('simple').match(null);
      await assert.rejects(Test.path('simple').doValidate(0), ValidatorError);
    });

    describe('non-required fields', function() {
      describe('are validated correctly', function() {
        let db;
        let Person;

        before(function() {
          db = start();
          const PersonSchema = new Schema({
            name: { type: String },
            num_cars: { type: Number, min: 20 }
          });
          Person = db.model('person-schema-validation-test', PersonSchema);
        });

        after(async function() {
          await db.close();
        });

        it('and can be set to "undefined" (gh-1594)', async function() {
          const p = new Person({ name: 'Daniel' });
          p.num_cars = 25;

          await p.save();
          assert.equal(p.num_cars, 25);
          p.num_cars = undefined;

          await p.save();
          assert.equal(p.num_cars, undefined);
          p.num_cars = 5;

          try {
            await p.save();
            assert.ok(false);
          } catch (err) {
            assert.ok(err);
          }
        });
      });

      it('number min and max', async function() {
        const Tobi = new Schema({
          friends: { type: Number, max: 15, min: 5 }
        });

        assert.equal(Tobi.path('friends').validators.length, 2);

        await Tobi.path('friends').doValidate(10);

        await assert.rejects(Tobi.path('friends').doValidate(100), (err) => {
          assert.ok(err instanceof ValidatorError);
          assert.equal(err.path, 'friends');
          assert.equal(err.kind, 'max');
          assert.equal(err.value, 100);
          return true;
        });

        await assert.rejects(Tobi.path('friends').doValidate(1), (err) => {
          assert.ok(err instanceof ValidatorError);
          assert.equal(err.path, 'friends');
          assert.equal(err.kind, 'min');
          return true;
        });

        // null is allowed
        await Tobi.path('friends').doValidate(null);

        Tobi.path('friends').min();
        Tobi.path('friends').max();

        assert.equal(Tobi.path('friends').validators.length, 0);
      });
    });

    describe('required', function() {
      it('string required', async function() {
        const Test = new Schema({
          simple: String
        });

        Test.path('simple').required(true);
        assert.equal(Test.path('simple').validators.length, 1);

        await assert.rejects(Test.path('simple').doValidate(null), ValidatorError);

        await assert.rejects(Test.path('simple').doValidate(undefined), ValidatorError);

        await assert.rejects(Test.path('simple').doValidate(''), ValidatorError);

        await Test.path('simple').doValidate('woot');
      });

      it('string conditional required', async function() {
        const Test = new Schema({
          simple: String
        });

        let required = true;
        const isRequired = function() {
          return required;
        };

        Test.path('simple').required(isRequired);
        assert.equal(Test.path('simple').validators.length, 1);

        await assert.rejects(
          Test.path('simple').doValidate(null),
          ValidatorError
        );

        await assert.rejects(
          Test.path('simple').doValidate(undefined),
          ValidatorError
        );

        await assert.rejects(
          Test.path('simple').doValidate(''),
          ValidatorError
        );

        await Test.path('simple').doValidate('woot');

        required = false;

        await Test.path('simple').doValidate(null);

        await Test.path('simple').doValidate(undefined);

        await Test.path('simple').doValidate('');

        await Test.path('simple').doValidate('woot');
      });

      it('number required', async function() {
        const Edwald = new Schema({
          friends: { type: Number, required: true }
        });

        await assert.rejects(
          Edwald.path('friends').doValidate(null),
          ValidatorError
        );

        await assert.rejects(
          Edwald.path('friends').doValidate(undefined),
          ValidatorError
        );

        await Edwald.path('friends').doValidate(0);
      });

      it('date required', async function() {
        const Loki = new Schema({
          birth_date: { type: Date, required: true }
        });

        await assert.rejects(
          Loki.path('birth_date').doValidate(null),
          ValidatorError
        );

        await assert.rejects(
          Loki.path('birth_date').doValidate(undefined),
          ValidatorError
        );

        await Loki.path('birth_date').doValidate(new Date());
      });

      it('date not empty string (gh-3132)', async function() {
        const HappyBirthday = new Schema({
          date: { type: Date, required: true }
        });

        await assert.rejects(
          HappyBirthday.path('date').doValidate(''),
          ValidatorError
        );
      });

      it('objectid required', async function() {
        const Loki = new Schema({
          owner: { type: ObjectId, required: true }
        });

        await assert.rejects(
          Loki.path('owner').doValidate(null),
          ValidatorError
        );

        await assert.rejects(
          Loki.path('owner').doValidate(undefined),
          ValidatorError
        );
      });

      it('array required', async function() {
        const Loki = new Schema({
          likes: { type: Array, required: true }
        });

        await assert.rejects(
          Loki.path('likes').doValidate(null),
          ValidatorError
        );

        await assert.rejects(
          Loki.path('likes').doValidate(undefined),
          ValidatorError
        );
      });

      it('array required custom required', async function() {
        const requiredOrig = mongoose.Schema.Types.Array.checkRequired();
        mongoose.Schema.Types.Array.checkRequired(v => Array.isArray(v) && v.length);
        try {
          const Loki = new Schema({
            likes: { type: Array, required: true }
          });

          await assert.rejects(
            Loki.path('likes').doValidate([]),
            ValidatorError
          );

          await Loki.path('likes').doValidate(['cake']);
        } finally {
          mongoose.Schema.Types.Array.checkRequired(requiredOrig);
        }
      });

      it('boolean required', async function() {
        const Animal = new Schema({
          isFerret: { type: Boolean, required: true }
        });

        await assert.rejects(Animal.path('isFerret').doValidate(null), ValidatorError);
        await assert.rejects(Animal.path('isFerret').doValidate(undefined), ValidatorError);
        await Animal.path('isFerret').doValidate(true);
        await Animal.path('isFerret').doValidate(false);
      });

      it('mixed required', async function() {
        const Animal = new Schema({
          characteristics: { type: Mixed, required: true }
        });

        await assert.rejects(
          Animal.path('characteristics').doValidate(null),
          ValidatorError
        );

        await assert.rejects(
          Animal.path('characteristics').doValidate(undefined),
          ValidatorError
        );

        await Animal.path('characteristics').doValidate({
          aggresive: true
        });

        await Animal.path('characteristics').doValidate('none available');
      });
    });

    describe('async', function() {
      it('works', async function() {
        function validator(value) {
          return new Promise(function(resolve) {
            setTimeout(function() {
              resolve(value === true);
            }, 5);
          });
        }

        const Animal = new Schema({
          ferret: { type: Boolean, validate: validator }
        });

        await Animal.path('ferret').doValidate(true);

        await assert.rejects(
          Animal.path('ferret').doValidate(false),
          ValidatorError
        );
      });

      it('scope', async function() {
        let called = false;

        function validator() {
          return new Promise(resolve => {
            assert.equal(this.a, 'b');
            setTimeout(function() {
              called = true;
              resolve(true);
            }, 5);
          });
        }

        const Animal = new Schema({
          ferret: {
            type: Boolean,
            validate: validator
          }
        });

        await Animal.path('ferret').doValidate(true, { a: 'b' });
        assert.equal(called, true);
      });

      it('doValidateSync should ignore async function and script waiting for promises (gh-4885)', function(done) {
        let asyncCalled = false;
        let normalCalled = false;
        let promiseCalled = false;
        let promiseCompleted = false;

        const schema = new Schema({
          prop: {
            type: Boolean,
            validate: [
              {
                validator: async() => { asyncCalled = true; }
              },
              {
                validator: () => { normalCalled = true; }
              },
              {
                validator: () => {
                  promiseCalled = true;
                  return new Promise((res) => setTimeout(() => {
                    promiseCompleted = true;
                    return res();
                  }, 1000));
                }
              }
            ]
          }
        });

        schema.path('prop').doValidateSync(true);

        assert.strictEqual(asyncCalled, false);
        assert.strictEqual(normalCalled, true);
        assert.strictEqual(promiseCalled, true);
        assert.strictEqual(promiseCompleted, false);
        done();
      });
    });

    describe('messages', function() {
      describe('are customizable', function() {
        it('within schema definitions', async function() {
          const schema = new Schema({
            name: { type: String, enum: ['one', 'two'] },
            myenum: { type: String, enum: { values: ['x'], message: 'enum validator failed for path: {PATH} with {VALUE}' } },
            requiredString1: { type: String, required: true },
            requiredString2: { type: String, required: 'oops, {PATH} is missing. {TYPE}' },
            matchString0: { type: String, match: /bryancranston/ },
            matchString1: { type: String, match: [/bryancranston/, 'invalid string for {PATH} with value: {VALUE}'] },
            numMin0: { type: Number, min: 10 },
            numMin1: { type: Number, min: [10, 'hey, {PATH} is too small'] },
            numMax0: { type: Number, max: 20 },
            numMax1: { type: Number, max: [20, 'hey, {PATH} ({VALUE}) is greater than {MAX}'] }
          });

          const A = mongoose.model('schema-validation-messages-' + random(), schema);

          const a = new A();
          const err = await a.validate().then(() => null, err => err);
          assert.equal(err.errors.requiredString1.message, 'Path `requiredString1` is required.');
          assert.equal(err.errors.requiredString2.message, 'oops, requiredString2 is missing. required');

          a.requiredString1 = a.requiredString2 = 'hi';
          a.name = 'three';
          a.myenum = 'y';
          a.matchString0 = a.matchString1 = 'no match';
          a.numMin0 = a.numMin1 = 2;
          a.numMax0 = a.numMax1 = 30;

          const err2 = await a.validate().then(() => null, err => err);
          assert.equal(err2.errors.name.message, '`three` is not a valid enum value for path `name`.');
          assert.equal(err2.errors.myenum.message, 'enum validator failed for path: myenum with y');
          assert.equal(err2.errors.matchString0.message, 'Path `matchString0` is invalid (no match).');
          assert.equal(err2.errors.matchString1.message, 'invalid string for matchString1 with value: no match');
          assert.equal(err2.errors.numMin0.message, 'Path `numMin0` (2) is less than minimum allowed value (10).');
          assert.equal(err2.errors.numMin1.message, 'hey, numMin1 is too small');
          assert.equal(err2.errors.numMax0.message, 'Path `numMax0` (30) is more than maximum allowed value (20).');
          assert.equal(err2.errors.numMax1.message, 'hey, numMax1 (30) is greater than 20');

          a.name = 'one';
          a.myenum = 'x';
          a.requiredString1 = 'fixed';
          a.matchString1 = a.matchString0 = 'bryancranston is an actor';
          a.numMin0 = a.numMax0 = a.numMin1 = a.numMax1 = 15;

          const err3 = await a.validate().then(() => null, err => err);
          assert(!err3);
        });

        it('for custom validators', async function() {
          const validate = function() {
            return false;
          };
          const validator = [validate, '{PATH} failed validation ({VALUE})'];

          const schema = new Schema({ x: { type: [], validate: validator } });
          const M = mongoose.model('custom-validator-' + random(), schema);

          const m = new M({ x: [3, 4, 5, 6] });

          await assert.rejects(m.validate(), /x failed validation/);
        });

        it('custom validators with promise (gh-5171)', async function() {
          const validate = async function(v) {
            return Promise.resolve(v === 'test');
          };

          const schema = new Schema({
            x: {
              type: String
            }
          });

          schema.path('x').validate({
            validator: validate
          });
          const M = mongoose.model('gh5171', schema);

          const m = new M({ x: 'not test' });

          try {
            await m.validate();
            assert.ok(false);
          } catch (err) {
            assert.ok(err.errors['x']);
          }
        });

        it('supports custom properties (gh-2132)', async function() {
          const schema = new Schema({
            x: {
              type: String,
              validate: [{
                validator: function() {
                  return false;
                },
                msg: 'Error code {ERRORCODE}',
                errorCode: 25
              }]
            }
          });
          const M = mongoose.model('gh-2132', schema, 'gh-2132');

          const m = new M({ x: 'a' });
          try {
            await m.validate();
            assert.ok(false);
          } catch (err) {
            assert.equal(err.errors.x.toString(), 'Error code 25');
            assert.equal(err.errors.x.properties.message, 'Error code 25');
            assert.equal(err.errors.x.properties.errorCode, 25);
          }
        });

        it('supports dynamic message for validators with callback (gh-1936)', async function() {
          const schema = new Schema({
            x: {
              type: String,
              validate: [{
                validator: function() {
                  throw new Error('Custom message');
                },
                msg: 'Does not matter'
              }]
            }
          });
          const M = mongoose.model('gh-1936', schema, 'gh-1936');

          const m = new M({ x: 'whatever' });
          const err = await m.validate().then(() => null, err => err);
          assert.equal(err.errors.x.toString(), 'Custom message');
        });

        it('passes full string value to custom validator message functions', async function() {
          const value = 'long-long-long.short-enough.com';
          const schema = new Schema({
            url: {
              type: String,
              validate: {
                validator: function() {
                  return false;
                },
                message: props => `${props.value} is not a valid url`
              }
            }
          });

          const M = mongoose.model('gh-15571', schema);
          const m = new M({ url: value });

          const err = await m.validate().then(() => null, err => err);
          assert.equal(err.errors.url.message, `${value} is not a valid url`);
          assert.equal(err.errors.url.value, value);
          assert.equal(err.errors.url.properties.value, value);
        });

        it('passes full string value to custom validator message functions on validateSync()', function() {
          const value = 'long-long-long.short-enough.com';
          const schema = new Schema({
            url: {
              type: String,
              validate: {
                validator: function() {
                  return false;
                },
                message: props => `${props.value} is not a valid url`
              }
            }
          });

          const M = mongoose.model('gh-15571-sync', schema);
          const m = new M({ url: value });

          const err = m.validateSync();
          assert.equal(err.errors.url.message, `${value} is not a valid url`);
          assert.equal(err.errors.url.value, value);
          assert.equal(err.errors.url.properties.value, value);
        });
      });
    });

    describe('types', function() {
      describe('are customizable', function() {
        it('for single custom validators', async function() {
          function validate() {
            return false;
          }

          const validator = [validate, '{PATH} failed validation ({VALUE})', 'customType'];

          const schema = new Schema({ x: { type: [], validate: validator } });
          const M = mongoose.model('custom-validator-' + random(), schema);

          const m = new M({ x: [3, 4, 5, 6] });

          try {
            await m.validate();
            assert.ok(false);
          } catch (err) {
            assert.equal(String(err.errors.x), 'x failed validation (3,4,5,6)');
            assert.equal(err.errors.x.properties.message, 'x failed validation (3,4,5,6)');
            assert.equal(err.errors.x.kind, 'customType');
          }
        });

        it('for many custom validators', async function() {
          function validate() {
            return false;
          }

          const validator = [
            { validator: validate, msg: '{PATH} failed validation ({VALUE})', type: 'customType' }
          ];
          const schema = new Schema({ x: { type: [], validate: validator } });
          const M = mongoose.model('custom-validator-' + random(), schema);

          const m = new M({ x: [3, 4, 5, 6] });

          try {
            await m.validate();
            assert.ok(false);
          } catch (err) {
            assert.equal(String(err.errors.x), 'x failed validation (3,4,5,6)');
            assert.equal(err.errors.x.kind, 'customType');
          }
        });
      });
    });

    it('should clear validator errors (gh-2302)', async function() {
      const userSchema = new Schema({ name: { type: String, required: true } });
      const User = mongoose.model('gh-2302', userSchema, 'gh-2302');

      const user = new User();
      const err = await user.validate().then(() => null, err => err);
      assert.ok(err);
      assert.ok(user.errors);
      assert.ok(user.errors.name);
      user.name = 'bacon';
      const err2 = await user.validate().then(() => null, err => err);
      assert.ok(!err2);
      assert.ok(!user.$__.validationError);
    });

    it('should allow an array of enums (gh-661)', async function() {
      const validBreakfastFoods = ['bacon', 'eggs', 'steak', 'coffee', 'butter'];
      const breakfastSchema = new Schema({
        foods: [{ type: String, enum: validBreakfastFoods }]
      });
      const Breakfast = mongoose.model('gh-661', breakfastSchema, 'gh-661');

      const goodBreakfast = new Breakfast({ foods: ['eggs', 'bacon'] });
      await goodBreakfast.validate();

      const badBreakfast = new Breakfast({ foods: ['tofu', 'waffles', 'coffee'] });
      try {
        await badBreakfast.validate();
        throw new Error('should not get here');
      } catch (error) {
        assert.ok(error);
        assert.ok(error.errors['foods.0']);
        assert.equal(error.errors['foods.0'].message,
          '`tofu` is not a valid enum value for path `foods.0`.');
        assert.ok(error.errors['foods.1']);
        assert.equal(error.errors['foods.1'].message,
          '`waffles` is not a valid enum value for path `foods.1`.');
        assert.ok(!error.errors['foods.2']);
      }
    });

    it('should allow null values for enum gh-3044', async function() {
      const testSchema = new Schema({
        name: {
          type: String,
          enum: ['test']
        }
      });
      const Test = mongoose.model('allow-null' + random(), testSchema);
      const a = new Test({ name: null });
      const err = await a.validate().then(() => null, err => err);
      assert.equal(err, null);
    });

    it('disallows null with allowNull false without making the path required', async function() {
      const schema = new Schema({
        name: { type: String, allowNull: false }
      });
      const Test = mongoose.model('allowNullFalse' + random(), schema);

      await new Test({}).validate();
      await new Test({ name: undefined }).validate();

      const err = await new Test({ name: null }).validate().then(() => null, err => err);
      assert.ok(err);
      assert.ok(err.errors['name']);
      assert.equal(err.errors['name'].name, 'ValidatorError');
      assert.equal(err.errors['name'].kind, 'allowNull');
      assert.equal(err.errors['name'].message, 'Path `name` does not allow null values.');
    });

    it('throws if allowNull is specified with required true', function() {
      assert.throws(function() {
        new Schema({
          name: { type: String, required: true, allowNull: false }
        });
      }, /Path "name" may not have `allowNull` specified when `required` is true/);

      assert.throws(function() {
        new Schema({
          name: { type: String, required: true, allowNull: true }
        });
      }, /Path "name" may not have `allowNull` specified when `required` is true/);

      assert.throws(function() {
        const schema = new Schema({ name: String });
        schema.path('name').allowNull(false).required(true);
      }, /Path "name" may not have `allowNull` specified when `required` is true/);

      assert.throws(function() {
        const schema = new Schema({ name: { type: String, required: true } });
        schema.path('name').allowNull(false);
      }, /Path "name" may not have `allowNull` specified when `required` is true/);
    });

    it('disallows null with allowNull false when required function returns false', async function() {
      const schema = new Schema({
        status: String,
        endDate: {
          type: Date,
          required: function() { return this.status === 'completed'; },
          allowNull: false
        }
      });
      const Test = mongoose.model('allowNullRequiredFunction' + random(), schema);

      await new Test({ status: 'pending', endDate: undefined }).validate();

      const err = await new Test({ status: 'pending', endDate: null }).validate().then(() => null, err => err);
      assert.ok(err);
      assert.ok(err.errors['endDate']);
      assert.equal(err.errors['endDate'].kind, 'allowNull');

      const requiredErr = await new Test({ status: 'completed', endDate: null }).validate().then(() => null, err => err);
      assert.ok(requiredErr);
      assert.ok(requiredErr.errors['endDate']);
      assert.equal(requiredErr.errors['endDate'].kind, 'required');
    });

    it('should allow an array of subdocuments with enums (gh-3521)', async function() {
      const coolSchema = new Schema({
        votes: [{
          vote: { type: String, enum: ['cool', 'not-cool'] }
        }]
      });
      const Cool = mongoose.model('gh-3521', coolSchema, 'gh-3521');

      const cool = new Cool();
      cool.votes.push(cool.votes.create({
        vote: 'cool'
      }));
      await cool.validate();

      const terrible = new Cool();
      terrible.votes.push(terrible.votes.create({
        vote: 'terrible'
      }));

      try {
        await terrible.validate();
        throw new Error('Should have failed validation');
      } catch (error) {
        assert.ok(error);
        assert.ok(error.errors['votes.0.vote']);
        assert.equal(error.errors['votes.0.vote'].message,
          '`terrible` is not a valid enum value for path `vote`.');
      }
    });

    it('should validate subdocuments subproperty enums (gh-4111)', async function() {
      const M = mongoose.model('M', new Schema({
        p: {
          val: { type: String, enum: ['test'] }
        },
        children: [{
          prop: {
            val: { type: String, enum: ['valid'] }
          }
        }]
      }));

      const model = new M();
      model.p = { val: 'test' };
      const child = model.children.create();
      child.prop = {
        val: 'valid'
      };

      model.children.push(child);

      try {
        await model.validate();
      } catch (error) {
        assert.ifError(error);
      }

      child.prop.val = 'invalid';

      assert.equal(model.children[0].prop.val, 'invalid');

      try {
        await model.validate();
        assert.ok(false);
      } catch (error) {
        assert.ok(error);
        assert.equal(error.errors['children.0.prop.val'].message,
          '`invalid` is not a valid enum value for path `prop.val`.');
      }
    });

    it('doesnt do double validation on document arrays (gh-2618)', async function() {
      const A = new Schema({ str: String });
      let B = new Schema({ a: [A] });
      let validateCalls = 0;
      B.path('a').validate(function() {
        ++validateCalls;
        return true;
      });

      B = mongoose.model('b', B);

      const p = new B();
      p.a.push({ str: 'asdf' });
      await p.validate();
      assert.equal(validateCalls, 1);
    });

    it('doesnt do double validation on document arrays underneath nested (gh-5411)', function(done) {
      const callScope = [];

      function myValidator() {
        callScope.push(this);
      }

      const TestSchema = new Schema({
        nest1: {
          nest2: {
            nestarr: [new Schema({
              value: {
                type: Boolean,
                required: false,
                validate: { validator: myValidator }
              }
            })]
          }
        }
      });

      const Test = mongoose.model('gh5411', TestSchema);
      const testInstance = new Test({
        nest1: {
          nest2: {
            nestarr: [{
              value: true
            }]
          }
        }
      });

      testInstance.nest1 = {
        nest2: {
          nestarr: [{
            value: false
          }]
        }
      };

      testInstance.validateSync();
      assert.equal(callScope.length, 1);
      assert.strictEqual(callScope[0], testInstance.nest1.nest2.nestarr[0]);
      done();
    });

    it('no double validation on set nested docarray (gh-4145)', async() => {
      let calls = 0;
      const myValidator = function() {
        ++calls;
        return true;
      };

      const InnerSchema = new mongoose.Schema({
        myfield: {
          type: String,
          validate: {
            validator: myValidator,
            message: 'Message'
          }
        },
        sibling: String
      });

      const MySchema = new mongoose.Schema({
        nest: {
          myarray: [InnerSchema]
        },
        rootSibling: String
      });

      const Model = mongoose.model('gh4145', MySchema);

      const instance = new Model({
        rootSibling: 'This is the root sibling'
      });
      // Direct object assignment
      instance.nest = {
        myarray: [{
          myfield: 'This is my field',
          sibling: 'This is the nested sibling'
        }]
      };

      await instance.validate();

      assert.equal(calls, 1);
    });

    it('returns cast errors', async function() {
      const breakfastSchema = new Schema({
        eggs: Number
      });
      const Breakfast = mongoose.model('gh-2611', breakfastSchema, 'gh-2611');

      const bad = new Breakfast({ eggs: 'none' });
      const error = await bad.validate().then(() => null, err => err);
      assert.ok(error);
    });

    it('handles multiple subdocument errors (gh-2589)', async function() {
      const foodSchema = new Schema({ name: { type: String, required: true, enum: ['bacon', 'eggs'] } });
      const breakfast = new Schema({ foods: [foodSchema], id: Number });

      const Breakfast = mongoose.model('gh-2589', breakfast, 'gh-2589');
      const bad = new Breakfast({ foods: [{ name: 'tofu' }, { name: 'waffles' }], id: 'Not a number' });
      try {
        await bad.validate();
        assert.ok(false);
      } catch (error) {
        assert.ok(error);
        assert.deepEqual(['id', 'foods.0.name', 'foods.1.name'], Object.keys(error.errors));
      }
    });

    it('handles subdocument cast errors (gh-2819)', async function() {
      const foodSchema = new Schema({ eggs: { type: Number, required: true } });
      const breakfast = new Schema({ foods: [foodSchema], id: Number });

      const Breakfast = mongoose.model('gh-2819', breakfast, 'gh-2819');

      // Initially creating subdocs with cast errors
      const bad = new Breakfast({ foods: [{ eggs: 'Not a number' }], id: 'Not a number' });
      let error = await bad.validate().then(() => null, err => err);
      assert.ok(error);
      assert.deepEqual(['foods.0.eggs', 'id'], Object.keys(error.errors).sort());
      assert.ok(error.errors['foods.0.eggs'] instanceof mongoose.Error.CastError);

      // Pushing docs with cast errors
      bad.foods.push({ eggs: 'Also not a number' });
      error = await bad.validate().then(() => null, err => err);
      assert.deepEqual(['foods.0.eggs', 'foods.1.eggs', 'id'], Object.keys(error.errors).sort());
      assert.ok(error.errors['foods.1.eggs'] instanceof mongoose.Error.CastError);

      // Splicing docs with cast errors
      bad.foods.splice(1, 1, { eggs: 'fail1' }, { eggs: 'fail2' });
      error = await bad.validate().then(() => null, err => err);
      assert.deepEqual(['foods.0.eggs', 'foods.1.eggs', 'foods.2.eggs', 'id'], Object.keys(error.errors).sort());
      assert.ok(error.errors['foods.0.eggs'] instanceof mongoose.Error.CastError);
      assert.ok(error.errors['foods.1.eggs'] instanceof mongoose.Error.CastError);
      assert.ok(error.errors['foods.2.eggs'] instanceof mongoose.Error.CastError);

      // Remove the cast error by setting field
      bad.foods[2].eggs = 3;
      error = await bad.validate().then(() => null, err => err);
      assert.deepEqual(['foods.0.eggs', 'foods.1.eggs', 'id'], Object.keys(error.errors).sort());
      assert.ok(error.errors['foods.0.eggs'] instanceof mongoose.Error.CastError);
      assert.ok(error.errors['foods.1.eggs'] instanceof mongoose.Error.CastError);

      // Remove the cast error using array.set()
      bad.foods[1].eggs = 1;
      error = await bad.validate().then(() => null, err => err);
      assert.deepEqual(['foods.0.eggs', 'id'], Object.keys(error.errors).sort());
      assert.ok(error.errors['foods.0.eggs'] instanceof mongoose.Error.CastError);
    });

    it('fails when you try to set a nested path to a primitive (gh-2592)', async function() {
      const breakfast = new Schema({ foods: { bacon: Number, eggs: Number } });

      const Breakfast = mongoose.model('gh-2592', breakfast, 'gh-2592');

      const bad = new Breakfast();
      bad.foods = 'waffles';
      try {
        await bad.validate();
        assert.ok(false);
      } catch (error) {
        assert.ok(error);
        const errorMessage = 'foods: Cast to Object failed for value ' +
            '"waffles" (type string) at path "foods"';
        assert.ok(error.toString().indexOf(errorMessage) !== -1, error.toString());
      }
    });

    it('doesnt execute other validators if required fails (gh-2725)', async function() {
      const breakfast = new Schema({ description: { type: String, required: true, maxlength: 50 } });

      const Breakfast = mongoose.model('gh2725', breakfast, 'gh2725');
      const bad = new Breakfast({});
      try {
        await bad.validate();
        assert.ok(false);
      } catch (error) {
        assert.ok(error);
        const errorMessage = 'ValidationError: description: Path `description` is required.';
        assert.equal(errorMessage, error.toString());
      }
    });

    it('doesnt execute other validators if required fails (gh-3025)', function(done) {
      const breakfast = new Schema({ description: { type: String, required: true, maxlength: 50 } });

      const Breakfast = mongoose.model('gh3025', breakfast, 'gh3025');
      const bad = new Breakfast({});
      const error = bad.validateSync();

      assert.ok(error);
      const errorMessage = 'ValidationError: description: Path `description` is required.';
      assert.equal(errorMessage, error.toString());
      done();
    });

    it('validateSync allows you to filter paths (gh-3153)', function(done) {
      const breakfast = new Schema({
        description: { type: String, required: true, maxlength: 50 },
        other: { type: String, required: true }
      });

      const Breakfast = mongoose.model('gh3153', breakfast, 'gh3153');
      const bad = new Breakfast({});
      const error = bad.validateSync('other');

      assert.ok(error);
      assert.equal(Object.keys(error.errors).length, 1);
      assert.ok(error.errors.other);
      assert.ok(!error.errors.description);
      done();
    });

    it('validateSync validates array elements when setting pathsToValidate (gh-13159)', function() {
      const schema = new Schema({
        permissions: [{ type: String, enum: ['users', 'anotherPermission'] }]
      });

      const Model = mongoose.model('gh13159', schema);

      const doc = new Model({
        permissions: ['avocado']
      });

      const error = doc.validateSync('permissions');
      assert.ok(error);
      assert.equal(Object.keys(error.errors).length, 1);
      assert.ok(error.errors['permissions.0']);
    });

    it('adds required validators to the front of the list (gh-2843)', async function() {
      const breakfast = new Schema({ description: { type: String, maxlength: 50, required: true } });

      const Breakfast = mongoose.model('gh2843', breakfast, 'gh2843');
      const bad = new Breakfast({});
      try {
        await bad.validate();
        assert.ok(false);
      } catch (error) {
        assert.ok(error);
        const errorMessage = 'ValidationError: description: Path `description` is required.';
        assert.equal(errorMessage, error.toString());
      }
    });

    it('sets path correctly when setter throws exception (gh-2832)', async function() {
      const breakfast = new Schema({
        description: {
          type: String, set: function() {
            throw new Error('oops');
          }
        }
      });

      const Breakfast = mongoose.model('gh2832', breakfast, 'gh2832');
      const bad = new Breakfast({ description: 'test' });
      try {
        await bad.validate();
        assert.ok(false);
      } catch (error) {
        assert.ok(error);
        const errorMessage = 'ValidationError: description: Cast to String failed for value "test" (type string) at path "description" for model "gh2832"';
        assert.equal(errorMessage, error.toString());
        assert.ok(error.errors.description);
        assert.equal(error.errors.description.reason.toString(), 'Error: oops');
      }
    });

    it('allows you to validate embedded doc that was .create()-ed (gh-2902) (gh-2929)', async function() {
      const parentSchema = mongoose.Schema({
        children: [{ name: { type: String, required: true } }]
      });

      const Parent = mongoose.model('gh2902', parentSchema);

      const p = new Parent();
      const n = p.children.create({ name: '2' });
      await n.validate();
      const bad = p.children.create({});
      p.children.push(bad);
      await assert.rejects(bad.validate(), /name/);
    });

    it('returns correct kind for user defined custom validators (gh-2885)', async function() {
      const s = mongoose.Schema({
        n: {
          type: String, validate: {
            validator: function() {
              return false;
            }
          }, msg: 'fail'
        }
      });
      const M = mongoose.model('gh2885', s);

      const m = new M({ n: 'test' });
      try {
        await m.validate();
        assert.ok(false);
      } catch (error) {
        assert.ok(error.errors);
        assert.equal(error.errors.n.kind, 'user defined');
      }
    });

    it('enums report kind (gh-3009)', async function() {
      const s = mongoose.Schema({ n: { type: String, enum: ['a', 'b'] } });
      const M = mongoose.model('gh3009', s);

      const m = new M({ n: 'test' });
      try {
        await m.validate();
        assert.ok(false);
      } catch (error) {
        assert.ok(error);
        assert.equal(error.errors.n.kind, 'enum');
      }
    });

    it('enums on arrays (gh-6102) (gh-8449)', async function() {
      assert.throws(function() {
        new Schema({
          array: {
            type: [Boolean],
            enum: [true]
          }
        });
      }, /`enum` can only be set on an array of strings or numbers/);

      let MySchema = new Schema({
        array: {
          type: [String],
          enum: ['qwerty']
        }
      });

      let Model = mongoose.model('gh6102', MySchema);
      const doc1 = new Model({ array: ['test'] });

      MySchema = new Schema({
        array: {
          type: [Number],
          enum: [3, 5, 7]
        }
      });

      mongoose.deleteModel('gh6102');
      Model = mongoose.model('gh6102', MySchema);
      const doc2 = new Model({ array: [1, 2, 3] });

      let err = await doc1.validate().then(() => null, err => err);
      assert.equal(err.name, 'ValidationError');
      err = await doc2.validate().then(() => null, err => err);
      assert.equal(err.name, 'ValidationError', err);
    });

    it('skips conditional required (gh-3539)', async function() {
      const s = mongoose.Schema({
        n: {
          type: Number, required: function() {
            return false;
          }, min: 0
        }
      });
      const M = mongoose.model('gh3539', s);

      const m = new M();
      const error = await m.validate().then(() => null, err => err);
      assert.ifError(error);
    });

    it('handles function for date min/max (gh-7600)', function() {
      const s = mongoose.Schema({
        minDate: String,
        date: {
          type: Date,
          min: function() { return this.minDate; }
        }
      });
      const M = mongoose.model('gh7600', s);

      let m = new M({ minDate: '2018-06-01', date: '2018-05-01' });
      let err = m.validateSync();
      assert.ok(err);
      assert.ok(err.errors['date']);

      m = new M({ minDate: '2018-06-01', date: '2018-07-01' });
      err = m.validateSync();
      assert.ifError(err);
    });

    it('evaluate message function gh6523', async function() {
      const s = mongoose.Schema({
        n: {
          type: String,
          // required: true,
          validate: {
            validator: function() {
              return false;
            },
            message: function(properties) {
              return 'fail ' + properties.value;
            }
          }
        }
      });
      const M = mongoose.model('gh6523', s);
      const m = new M({ n: 0 });

      try {
        await m.validate();
        assert.ok(false);
      } catch (error) {
        assert.equal('fail 0', error.errors['n'].message);
      }
    });

    it('Allows for doc to be passed as another parameter (gh-12564)', async function() {
      let document = null;
      const s = mongoose.Schema({
        n: {
          type: String,
          validate: {
            validator: function(v) {
              return v != null;
            },
            message: function(properties, doc) {
              document = doc;
              return 'fail ' + properties.path + ' on doc ' + doc._id;
            }
          }
        },
        field: String
      });
      const M = mongoose.model('gh-12564', s);
      const m = new M({ n: null, field: 'Yo' });

      try {
        await m.validate();
        assert.ok(false);
      } catch (error) {
        assert.strictEqual(document, m);
        assert.ok(error.errors['n'].message.includes(m._id));
        assert.equal('fail n on doc ' + m._id, error.errors['n'].message);
      }
    });

    it('evaluate message function for required field gh6523', async function() {
      const s = mongoose.Schema({
        n: {
          type: String,
          // required: true,
          required: [true, function(properties) {
            return 'fail ' + properties.path;
          }]
        }
      });
      const M = mongoose.model('gh6523-2', s);
      const m = new M();

      try {
        await m.validate();
        assert.ok(false);
      } catch (error) {
        assert.equal('fail n', error.errors['n'].message);
      }
    });

    describe('`enum` accepts an object to support TypeScript enums (gh-9546) (gh-9535) (gh-15913)', function() {
      it('strings', function() {
        // Arrange
        const userSchema = new Schema({
          name: {
            type: String,
            enum: {
              hafez: 'Hafez',
              nada: 'Nada'
            }
          }
        });

        const User = mongoose.model('User_gh9546_1', userSchema);

        // Act
        const user = new User({ name: 'Ameen' });
        const err = user.validateSync();

        // Assert
        assert.equal(err.message, 'User_gh9546_1 validation failed: name: `Ameen` is not a valid enum value for path `name`.');
      });

      it('numbers', function() {
        // Arrange
        const userSchema = new Schema({
          status: {
            type: Number,
            enum: {
              0: 0,
              1: 1
            }
          }
        });

        const User = mongoose.model('User_gh9546_2', userSchema);

        // Act
        const user = new User({ status: 2 });
        const err = user.validateSync();

        // Assert
        assert.equal(err.message, 'User_gh9546_2 validation failed: status: `2` is not a valid enum value for path `status`.');
      });

      it('arrays', function() {
        // Arrange
        const userSchema = new Schema({
          favoriteNumbers: {
            type: [Number],
            enum: { 1: 1, 2: 2 }
          }
        });

        const User = mongoose.model('User_gh9546_3', userSchema);

        // Act
        const user = new User({ favoriteNumbers: [1, 10, 2, 20] });
        const err = user.validateSync();

        // Assert
        assert.equal(err.message, 'User_gh9546_3 validation failed: favoriteNumbers.1: `10` is not a valid enum value for path `favoriteNumbers.1`., favoriteNumbers.3: `20` is not a valid enum value for path `favoriteNumbers.3`.');
      });

      it('passes when using valid data', function() {
        // Arrange
        const userSchema = new Schema({
          name: {
            type: String,
            enum: {
              hafez: 'Hafez',
              nada: 'Nada'
            }
          },
          status: {
            type: Number,
            enum: {
              0: 0,
              1: 1
            }
          },
          favoriteNumbers: {
            type: [Number],
            enum: { 1: 1, 2: 2 }
          }
        });

        const User = mongoose.model('User_gh9546_4', userSchema);

        // Act
        const user = new User({ name: 'Hafez', status: 1, favoriteNumbers: [1, 2, 2, 2] });
        const err = user.validateSync();

        // Assert
        assert.ifError(err);
      });

      it('supports TypeScript-style enums (numeric reverse-mapping and string enums)', function() {
        const tsNumericEnum = {
          0: 'Zero',
          1: 'One',
          Zero: 0,
          One: 1
        };

        const tsStringEnum = {
          Alpha: 'ALPHA',
          Beta: 'BETA'
        };

        const userSchema = new Schema({
          status: {
            type: Number,
            enum: tsNumericEnum
          },
          kind: {
            type: String,
            enum: tsStringEnum
          }
        });

        const User = mongoose.model('User_gh15913_ts_' + random(), userSchema);

        // invalid numeric and string values should fail
        const badUser = new User({ status: 2, kind: 'GAMMA' });
        const err = badUser.validateSync();
        assert.ok(err);
        assert.ok(err.errors.status);

        // valid values should pass
        const goodUser = new User({ status: 1, kind: 'BETA' });
        assert.ifError(goodUser.validateSync());
      });
    });

    it('should validate required UUID fields correctly (gh-12991)', function() {
      const uuidSchema = new mongoose.Schema({
        _id: { type: mongoose.Schema.Types.UUID, required: true },
        name: { type: mongoose.Schema.Types.String, required: true }
      });

      const uuidRefSchema = new mongoose.Schema({
        _id: { type: mongoose.Schema.Types.UUID, required: true },
        uuidRef: { type: mongoose.Schema.Types.UUID, ref: 'UUIDModel', required: true },
        uuidNonRef: { type: mongoose.Schema.Types.UUID, required: true },
        uuidRefNonRequired: { type: mongoose.Schema.Types.UUID, ref: 'UUIDModel' },
        name: { type: mongoose.Schema.Types.String, required: true }
      });

      const UUIDModel = mongoose.model('UUIDModel', uuidSchema, 'uuids');

      const UUIDRefModel = mongoose.model('UUIDRefModel', uuidRefSchema, 'uuidRefs');

      const uuid = new UUIDModel({ _id: uuidv4(), name: 'uuidName' });
      assert.ifError(uuid.validateSync());

      const uuidRef = new UUIDRefModel({
        _id: uuidv4(),
        uuidRef: uuidv4(),
        uuidNonRef: uuidv4(),
        uuidRefNonRequired: uuidv4(),
        name: 'uuidRefName'
      });
      assert.ifError(uuidRef.validateSync());

      const uuidRef2 = new UUIDRefModel({
        _id: uuidv4(),
        uuidNonRef: uuidv4(),
        uuidRefNonRequired: uuidv4(),
        name: 'uuidRefName'
      });

      const err2 = uuidRef2.validateSync();
      assert.ok(err2);
      assert.ok(err2.errors['uuidRef']);

      const uuidRef3 = new UUIDRefModel({
        _id: uuidv4(),
        uuidRef: uuidv4(),
        uuidRefNonRequired: uuidv4(),
        name: 'uuidRefName'
      });

      const err3 = uuidRef3.validateSync();
      assert.ok(err3);
      assert.ok(err3.errors['uuidNonRef']);

      const uuidRef4 = new UUIDRefModel({
        _id: uuidv4(),
        uuidRef: uuidv4(),
        uuidNonRef: uuidv4(),
        name: 'uuidRefName'
      });

      assert.ifError(uuidRef4.validateSync());
    });
  });
});