Repository: codekie/openapi-examples-validator (js)

Issue:
the "--no-additional-properties" flag is not working
It looks like the `--no-additional-properties` flag is not working as expected.

**Expected result:** 
It should raise an error if an attribute is present in an example but not declared in schema.

**Actual result:**
No errors were found. The given example is considered valid. 

**To reproduce the issue:**

> test-with-extra-property.json
```
{ "id": 123, "name": "Bob", "gender": "male" }
```


> openapi-spec.yml
```
swagger: "2.0"
info:
  version: 1.0.0
consumes:
  - application/json
produces:
  - application/json
paths:
  /user:
    get:
      responses:
        "200":
          schema:
            type: object
            properties:
              name:
                type: string
              id:
                type: integer
            required:
              - name
              - id
```

> command ran
```
yarn openapi-examples-validator \
--schema-jsonpath $.paths./user.get.responses.200.schema \
--example-filepath ./test-with-extra-property.json \
--no-additional-properties \
./openapi-spec.yml
```

> console output
```
Validating single external example
Schemas with examples found: 1
Examples without schema found: 0
Total examples found: 1

No errors found.
```

I would have expected the `gender` attribute to raise an error because it's not part of the API spec definition.


Let me know if you need more info or if I can help.

Pull request description:
fix: fix "no-additional-properties" option, especially for OpenAPI v2
Closes #178 

Patch:
diff --git a/src/impl/service/common.js b/src/impl/service/common.js
index c90d8af..6ca6f20 100644
--- a/src/impl/service/common.js
+++ b/src/impl/service/common.js
@@ -1,11 +1,6 @@
 const { JSONPath: jsonPath } = require('jsonpath-plus'),
     ResultType = require('../../const/result-type');
 
-const JSON_PATHS__OBJECTS = [
-    '$..application/json.schema',
-    '$..schema..[?(@.properties && (@property === "schema" || @property === "items" || @.type === "object"))]'
-];
-
 module.exports = {
     applyCallbackToAllObjectModels
 };
@@ -45,17 +40,20 @@ module.exports = {
 function applyCallbackToAllObjectModels(openApiSpec, examplePaths, matchCallbackBuilder) {
     // Find all matches
     const paths = new Set();
-    JSON_PATHS__OBJECTS.forEach(jsPath => {
-        _find(openApiSpec, jsPath)
-            .forEach(match => {
-                paths.add(match);
-            });
-    });
+    _find(openApiSpec, '$..schema..')
+        .forEach(match => {
+            if (_isPropertiesDefinition(match)) { return; }
+            paths.add(match);
+        });
     // Exclude examples
     _excludeExamples(openApiSpec, paths, examplePaths);
     // Set flag
     for (const jsPath of paths) {
-        _find(openApiSpec, jsPath, ResultType.value, matchCallbackBuilder(jsPath));
+        const callback = matchCallbackBuilder(jsPath);
+        _find(openApiSpec, jsPath, ResultType.value, (result, resultType, data) => {
+            if (!_isObjectDefinition(result)) { return; }
+            callback(result, resultType, data);
+        });
     }
 }
 
@@ -96,3 +94,15 @@ function _excludeExamples(openApiSpec, paths, examplePaths) {
                 });
         });
 }
+
+function _isPropertiesDefinition(path) {
+    // Path has to end with `properties`
+    if (!path.match(/\['properties']$/)) { return; }
+    // Every second consecutive `properties` actually is not a property-definition, but a property itself
+    const consecutiveMatch = path.match(/(?<!\['properties'])(\['properties']\['properties'])+$/);
+    return !consecutiveMatch || consecutiveMatch.length % 2 !== 0;
+}
+
+function _isObjectDefinition(entity) {
+    return entity.type === 'object' || entity.properties;
+}
diff --git a/src/impl/service/no-additional-properties.js b/src/impl/service/no-additional-properties.js
index 5a4683b..718b2af 100644
--- a/src/impl/service/no-additional-properties.js
+++ b/src/impl/service/no-additional-properties.js
@@ -41,17 +41,7 @@ function setNoAdditionalProperties(openApiSpec, examplePaths = []) {
                 if (schema.hasOwnProperty('additionalProperties')) {
                     return;
                 }
-                // Exclude schema that are not objects
-                if (!isAnObjectSchema(schema)) {
-                    return;
-                }
                 schema.additionalProperties = false;
             };
         });
 }
