Repository: codekie/openapi-examples-validator
Pull request #120 (2020-12-18 11:32:01)

feat: fail on unhandled promise rejections
**Problem**

Any failures related to missing properties will throw exceptions that are not caught for rejected promises. These failures will (not yet) terminate the node process but instead continue as if nothing happened. Because these failures are valid for determining the validity of the schema's, the process should be terminated.

**Solution**

A straightforward solution is adding the `--unhandled-rejections=strict` flag so the node process will terminate on unhandled promise rejections. So this fix does just that by adding the `ENV NODE_OPTIONS="--unhandled-rejections=strict"` to the `build-dockerfile.js` script that generates the `Dockerfile`. 

But this is but a patch on the wound, the real fix is catching the exception that occurs when you parse the botched schema using `Ajv.compile`. So catching the error here and adding this to the error results will give the user proper feedback about what went wrong. The previously mentioned unhandled rejections flag will make sure that for other processes or internal errors the node process will still be terminated. 

**Preview**

Old:
```
(node:1) UnhandledPromiseRejectionWarning: Error: unknown format "timestamp" is used in schema at path "<redacted>/items/properties/change_timestamp"
    at Object.generate_format [as code] (/usr/local/lib/node_modules/openapi-examples-validator/node_modules/ajv/lib/dotjs/format.js:69:15)
    at Object.generate_validate [as validate] (/usr/local/lib/node_modules/openapi-examples-validator/node_modules/ajv/lib/dotjs/validate.js:374:35)
    at Object.generate_properties [as code] (/usr/local/lib/node_modules/openapi-examples-validator/node_modules/ajv/lib/dotjs/properties.js:201:26)
    at Object.generate_validate [as validate] (/usr/local/lib/node_modules/openapi-examples-validator/node_modules/ajv/lib/dotjs/validate.js:374:35)
    at Object.generate_items [as code] (/usr/local/lib/node_modules/openapi-examples-validator/node_modules/ajv/lib/dotjs/items.js:124:20)
    at Object.generate_validate [as validate] (/usr/local/lib/node_modules/openapi-examples-validator/node_modules/ajv/lib/dotjs/validate.js:374:35)
    at Object.generate_properties [as code] (/usr/local/lib/node_modules/openapi-examples-validator/node_modules/ajv/lib/dotjs/properties.js:201:26)
    at generate_validate (/usr/local/lib/node_modules/openapi-examples-validator/node_modules/ajv/lib/dotjs/validate.js:374:35)
    at localCompile (/usr/local/lib/node_modules/openapi-examples-validator/node_modules/ajv/lib/compile/index.js:88:22)
    at Ajv.compile (/usr/local/lib/node_modules/openapi-examples-validator/node_modules/ajv/lib/compile/index.js:55:13)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:1) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:1) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
```

New:
```
Validating examples
Schemas with examples found: 2
Examples without schema found: 0
Total examples found: 3

Errors found.

[
    {
        "type": "Validation",
        "message": "unknown format \"timestamp\" is used in schema at path \"<redacted>/items/properties/change_timestamp\"",
        "examplePath": "<redactedpath>"
    }
]
```

