Repository: Automattic/mongoose (JavaScript)

Issue:
Bug on validateSync Method
### Prerequisites

- [X] I have written a descriptive issue title
- [ ] I have searched existing issues to ensure the bug has not already been reported


### Mongoose version

7.0.1

### Node.js version

18.13.0

### MongoDB server version

6.0.4

### Typescript version (if applicable)

4.9.5

### Description

On the method validateSync, you can provide the paths to validate, if you provide a path that corresponds to an array, and the item's values of that array are wrong, it won't throw an error, because you need to provide in the path the index of the value you want to validate.

### Steps to Reproduce

Create a model like this: 
```
import { model, Schema } from "mongoose";

const schema = new Schema({
  permissions: [{type: String, enum: ['users', 'anotherPermission']}]
});

const Model = model('Model', UserSchema);
```

Then create an object with invalid values on permissions:
```
const a = {
  permissions: ["avocado"]
}
```

Then validate the object with the model you previously created in these two different ways and see the errors:
```
const document = new model(a);
const error = document.validateSync();
const error1 = document.validateSync(["permissions.0"]);
const error2 = document.validateSync(["permissions"]);
```

In the first one, and the second one, you should be able to get an error, and in the third one, you don't get any error.

### Expected Behavior

It is clear that there is an error in the Object that we created previously, but you only are available to get in two of three scenarios, but for me, we should get the error in the three scenarios, when we specify a path that we want to validate, the method should be able to validate all the nested object in that path.


Discussion:

Patch:
diff --git a/lib/document.js b/lib/document.js
index 8fa28f22aca..83da18ebd2f 100644
--- a/lib/document.js
+++ b/lib/document.js
@@ -2557,7 +2557,7 @@ function _evaluateRequiredFunctions(doc) {
  * ignore
  */
 
-function _getPathsToValidate(doc) {
+function _getPathsToValidate(doc, pathsToValidate, pathsToSkip) {
   const skipSchemaValidators = {};
 
   _evaluateRequiredFunctions(doc);
@@ -2628,6 +2628,22 @@ function _getPathsToValidate(doc) {
     }
   }
 
+  for (const path of paths) {
+    // Single nested paths (paths embedded under single nested subdocs) will
+    // be validated on their own when we call `validate()` on the subdoc itself.
+    // Re: gh-8468
+    if (doc.$__schema.singleNestedPaths.hasOwnProperty(path)) {
+      paths.delete(path);
+      continue;
+    }
+  }
+
+  if (Array.isArray(pathsToValidate)) {
+    paths = _handlePathsToValidate(paths, pathsToValidate);
+  } else if (Array.isArray(pathsToSkip)) {
+    paths = _handlePathsToSkip(paths, pathsToSkip);
+  }
+
   // from here on we're not removing items from paths
 
   // gh-661: if a whole array is modified, make sure to run validation on all
@@ -2687,13 +2703,6 @@ function _getPathsToValidate(doc) {
   }
 
   for (const path of paths) {
-    // Single nested paths (paths embedded under single nested subdocs) will
-    // be validated on their own when we call `validate()` on the subdoc itself.
-    // Re: gh-8468
-    if (doc.$__schema.singleNestedPaths.hasOwnProperty(path)) {
-      paths.delete(path);
-      continue;
-    }
     const _pathType = doc.$__schema.path(path);
     if (!_pathType || !_pathType.$isSchemaMap) {
       continue;
@@ -2776,7 +2785,7 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
   };
 
   // only validate required fields when necessary
-  const pathDetails = _getPathsToValidate(this);
+  const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip);
   let paths = shouldValidateModifiedOnly ?
     pathDetails[0].filter((path) => this.$isModified(path)) :
     pathDetails[0];
@@ -2784,11 +2793,6 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
   if (typeof pathsToValidate === 'string') {
     pathsToValidate = pathsToValidate.split(' ');
   }
-  if (Array.isArray(pathsToValidate)) {
-    paths = _handlePathsToValidate(paths, pathsToValidate);
-  } else if (pathsToSkip) {
-    paths = _handlePathsToSkip(paths, pathsToSkip);
-  }
 
   if (paths.length === 0) {
     return immediate(function() {
@@ -2907,12 +2911,12 @@ function _handlePathsToValidate(paths, pathsToValidate) {
     }
   }
 
-  const ret = [];
+  const ret = new Set();
   for (const path of paths) {
     if (_pathsToValidate.has(path)) {
-      ret.push(path);
+      ret.add(path);
     } else if (parentPaths.has(path)) {
-      ret.push(parentPaths.get(path));
+      ret.add(parentPaths.get(path));
     }
   }
   return ret;
@@ -2924,8 +2928,8 @@ function _handlePathsToValidate(paths, pathsToValidate) {
 
 function _handlePathsToSkip(paths, pathsToSkip) {
   pathsToSkip = new Set(pathsToSkip);
-  paths = paths.filter(p => !pathsToSkip.has(p));
-  return paths;
+  paths = Array.from(paths).filter(p => !pathsToSkip.has(p));
+  return new Set(paths);
 }
 
 /**
@@ -2981,17 +2985,12 @@ Document.prototype.validateSync = function(pathsToValidate, options) {
   }
 
   // only validate required fields when necessary
-  const pathDetails = _getPathsToValidate(this);
+  const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip);
   let paths = shouldValidateModifiedOnly ?
     pathDetails[0].filter((path) => this.$isModified(path)) :
     pathDetails[0];
   const skipSchemaValidators = pathDetails[1];
 
-  if (Array.isArray(pathsToValidate)) {
-    paths = _handlePathsToValidate(paths, pathsToValidate);
-  } else if (Array.isArray(pathsToSkip)) {
-    paths = _handlePathsToSkip(paths, pathsToSkip);
-  }
   const validating = {};
 
   for (let i = 0, len = paths.length; i < len; ++i) {


Test patch:
diff --git a/test/schema.validation.test.js b/test/schema.validation.test.js
index a62f18e183f..22fab457bd1 100644
--- a/test/schema.validation.test.js
+++ b/test/schema.validation.test.js
@@ -1134,6 +1134,23 @@ describe('schema', function() {
       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 } });
 
diff --git a/test/types/querycursor.test.ts b/test/types/querycursor.test.ts
index 82bab1f33bd..611e9543977 100644
--- a/test/types/querycursor.test.ts
+++ b/test/types/querycursor.test.ts
@@ -19,4 +19,4 @@ Test.find().cursor().
     expectType<Types.ObjectId>(doc._id);
     expectType<number>(i);
   }).
-  then(() => console.log('Done!'));
\ No newline at end of file
+  then(() => console.log('Done!'));