-
-function isAnObjectSchema(schema) {
-    return schema.hasOwnProperty('properties')
-        || schema.hasOwnProperty('additionalProperties')
-        || (schema.hasOwnProperty('type') && schema.type === 'object');
-}


Test patch:
diff --git a/test/data/output/api-additional-properties.js b/test/data/output/api-additional-properties.js
new file mode 100644
index 0000000..f5df5aa
--- /dev/null
+++ b/test/data/output/api-additional-properties.js
@@ -0,0 +1,8 @@
+module.exports.value = `Validating examples
+Schemas with examples found: 2
+Examples without schema found: 0
+Total examples found: 2
+
+Errors found.
+
+`;
diff --git a/test/data/v2/additional-properties/errors-schema-invalid-with-examples.json b/test/data/v2/additional-properties/errors-schema-invalid-with-examples.json
new file mode 100644
index 0000000..155d02b
--- /dev/null
+++ b/test/data/v2/additional-properties/errors-schema-invalid-with-examples.json
@@ -0,0 +1,101 @@
+[
+  {
+    "type": "Validation",
+    "message": "should NOT have additional properties",
+    "keyword": "additionalProperties",
+    "dataPath": "[0]",
+    "schemaPath": "#/items/additionalProperties",
+    "params": {
+      "additionalProperty": "location"
+    },
+    "examplePath": "/paths/~1pets/get/responses/200/examples/application~1json"
+  },
+  {
+    "type": "Validation",
+    "message": "should NOT have additional properties",
+    "keyword": "additionalProperties",
+    "dataPath": "[0].schema",
+    "schemaPath": "#/items/properties/schema/additionalProperties",
+    "params": {
+      "additionalProperty": "spec"
+    },
+    "examplePath": "/paths/~1pets/get/responses/200/examples/application~1json"
+  },
+  {
+    "type": "Validation",
+    "message": "should NOT have additional properties",
+    "keyword": "additionalProperties",
+    "dataPath": "[0].schema.properties",
+    "schemaPath": "#/items/properties/schema/properties/properties/additionalProperties",
+    "params": {
+      "additionalProperty": "nickname"
+    },
+    "examplePath": "/paths/~1pets/get/responses/200/examples/application~1json"
+  },
+  {
+    "type": "Validation",
+    "message": "should NOT have additional properties",
+    "keyword": "additionalProperties",
+    "dataPath": "[0].schema.properties.properties",
+    "schemaPath": "#/items/properties/schema/properties/properties/properties/properties/additionalProperties",
+    "params": {
+      "additionalProperty": "some"
+    },
+    "examplePath": "/paths/~1pets/get/responses/200/examples/application~1json"
+  },
+  {
+    "type": "Validation",
+    "message": "should NOT have additional properties",
+    "keyword": "additionalProperties",
+    "dataPath": "",
+    "schemaPath": "#/additionalProperties",
+    "params": {
+      "additionalProperty": "age"
+    },
+    "examplePath": "/paths/~1pets~1{petId}/get/responses/200/examples/application~1json"
+  },
+  {
+    "type": "Validation",
+    "message": "should NOT have additional properties",
+    "keyword": "additionalProperties",
+    "dataPath": "",
+    "schemaPath": "#/additionalProperties",
+    "params": {
+      "additionalProperty": "owner"
+    },
+    "examplePath": "/paths/~1pets~1{petId}/get/responses/200/examples/application~1json"
+  },
+  {
+    "type": "Validation",
+    "message": "should NOT have additional properties",
+    "keyword": "additionalProperties",
+    "dataPath": ".schema",
+    "schemaPath": "#/properties/schema/additionalProperties",
+    "params": {
+      "additionalProperty": "spec"
+    },
+    "examplePath": "/paths/~1pets~1{petId}/get/responses/200/examples/application~1json"
+  },
+  {
+    "type": "Validation",
+    "message": "should NOT have additional properties",
+    "keyword": "additionalProperties",
+    "dataPath": ".schema.properties",
+    "schemaPath": "#/properties/schema/properties/properties/additionalProperties",
+    "params": {
+      "additionalProperty": "callname"
+    },
+    "examplePath": "/paths/~1pets~1{petId}/get/responses/200/examples/application~1json"
+  },
+  {
+    "type": "Validation",
+    "message": "should NOT have additional properties",
+    "keyword": "additionalProperties",
+    "dataPath": "[0].schema.properties.type",
+    "schemaPath": "#/items/properties/schema/properties/properties/properties/type/additionalProperties",
+    "params": {
+      "additionalProperty": "bar"
+    },
+    "examplePath": "/paths/~1pets/get/responses/200/examples/application~1json"
+  }
+]
diff --git a/test/data/v2/additional-properties/invalid-with-examples.yaml b/test/data/v2/additional-properties/invalid-with-examples.yaml
new file mode 100644
index 0000000..4d7c0df
--- /dev/null
+++ b/test/data/v2/additional-properties/invalid-with-examples.yaml
@@ -0,0 +1,155 @@
+---
+swagger: '2.0'
+info:
+  license:
+    name: MIT
+  title: Swagger Petstore
+  version: 1.0.0
+host: petstore.swagger.io
+basePath: "/v1"
+schemes:
+- http
+paths:
+  "/pets":
+    get:
+      produces:
+      - application/json
+      parameters: []
+      responses:
+        '200':
+          description: An array of pets
+          schema:
+            items:
+              properties:
+                id:
+                  type: number
+                name:
+                  type: string
+                schema:
+                  properties:
+                    properties:
+                      properties:
+                        color:
+                          type: string
+                        properties:
+                          properties:
+                            foo:
+                              type: number
+                          type: object
+                        type:
+                          type: object
+                      type: object
+                  type: object
+                tag:
+                  type: string
+              required:
+              - id
+              - name
+            type: array
+          examples:
+            application/json:
+              - id: 1
+                name: john
+                location: home
+                schema:
+                  spec: 'pet_definition'
+                  properties:
+                    nickname: buddy
+                    color: black
+                    type:
+                      bar: 'baz'
+                    properties:
+                     foo: 5
+                     some: id
+        default:
+          description: unexpected error
+          schema:
+            properties:
+              code:
+                type: number
+              message:
+                type: string
+            required:
+            - code
+            - message
+      operationId: listPets
+      summary: List all pets
+    post:
+      produces:
+      - application/json
+      parameters: []
+      responses:
+        '201':
+          description: Null response
+        default:
+          description: unexpected error
+          schema:
+            properties:
+              code:
+                type: number
+              message:
+                type: string
+            required:
+            - code
+            - message
+      tags:
+      - pets
+      operationId: createPets
+      summary: Create a pet
+  "/pets/{petId}":
+    get:
+      produces:
+      - application/json
+      parameters:
+      - description: The id of the pet to retrieve
+        in: path
+        name: petId
+        required: true
+        type: string
+      responses:
+        '200':
+          description: Expected response to a valid request
+          examples:
+            application/json:
+              age: 5
+              id: 0
+              name: Herbert
+              owner: John
+              schema:
+                spec: pet_definition
+                properties:
+                  callname: buddy
+                  color: black
+              tag: Doggo
+          schema:
+            properties:
+              id:
+                type: number
+              name:
+                type: string
+              schema:
+                properties:
+                  properties:
+                    properties:
+                      color:
+                        type: string
+                    type: object
+                type: object
+              tag:
+                type: string
+            required:
+            - id
+            - name
+        default:
+          description: unexpected error
+          schema:
+            properties:
+              code:
+                type: number
+              message:
+                type: string
+            required:
+            - code
+            - message
+      operationId: showPetById
+      summary: Info for a specific pet
diff --git a/test/data/v2/valid-with-no-additional-properties.json b/test/data/v2/valid-with-no-additional-properties.json
index 48f2fc9..becb18b 100644
--- a/test/data/v2/valid-with-no-additional-properties.json
+++ b/test/data/v2/valid-with-no-additional-properties.json
@@ -48,6 +48,7 @@
               "required": [
                 "versions"
               ],