coveralls: 
[![Coverage Status](https://coveralls.io/builds/35819144/badge)](https://coveralls.io/builds/35819144)

Coverage decreased (-0.5%) to 97.135% when pulling **dc3fc7472dfe0d36747b719e19bb073d26419f11 on DerkSchooltink:DerkSchooltink/fail-on-unhandled-promise-rejections** into **7c9e74dcb1be1ea5638c87f366c8dc15c08f7e24 on codekie:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/35819144/badge)](https://coveralls.io/builds/35819144)

Coverage decreased (-0.5%) to 97.135% when pulling **dc3fc7472dfe0d36747b719e19bb073d26419f11 on DerkSchooltink:DerkSchooltink/fail-on-unhandled-promise-rejections** into **7c9e74dcb1be1ea5638c87f366c8dc15c08f7e24 on codekie:master**.

codekie: Thank you, for your contribution 👍  . I will look into it, after the christmas holidays.
codekie: lgtm 👍 

Patch:
diff --git a/etc/src/build-dockerfile.js b/etc/src/build-dockerfile.js
index ded8174..754fe48 100644
--- a/etc/src/build-dockerfile.js
+++ b/etc/src/build-dockerfile.js
@@ -18,6 +18,7 @@ FROM node:14.11.0-alpine3.12
 
 RUN npm install -g openapi-examples-validator@${ VERSION }
 
+ENV NODE_OPTIONS="--unhandled-rejections=strict"
 ENTRYPOINT ["openapi-examples-validator"]
 CMD ["--help"]
     `;
diff --git a/src/validator.js b/src/validator.js
index e099a3f..e668d75 100644
--- a/src/validator.js
+++ b/src/validator.js
@@ -46,7 +46,14 @@ function getValidatorFactory(specSchema, options) {
 function compileValidate(validator, responseSchema) {
     const preparedResponseSchema = _prepareResponseSchema(responseSchema, ID__RESPONSE_SCHEMA);
     _replaceRefsToPreparedSpecSchema(preparedResponseSchema);
-    return validator.compile(preparedResponseSchema);
+    let result;
+    try {
+        result = validator.compile(preparedResponseSchema);
+    } catch (e) {
+        result = () => {};
+        result.errors = [e];
+    }
+    return result;
 }
 
 /**


Test patch:
diff --git a/test/data/v2/invalid-format.json b/test/data/v2/invalid-format.json
new file mode 100644
index 0000000..3865081
--- /dev/null
+++ b/test/data/v2/invalid-format.json
@@ -0,0 +1,68 @@
+{
+  "swagger": "2.0",
+  "info": {
+    "title": "Simple API overview",
+    "version": "v2"
+  },
+  "paths": {
+    "/": {
+      "get": {
+        "operationId": "listVersionsv2",
+        "summary": "List API versions",
+        "produces": [
+          "application/json"
+        ],
+        "responses": {
+          "200": {
+            "description": "200 response",
+            "examples": {
+              "application/json": {
+                "versions": [
+                  {
+                    "status": "CURRENT",
+                    "updated": "2016-01-21T11:33:21Z",
+                    "occurred": "2020-01-01"
+                  }
+                ]
+              }
+            },
+            "schema": {
+              "type": "object",
+              "required": [
+                "versions"
+              ],
+              "properties": {
+                "versions": {
+                  "type": "array",
+                  "items": {
+                    "type": "object",
+                    "required": [
+                      "status",
+                      "id",
+                      "links"
+                    ],
+                    "properties": {
+                      "status": {
+                        "type": "string"
+                      },
+                      "updated": {
+                        "type": "string"
+                      },
+                      "occurred": {
+                        "type": "string",
+                        "format": "timestamp"
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "consumes": [
+    "application/json"
+  ]
+}
diff --git a/test/specs/cli.js b/test/specs/cli.js
index bd6b0e3..5b8454f 100644
--- a/test/specs/cli.js
+++ b/test/specs/cli.js
@@ -7,7 +7,7 @@ const util = require('util'),
         getPathOfTestData
     } = require('../util/setup-tests');
 
-const CMD__RUN = `node ${ require.resolve('../../src/cli.js') }`,   // Resolve path, for mutation-tests
+const CMD__RUN = `node ${require.resolve('../../src/cli.js')}`,   // Resolve path, for mutation-tests
     CMD__RUN_BUILT = 'node dist/cli.js',
     JSON_PATH__SCHEMA = '$.paths./.get.responses.200.schema';
 
@@ -23,14 +23,14 @@ describe('CLI-module', function() {
     describe('spawning a child process', function() {
         describe('version', function() {
             it('should print out the current version', async function() {
-                const { stdout, stderr } = await exec(`${ CMD__RUN } --version`);
-                stdout.should.equal(`${ VERSION }\n`);
+                const { stdout, stderr } = await exec(`${CMD__RUN} --version`);
+                stdout.should.equal(`${VERSION}\n`);
                 stderr.should.equal('');
             });
         });
         describe('help', function() {
             it('should show the right text', async function() {
-                const { stdout, stderr } = await exec(`${ CMD__RUN } --help`);
+                const { stdout, stderr } = await exec(`${CMD__RUN} --help`);
                 stdout.should.equal(_textHelp);
                 stderr.should.equal('');
             });
@@ -40,7 +40,7 @@ describe('CLI-module', function() {
                 const pathMapExamples = getPathOfTestData('v2/map-external-examples-relative-invalid'),
                     pathSchema = getPathOfTestData('v2/external-examples-schema');
                 try {
-                    await exec(`${ CMD__RUN } -m ${ pathMapExamples } -c ${ pathSchema }`);
+                    await exec(`${CMD__RUN} -m ${pathMapExamples} -c ${pathSchema}`);
                 } catch ({ stdout, stderr }) {
                     stdout.should.equal(_textMapExternalExamples);
                     stderr.should.not.equal('');
@@ -50,7 +50,7 @@ describe('CLI-module', function() {
                 const pathMapExamples = getPathOfTestData('v2/map-external-examples-relative-invalid'),
                     pathSchema = getPathOfTestData('v2/external-examples-schema');
                 try {
-                    await exec(`${ CMD__RUN_BUILT } -m ${ pathMapExamples } -c ${ pathSchema }`);
+                    await exec(`${CMD__RUN_BUILT} -m ${pathMapExamples} -c ${pathSchema}`);
                 } catch ({ stdout, stderr }) {
                     stdout.should.equal(_textMapExternalExamples);
                     stderr.should.not.equal('');
@@ -59,7 +59,7 @@ describe('CLI-module', function() {
         });
         describe('with valid examples', function() {
             it('should write to stdout, but not into stderr', async function() {
-                const { stdout, stderr } = await exec(`${ CMD__RUN } ${ getPathOfTestData('v2/simple-example') }`);
+                const { stdout, stderr } = await exec(`${CMD__RUN} ${getPathOfTestData('v2/simple-example')}`);
                 stdout.should.not.equal('');
                 stderr.should.equal('');
             });
@@ -67,19 +67,34 @@ describe('CLI-module', function() {
         describe('with invalid examples', function() {
             it('should write to stdout and stderr', async function() {
                 try {
-                    await exec(`${ CMD__RUN } ${ getPathOfTestData('v2/multiple-errors') }`);
+                    await exec(`${CMD__RUN} ${getPathOfTestData('v2/multiple-errors')}`);
                 } catch ({ stdout, stderr }) {
                     stdout.should.not.equal('');
                     stderr.should.not.equal('');
                 }
             });
+            it('should capture invalid references from ajv', async function() {
+                await exec(`${CMD__RUN} ${getPathOfTestData('v2/invalid-format')}`)
+                    .then(() => {
+                        // should never reach this
+                        throw { stdout: 'unreachable', stderr: 'unreachable' };
+                    })
+                    .catch(({ stdout, stderr }) => {
+                        stdout.should.contain('Errors found.', 'Stdout did not contain "Errors found." message');
+                        stderr.should.contain(
+                            'unknown format \\"timestamp\\" is used in schema at '
+                            + 'path \\"#/properties/versions/items/properties/occurred',
+                            'Stderr did not contain unknown format errors'
+                        );
+                    });
+            });
         });
         describe('single external example', function() {
             it('should have no error', async function() {
                 const pathMapExamples = getPathOfTestData('v2/external-examples-valid-example1'),
                     pathSchema = getPathOfTestData('v2/external-examples-schema');
                 const { stdout, stderr } = await exec(
-                    `${ CMD__RUN } -s ${ JSON_PATH__SCHEMA } -e ${ pathMapExamples } ${ pathSchema }`
+                    `${CMD__RUN} -s ${JSON_PATH__SCHEMA} -e ${pathMapExamples} ${pathSchema}`
                 );
                 stdout.should.not.equal('');
                 stderr.should.equal('');
@@ -88,14 +103,14 @@ describe('CLI-module', function() {
         describe('with additional properties', function() {
             it('should show no error without providing the flag', async function() {
                 const pathSchema = getPathOfTestData('v3/additional-properties/schema-with-examples.yaml', true);
-                const { stdout, stderr } = await exec(`${ CMD__RUN } ${ pathSchema }`);
+                const { stdout, stderr } = await exec(`${CMD__RUN} ${pathSchema}`);
                 stdout.should.not.equal('');
                 stderr.should.equal('');
             });
             it('should show error with providing the flag', async function() {
                 const pathSchema = getPathOfTestData('v3/additional-properties/schema-with-examples.yaml', true);
                 try {
-                    await exec(`${ CMD__RUN } -n ${ pathSchema }`);
+                    await exec(`${CMD__RUN} -n ${pathSchema}`);
                 } catch ({ stdout, stderr }) {
                     stdout.should.equal(require('../data/output/api-with-examples-and-additional-properties').value);
                     stderr.should.equal(JSON.stringify(
@@ -130,7 +145,8 @@ describe('CLI-module', function() {
             };
             this.origExit = process.exit;
             // Prevent commander from exiting the test-process
-            process.exit = () => {};
+            process.exit = () => {
+            };
         });
         after(function() {
             process.argv = this.origArgv;
@@ -150,7 +166,9 @@ describe('CLI-module', function() {
         it('should show the right help-text', async function() {
             process.argv = ['node', require.resolve('../../src/cli'), '--help'];
             // Hack, for `commander` to exit, after the help has been printed
-            process.exit = () => { throw new Error('Exited'); };
+            process.exit = () => {
+                throw new Error('Exited');
+            };
             try {
                 await require('../../src/cli');
             } catch (e) {