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

Issue:
Catch YAML-errors and map them into serializable `ApplicationErrors`
See #67 

Catch the error of the YAML-parser, in `_parseSpec(filePath)` (`src\index.js:221`) and throw a serializable `ParseError`

Pull request description:
Map yaml-parser errors to ApplicationErros
Closes #68 

Patch:
diff --git a/.gitignore b/.gitignore
index b661e2d..b729d86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@
 /.nyc_output
 /coverage
 /node_modules
+/tmp
\ No newline at end of file
diff --git a/src/application-error.js b/src/application-error.js
index bbf097b..1033377 100644
--- a/src/application-error.js
+++ b/src/application-error.js
@@ -33,6 +33,7 @@ const ErrorType = {
     jsENOENT: ENOENT.code,
     jsonPathNotFound: 'JsonPathNotFound',
     errorAndErrorsMutuallyExclusive: 'ErrorErrorsMutuallyExclusive',
+    parseError: 'ParseError',
     validation: 'Validation'
 };
 
diff --git a/src/index.js b/src/index.js
index bcfccac..0489ce5 100644
--- a/src/index.js
+++ b/src/index.js
@@ -217,9 +217,20 @@ async function validateExample(filePathSchema, pathSchema, filePathExample) {
  * @private
  */
 async function _parseSpec(filePath) {
-    const jsonSchema = _isFileTypeYaml(filePath)
-        ? yaml.parse(fs.readFileSync(filePath, 'utf-8'))
-        : JSON.parse(fs.readFileSync(filePath, 'utf-8'));
+    const isYaml = _isFileTypeYaml(filePath);
+    let jsonSchema;
+
+    if (isYaml) {
+        try {
+            jsonSchema = yaml.parse(fs.readFileSync(filePath, 'utf-8'));
+        } catch (e) {
+            const { name, message } = e;
+            throw new ApplicationError(ErrorType.parseError, { message: `${ name }: ${ message }` });
+        }
+    } else {
+        jsonSchema = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
+    }
+
     return await dereferenceJsonSchema(filePath, jsonSchema);
 }
 


Test patch:
diff --git a/test/data/v3/simple-api-with-examples-invalid-indentation.yml b/test/data/v3/simple-api-with-examples-invalid-indentation.yml
new file mode 100644
index 0000000..329f163
--- /dev/null
+++ b/test/data/v3/simple-api-with-examples-invalid-indentation.yml
@@ -0,0 +1,112 @@
+openapi: 3.0.0
+info:
+  version: 1.0.0
+  title: Swagger Petstore
+  license:
+    name: MIT
+servers:
+  - url: 'http://petstore.swagger.io/v1'
+paths:
+  /pets:
+    get:
+      summary: List all pets
+      operationId: listPets
+      tags:
+        - pets
+      parameters:
+        - name: limit
+          in: query
+          description: How many items to return at one time (max 100)
+          required: false
+          schema:
+            type: number
+                    responses:
+                      '200':
+                        description: An paged array of pets
+                        headers:
+                          x-next:
+                            description: A link to the next page of responses
+                            schema:
+                              type: string
+                        content:
+                          application/json:
+                            schema:
+                              $ref: '#/components/schemas/Pets'
+                            examples:
+                              single:
+                                value:
+                                  - id: 0
+                                    name: Herbert
+                                    tag: Doggo
+                      default:
+                        description: unexpected error
+                        content:
+                          application/json:
+                            schema:
+                              $ref: '#/components/schemas/Error'
+    post:
+      summary: Create a pet
+      operationId: createPets
+      tags:
+        - pets
+      responses:
+        '201':
+          description: Null response
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+  '/pets/{petId}':
+    get:
+      summary: Info for a specific pet
+      operationId: showPetById
+      tags:
+        - pets
+      parameters:
+        - name: petId
+          in: path
+          required: true
+          description: The id of the pet to retrieve
+          schema:
+            type: string
+      responses:
+        '200':
+          description: Expected response to a valid request
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Pets'
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+components:
+  schemas:
+    Pet:
+      required:
+        - id
+        - name
+      properties:
+        id:
+          type: number
+        name:
+          type: string
+        tag:
+          type: string
+    Pets:
+      type: array
+      items:
+        $ref: '#/components/schemas/Pet'
+    Error:
+      required:
+        - code
+        - message
+      properties:
+        code:
+          type: number
+        message:
+          type: string
diff --git a/test/specs/impl/v2/index-dereferencing.js b/test/specs/impl/v2/index-dereferencing.js
index b3322c5..6818738 100644
--- a/test/specs/impl/v2/index-dereferencing.js
+++ b/test/specs/impl/v2/index-dereferencing.js
@@ -1,5 +1,6 @@
 const path = require('path'),
-    { validateFile, validateExamplesByMap } = require('../../../../src/index');
+    { validateFile, validateExamplesByMap } = require('../../../../src/index'),
+    { normalizeValidationResultPaths } = require('../../../util/setup-tests');
 
 const FILE_PATH__DATA = path.join(__dirname, '..', '..', '..', 'data', 'v2'),
     FILE_PATH__EXTERNAL_EXAMPLES_SCHEMA = path.join(FILE_PATH__DATA,
@@ -57,7 +58,7 @@ describe('Main-module, for v2 should', () => {
             it('without changing the working directory', async() => {
                 const result = await validateExamplesByMap(FILE_PATH__EXTERNAL_EXAMPLES_SCHEMA,
                     FILE_PATH__EXTERNAL_EXAMPLES_MAP, { cwdToMappingFile: false });
-                result.should.deep.equal({
+                normalizeValidationResultPaths(result).should.deep.equal({
                     valid: false,
                     statistics: {
                         schemasWithExamples: 2,
@@ -75,7 +76,7 @@ describe('Main-module, for v2 should', () => {
                             params: {
                                 type: 'string'
                             },
-                            exampleFilePath: 'test/data/v2/external-examples-invalid-type.json',
+                            exampleFilePath: path.normalize('test/data/v2/external-examples-invalid-type.json'),
                             mapFilePath: FILE_PATH__EXTERNAL_EXAMPLES_MAP
                         },
                         {
@@ -87,7 +88,7 @@ describe('Main-module, for v2 should', () => {
                             params: {
                                 missingProperty: 'links'
                             },
-                            exampleFilePath: 'test/data/v2/external-examples-invalid-missing-link.json',
+                            exampleFilePath: path.normalize('test/data/v2/external-examples-invalid-missing-link.json'),
                             mapFilePath: FILE_PATH__EXTERNAL_EXAMPLES_MAP
                         }
                     ]
diff --git a/test/specs/impl/v2/index.js b/test/specs/impl/v2/index.js
index 342e310..6e1ec6e 100644
--- a/test/specs/impl/v2/index.js
+++ b/test/specs/impl/v2/index.js
@@ -1,7 +1,5 @@
 const path = require('path'),
-    {
-        loadTestData
-    } = require('../../../util/setup-tests'),
+    { loadTestData, normalizeValidationResultPaths } = require('../../../util/setup-tests'),
     { validateExample, 'default': validateExamples, validateExamplesByMap } = require('../../../../src/index'),
     { ApplicationError, ErrorType } = require('../../../../src/application-error');
 
@@ -161,6 +159,7 @@ describe('Main-module, for v2 should', () => {
         it('with an example-map', async() => {
             const result = await validateExamplesByMap(FILE_PATH__EXTERNAL_EXAMPLES_SCHEMA,
                 FILE_PATH__EXTERNAL_EXAMPLES_MAP);
+            normalizeValidationResultPaths(result);
             result.valid.should.equal(false);
             result.errors.should.deep.equal([
                 new ApplicationError(ErrorType.validation, {
@@ -172,7 +171,7 @@ describe('Main-module, for v2 should', () => {
                     },
                     schemaPath: '#/properties/versions/items/properties/id/type',
                     mapFilePath: FILE_PATH__EXTERNAL_EXAMPLES_MAP,
-                    exampleFilePath: 'test/data/v2/external-examples-invalid-type.json'
+                    exampleFilePath: path.normalize('test/data/v2/external-examples-invalid-type.json')
                 }),
                 new ApplicationError(ErrorType.validation, {
                     dataPath: '.versions[0]',
@@ -206,6 +205,7 @@ describe('Main-module, for v2 should', () => {
             it('referenced example-file in the mapping-file', async() => {
                 const result = await validateExamplesByMap(FILE_PATH__EXTERNAL_EXAMPLES_SCHEMA,
                     FILE_PATH__EXTERNAL_EXAMPLES_MAP_WITH_MISSING_EXAMPLE);
+                normalizeValidationResultPaths(result);
                 result.valid.should.equal(false);
                 result.errors.should.deep.equal([
                     new ApplicationError(ErrorType.jsENOENT, {
@@ -268,6 +268,7 @@ describe('Main-module, for v2 should', () => {
             it('while validating a map of external examples', async() => {
                 const result = await validateExamplesByMap(FILE_PATH__EXTERNAL_EXAMPLES_SCHEMA,
                     FILE_PATH__EXTERNAL_EXAMPLES_MAP_WITH_WRONG_SCHEMA_PATH);
+                normalizeValidationResultPaths(result);
                 result.valid.should.equal(false);
                 result.errors.should.deep.equal([
                     new ApplicationError(ErrorType.jsonPathNotFound, {
@@ -286,6 +287,7 @@ describe('Main-module, for v2 should', () => {
         it('and collect the errors for all mapping-files', async() => {
             const result = await validateExamplesByMap(FILE_PATH__EXTERNAL_EXAMPLES_SCHEMA,
                 FILE_PATH__EXTERNAL_EXAMPLES_GLOB);
+            normalizeValidationResultPaths(result);
             result.valid.should.equal(false);
             result.errors.should.deep.equal([
                 new ApplicationError(ErrorType.validation, {
diff --git a/test/specs/impl/v3/index.js b/test/specs/impl/v3/index.js
index a9002f4..07e039d 100644
--- a/test/specs/impl/v3/index.js
+++ b/test/specs/impl/v3/index.js
@@ -13,6 +13,8 @@ const JSON_PATH__CONTEXT_MUTUALLY_EXCLUSIVE = '/paths/~1pets/get/responses/200/c
     FILE_PATH__EXAMPLES_WITH_MISSING_SCHEMA
         = path.join(__dirname, '../../../data/v3/simple-api-with-examples-and-missing-schema.json'),
     FILE_PATH__INVALID__YAML = path.join(__dirname, '../../../data/v3/simple-api-with-examples-with-refs-invalid.yaml'),
+    FILE_PATH__INVALID__INDENTATION__YAML
+        = path.join(__dirname, '../../../data/v3/simple-api-with-examples-invalid-indentation.yml'),
     FILE_PATH__INVALID__YML = path.join(__dirname, '../../../data/v3/simple-api-with-examples-with-refs-invalid.yml'),
     FILE_PATH__VALID__REQUEST_PARAMETER = path.join(__dirname, '../../../data/v3/request-valid-parameter.json'),
     FILE_PATH__VALID__REQUEST_PARAMETER__EXAMPLES
@@ -93,6 +95,15 @@ describe('Main-module, for v3 should', function() {
                 (await validateFile(FILE_PATH__INVALID__YML)).valid.should.equal(false);
             });
         });
+        describe('containing an invalid yaml', function() {
+            it('with wrong indentation', async function() {
+                const errors = (await validateFile(FILE_PATH__INVALID__INDENTATION__YAML)).errors,
+                    error = errors[0];
+                errors.length.should.equal(1);
+                error.message.should.equal('YAMLSemanticError: Nested mappings are not allowed in compact mappings');
+                error.type.should.equal('ParseError');
+            });
+        });
     });
     describe('be able to validate request parameters', function() {
         describe('in example-property', function() {
diff --git a/test/util/setup-tests.js b/test/util/setup-tests.js
index 98e6fc4..1ccc7fe 100644
--- a/test/util/setup-tests.js
+++ b/test/util/setup-tests.js
@@ -13,7 +13,8 @@ const should = chai.should();
 module.exports = {
     should,
     getPathOfTestData,
-    loadTestData
+    loadTestData,
+    normalizeValidationResultPaths
 };
 
 // IMPLEMENTATION DETAILS
@@ -41,3 +42,18 @@ function loadTestData(fileName, asString) {
 function getPathOfTestData(fileName, asString) {
     return path.join(BASE_PATH__TEST_DATA_STRING, fileName + (!asString ? SUFFIX__JSON : ''));
 }
+
+/**
+ * Normalizes the file paths in the validation results. The passed result will be modified in place.
+ * @param {ValidationResult} validationResult   The modified validation-result
+ */
+function normalizeValidationResultPaths(validationResult) {
+    if (!validationResult.errors) { return validationResult; }
+    validationResult.errors.forEach(error => {
+        ['mapFilePath', 'exampleFilePath'].forEach(prop => {
+            if (!error[prop]) { return; }
+            error[prop] = path.normalize(error[prop]);
+        });
+    });
+    return validationResult;
+}
\ No newline at end of file