+              "additionalProperties": false,
               "properties": {
                 "versions": {
                   "type": "array",
@@ -101,6 +102,7 @@
               "required": [
                 "versions"
               ],
+              "additionalProperties": false,
               "properties": {
                 "versions": {
                   "type": "array",
diff --git a/test/data/v3/additional-properties/errors-schema-invalid-with-examples.json b/test/data/v3/additional-properties/errors-schema-invalid-with-examples.json
new file mode 100644
index 0000000..30b5df5
--- /dev/null
+++ b/test/data/v3/additional-properties/errors-schema-invalid-with-examples.json
@@ -0,0 +1,101 @@
+[
+  {
+    "type": "Validation",
+    "message": "should NOT have additional properties",
+    "keyword": "additionalProperties",
+    "dataPath": "[0]",
+    "schemaPath": "#/items/additionalProperties",
+    "params": {
+      "additionalProperty": "location"
+    },
+    "examplePath": "/paths/~1pets/get/responses/200/content/application~1json/examples/response/value"
+  },
+  {
+    "type": "Validation",
+    "message": "should NOT have additional properties",
+    "keyword": "additionalProperties",
+    "dataPath": "[0].schema",
+    "schemaPath": "#/items/properties/schema/additionalProperties",
+    "params": {
+      "additionalProperty": "spec"
+    },
+    "examplePath": "/paths/~1pets/get/responses/200/content/application~1json/examples/response/value"
+  },
+  {
+    "type": "Validation",
+    "message": "should NOT have additional properties",
+    "keyword": "additionalProperties",
+    "dataPath": "[0].schema.properties",
+    "schemaPath": "#/items/properties/schema/properties/properties/additionalProperties",
+    "params": {
+      "additionalProperty": "nickname"
+    },
+    "examplePath": "/paths/~1pets/get/responses/200/content/application~1json/examples/response/value"
+  },
+  {
+    "type": "Validation",
+    "message": "should NOT have additional properties",
+    "keyword": "additionalProperties",
+    "dataPath": "[0].schema.properties.properties",
+    "schemaPath": "#/items/properties/schema/properties/properties/properties/properties/additionalProperties",
+    "params": {
+      "additionalProperty": "some"
+    },
+    "examplePath": "/paths/~1pets/get/responses/200/content/application~1json/examples/response/value"
+  },
+  {
+    "type": "Validation",
+    "message": "should NOT have additional properties",
+    "keyword": "additionalProperties",
+    "dataPath": "",
+    "schemaPath": "#/additionalProperties",
+    "params": {
+      "additionalProperty": "age"
+    },
+    "examplePath": "/paths/~1pets~1{petId}/get/responses/200/content/application~1json/examples/response/value"
+  },
+  {
+    "type": "Validation",
+    "message": "should NOT have additional properties",
+    "keyword": "additionalProperties",
+    "dataPath": "",
+    "schemaPath": "#/additionalProperties",
+    "params": {
+      "additionalProperty": "owner"
+    },
+    "examplePath": "/paths/~1pets~1{petId}/get/responses/200/content/application~1json/examples/response/value"
+  },
+  {
+    "type": "Validation",
+    "message": "should NOT have additional properties",
+    "keyword": "additionalProperties",
+    "dataPath": ".schema",
+    "schemaPath": "#/properties/schema/additionalProperties",
+    "params": {
+      "additionalProperty": "spec"
+    },
+    "examplePath": "/paths/~1pets~1{petId}/get/responses/200/content/application~1json/examples/response/value"
+  },
+  {
+    "type": "Validation",
+    "message": "should NOT have additional properties",
+    "keyword": "additionalProperties",
+    "dataPath": ".schema.properties",
+    "schemaPath": "#/properties/schema/properties/properties/additionalProperties",
+    "params": {
+      "additionalProperty": "callname"
+    },
+    "examplePath": "/paths/~1pets~1{petId}/get/responses/200/content/application~1json/examples/response/value"
+  },
+  {
+    "type": "Validation",
+    "message": "should NOT have additional properties",
+    "keyword": "additionalProperties",
+    "dataPath": "[0].schema.properties.type",
+    "schemaPath": "#/items/properties/schema/properties/properties/properties/type/additionalProperties",
+    "params": {
+      "additionalProperty": "bar"
+    },
+    "examplePath": "/paths/~1pets/get/responses/200/content/application~1json/examples/response/value"
+  }
+]
diff --git a/test/data/v3/additional-properties/invalid-with-examples.yaml b/test/data/v3/additional-properties/invalid-with-examples.yaml
new file mode 100644
index 0000000..3133e52
--- /dev/null
+++ b/test/data/v3/additional-properties/invalid-with-examples.yaml
@@ -0,0 +1,163 @@
+openapi: 3.0.0
+info:
+  license:
+    name: MIT
+  title: Swagger Petstore
+  version: 1.0.0
+servers:
+  - url: 'http://petstore.swagger.io/v1'
+paths:
+  /pets:
+    get:
+      responses:
+        '200':
+          content:
+            application/json:
+              examples:
+                response:
+                  value:
+                    - location: home
+                      id: 1
+                      name: john
+                      schema:
+                        spec: pet_definition
+                        properties:
+                          nickname: buddy
+                          color: black
+                          properties:
+                            some: id
+                            foo: 5
+                          type:
+                            bar: baz
+              schema:
+                items:
+                  properties:
+                    id:
+                      type: number
+                    name:
+                      type: string
+                    schema:
+                      properties:
+                        properties:
+                          properties:
+                            color:
+                              type: string
+                            properties:
+                              properties:
+                                foo:
+                                  type: number
+                              type: object
+                            type:
+                              type: object
+                          type: object
+                      type: object
+                    tag:
+                      type: string
+                  required:
+                    - id
+                    - name
+                  type: object
+                type: array
+          description: An array of pets
+        default:
+          content:
+            application/json:
+              schema:
+                properties:
+                  code:
+                    type: number
+                  message:
+                    type: string
+                required:
+                  - code
+                  - message
+                type: object
+          description: unexpected error
+      operationId: listPets
+      summary: List all pets
+    post:
+      responses:
+        '201':
+          description: Null response
+        default:
+          content:
+            application/json:
+              schema:
+                properties:
+                  code:
+                    type: number
+                  message:
+                    type: string
+                required:
+                  - code
+                  - message
+                type: object
+          description: unexpected error
+      tags:
+        - pets
+      operationId: createPets
+      summary: Create a pet
+  '/pets/{petId}':
+    get:
+      parameters:
+        - description: The id of the pet to retrieve
+          in: path
+          name: petId
+          required: true
+          schema:
+            type: string
+      responses:
+        '200':
+          content:
+            application/json:
+              examples:
+                response:
+                  value:
+                    age: 5
+                    id: 0
+                    name: Herbert
+                    owner: John
+                    schema:
+                      spec: pet_definition
+                      properties:
+                        callname: buddy
+                        color: black
+                    tag: Doggo
+              schema:
+                properties:
+                  id:
+                    type: number
+                  name:
+                    type: string
+                  schema:
+                    properties:
+                      properties:
+                        properties:
+                          color:
+                            type: string
+                        type: object
+                    type: object
+                  tag:
+                    type: string
+                required:
+                  - id
+                  - name
+                type: object
+          description: Expected response to a valid request
+        default:
+          content:
+            application/json:
+              schema:
+                properties:
+                  code:
+                    type: number
+                  message:
+                    type: string
+                required:
+                  - code
+                  - message
+                type: object
+          description: unexpected error
+      operationId: showPetById
+      summary: Info for a specific pet
+
diff --git a/test/specs/cli.js b/test/specs/cli.js
index 531c53a..e0dd4c5 100644
--- a/test/specs/cli.js
+++ b/test/specs/cli.js
@@ -169,6 +169,44 @@ describe('CLI-module', function() {
                 stdout.should.include('No errors found.');
                 stderr.should.equal('');
             });
+
+            describe('with OpenAPI v2', async function() {
+                it('should show error with providing the flag', async function() {
+                    const pathSchema = getPathOfTestData('v2/additional-properties/invalid-with-examples.yaml', true);
+                    try {
+                        await exec(`${CMD__RUN} -n ${pathSchema}`);
+                        should.fail('Expected to throw an error');
+                    } catch ({ stdout, stderr }) {
+                        stdout.should.equal(
+                            require('../data/output/api-additional-properties').value
+                        );
+                        stderr.should.equal(JSON.stringify(
+                            require('../data/v2/additional-properties/errors-schema-invalid-with-examples.json'),
+                            null,
+                            4
+                        ));
+                    }
+                });
+            });
+
+            describe('with OpenAPI v3', async function() {
+                it('should show error with providing the flag', async function() {
+                    const pathSchema = getPathOfTestData('v3/additional-properties/invalid-with-examples.yaml', true);
+                    try {
+                        await exec(`${CMD__RUN} -n ${pathSchema}`);
+                        should.fail('Expected to throw an error');
+                    } catch ({ stdout, stderr }) {
+                        stdout.should.equal(
+                            require('../data/output/api-additional-properties').value
+                        );
+                        stderr.should.equal(JSON.stringify(
+                            require('../data/v3/additional-properties/errors-schema-invalid-with-examples.json'),
+                            null,
+                            4
+                        ));
+                    }
+                });
+            });
         });
     });
     describe('with all properties required', function() {
diff --git a/test/specs/impl/service/schema-extender.js b/test/specs/impl/service/schema-extender.js
index cd158a2..101535c 100644
--- a/test/specs/impl/service/schema-extender.js
+++ b/test/specs/impl/service/schema-extender.js
@@ -39,8 +39,6 @@ function _getSchema() {
                             'description': 'An array of pets',
                             'content': {
                                 'application/json': {
-// MAYBE (not necessary, but doesn't break if it's included)
-// I haven't been able to write a JSONPath-query, to exclude it (`$..application/json.schema` for the type `array`)
                                     'schema': {
                                         'type': 'array',
 // TO BE FOUND
@@ -277,10 +275,10 @@ function _getSchema() {
 function _getPaths() {
     /* eslint-disable max-len */
     return [
-        "$['paths']['/pets']['get']['responses'][200]['content']['application/json']['schema']",
         "$['paths']['/pets']['get']['responses'][200]['content']['application/json']['schema']['items']",
         "$['paths']['/pets']['get']['responses'][200]['content']['application/json']['schema']['items']['properties']['schema']",
         "$['paths']['/pets']['get']['responses'][200]['content']['application/json']['schema']['items']['properties']['schema']['properties']['properties']",
+        "$['paths']['/pets']['get']['responses'][200]['content']['application/json']['schema']['items']['properties']['schema']['properties']['properties']['properties']['type']",
         "$['paths']['/pets']['get']['responses'][200]['content']['application/json']['schema']['items']['properties']['schema']['properties']['properties']['properties']['properties']",
         "$['paths']['/pets']['get']['responses']['default']['content']['application/json']['schema']",
         "$['paths']['/pets']['post']['responses']['default']['content']['application/json']['schema']",