Repository: stoplightio/spectral
Pull request #588 (2019-09-24 01:06:51)

feat(junit): include property path
Relates to https://github.com/stoplightio/spectral/issues/572

**Checklist**

- [x] Tests added / updated
- [x] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No




Patch:
diff --git a/src/formatters/junit.ts b/src/formatters/junit.ts
index 5811aaad..92a57daf 100644
--- a/src/formatters/junit.ts
+++ b/src/formatters/junit.ts
@@ -23,8 +23,9 @@
  * @author Jamund Ferguson
  */
 
+import { encodePointerFragment } from '@stoplight/json';
 import { extname } from '@stoplight/path';
-import { DiagnosticSeverity, Dictionary } from '@stoplight/types';
+import { DiagnosticSeverity, Dictionary, JsonPath } from '@stoplight/types';
 import { escapeRegExp } from 'lodash';
 import { IRuleResult, SpectralDiagnosticSeverity } from '../types';
 import { Formatter } from './types';
@@ -43,6 +44,10 @@ function getMessageType(result: IRuleResult) {
   return SEVERITY_MAP[result.severity];
 }
 
+function stringifyPath(path: JsonPath) {
+  return ['#', ...path.map(encodePointerFragment)].join('/');
+}
+
 export const junit: Formatter = results => {
   let output = '';
 
@@ -62,7 +67,7 @@ export const junit: Formatter = results => {
       for (const result of validationResults) {
         const tag = getMessageType(result).toLowerCase();
         output += `<testcase time="0" name="org.spectral.${result.code || 'unknown'}" classname="${classname}">`;
-        output += `<${tag} message="${xmlEscape(result.message)}">`;
+        output += `<${tag} message="${xmlEscape(result.message)}" path="${stringifyPath(result.path)}">`;
         output += '<![CDATA[';
         output += `line ${result.range.start.line + 1}, col `;
         output += `${result.range.start.character + 1}, ${getMessageType(result)}`;


Test patch:
diff --git a/src/formatters/__tests__/__fixtures__/mixed-errors-with-different-paths.json b/src/formatters/__tests__/__fixtures__/mixed-errors-with-different-paths.json
new file mode 100644
index 00000000..017825b9
--- /dev/null
+++ b/src/formatters/__tests__/__fixtures__/mixed-errors-with-different-paths.json
@@ -0,0 +1,79 @@
+[
+  {
+    "code": "info-contact",
+    "path": [],
+    "message": "Info object should contain `contact` object.",
+    "severity": 3,
+    "range": {
+      "start": {
+        "line": 2,
+        "character": 9
+      },
+      "end": {
+        "line": 6,
+        "character": 19
+      }
+    },
+    "source": "/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json"
+  },
+  {
+    "code": "info-description",
+    "path": [
+      ""
+    ],
+    "message": "OpenAPI object info `description` must be present and non-empty string.",
+    "severity": 1,
+    "range": {
+      "start": {
+        "line": 2,
+        "character": 9
+      },
+      "end": {
+        "line": 6,
+        "character": 19
+      }
+    },
+    "source": "/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json"
+  },
+  {
+    "code": "operation-description",
+    "path": [
+      "paths",
+      "/pets",
+      "post"
+    ],
+    "message": "Operation `description` must be present and non-empty string.",
+    "severity": 2,
+    "range": {
+      "start": {
+        "line": 63,
+        "character": 13
+      },
+      "end": {
+        "line": 76,
+        "character": 54
+      }
+    },
+    "source": "/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json"
+  },
+  {
+    "code": "info-matches-stoplight",
+    "path": [
+      "info",
+      "title"
+    ],
+    "message": "Info must contain Stoplight",
+    "severity": 0,
+    "range": {
+      "start": {
+        "line": 4,
+        "character": 13
+      },
+      "end": {
+        "line": 4,
+        "character": 31
+      }
+    },
+    "source": "/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json"
+  }
+]
diff --git a/src/formatters/__tests__/junit.test.ts b/src/formatters/__tests__/junit.test.ts
index cf58da27..5cec447b 100644
--- a/src/formatters/__tests__/junit.test.ts
+++ b/src/formatters/__tests__/junit.test.ts
@@ -2,7 +2,8 @@ import { promisify } from 'util';
 import { Parser } from 'xml2js';
 import { junit } from '../junit';
 
-const results = require('./__fixtures__/oas3-schema-errors.json');
+const oas3SchemaErrors = require('./__fixtures__/oas3-schema-errors.json');
+const mixedErrors = require('./__fixtures__/mixed-errors-with-different-paths.json');
 
 describe('JUnit formatter', () => {
   let parse: Parser['parseString'];
@@ -13,7 +14,7 @@ describe('JUnit formatter', () => {
   });
 
   test('should produce valid report', async () => {
-    const result = await parse(junit(results));
+    const result = await parse(junit(oas3SchemaErrors));
     expect(result).toEqual({
       testsuites: {
         testsuite: [
@@ -36,6 +37,7 @@ describe('JUnit formatter', () => {
                   {
                     $: {
                       message: 'should NOT have additional properties: type',
+                      path: '#/paths/~1pets/get/responses/200/headers/header-1',
                     },
                     _: 'line 36, col 22, Error - should NOT have additional properties: type (oas3-schema)',
                   },
@@ -51,6 +53,7 @@ describe('JUnit formatter', () => {
                   {
                     $: {
                       message: 'should match exactly one schema in oneOf',
+                      path: '#/paths/~1pets/get/responses/200/headers/header-1',
                     },
                     _: 'line 36, col 22, Error - should match exactly one schema in oneOf (oas3-schema)',
                   },
@@ -66,6 +69,7 @@ describe('JUnit formatter', () => {
                   {
                     $: {
                       message: "should have required property '$ref'",
+                      path: '#/paths/~1pets/get/responses/200/headers/header-1',
                     },
                     _: 'line 36, col 22, Error - should have required property &apos;$ref&apos; (oas3-schema)',
                   },
@@ -77,4 +81,77 @@ describe('JUnit formatter', () => {
       },
     });
   });
+
+  test('should produce valid paths', async () => {
+    const result = await parse(junit(mixedErrors));
+    expect(result).toEqual({
+      testsuites: {
+        testsuite: [
+          {
+            $: {
+              errors: '4',
+              name: '/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.oas3.json',
+              package: 'org.spectral',
+              tests: '4',
+              time: '0',
+            },
+            testcase: [
+              {
+                $: expect.objectContaining({
+                  name: 'org.spectral.info-contact',
+                }),
+                hint: [
+                  expect.objectContaining({
+                    $: {
+                      message: 'Info object should contain `contact` object.',
+                      path: '#',
+                    },
+                  }),
+                ],
+              },
+              {
+                $: expect.objectContaining({
+                  name: 'org.spectral.info-description',
+                }),
+                warning: [
+                  expect.objectContaining({
+                    $: {
+                      message: 'OpenAPI object info `description` must be present and non-empty string.',
+                      path: '#/',
+                    },
+                  }),
+                ],
+              },
+              {
+                $: expect.objectContaining({
+                  name: 'org.spectral.operation-description',
+                }),
+                information: [
+                  expect.objectContaining({
+                    $: {
+                      message: 'Operation `description` must be present and non-empty string.',
+                      path: '#/paths/~1pets/post',
+                    },
+                  }),
+                ],
+              },
+              {
+                $: expect.objectContaining({
+                  name: 'org.spectral.info-matches-stoplight',
+                }),
+                error: [
+                  expect.objectContaining({
+                    $: {
+                      message: 'Info must contain Stoplight',
+                      path: '#/info/title',
+                    },
+                  }),
+                ],
+              },
+            ],
+          },
+        ],
+      },
+    });
+  });
 });
diff --git a/test-harness/scenarios/results-format-junit.oas3.scenario b/test-harness/scenarios/results-format-junit.oas3.scenario
index d8cfe9b3..1a79b64a 100644
--- a/test-harness/scenarios/results-format-junit.oas3.scenario
+++ b/test-harness/scenarios/results-format-junit.oas3.scenario
@@ -14,9 +14,9 @@ OpenAPI 3.x detected
 <?xml version="1.0" encoding="utf-8"?>
 <testsuites>
 <testsuite package="org.spectral" time="0" tests="4" errors="4" name="{document}">
-<testcase time="0" name="org.spectral.api-servers" classname="{documentWithoutExt}"><warning message="OpenAPI `servers` must be present and non-empty array."><![CDATA[line 1, col 1, Warning - OpenAPI `servers` must be present and non-empty array. (api-servers)]]></warning></testcase>
-<testcase time="0" name="org.spectral.oas3-schema" classname="{documentWithoutExt}"><error message="/info Property foo is not expected to be here"><![CDATA[line 2, col 6, Error - /info Property foo is not expected to be here (oas3-schema)]]></error></testcase>
-<testcase time="0" name="org.spectral.info-contact" classname="{documentWithoutExt}"><warning message="Info object should contain `contact` object."><![CDATA[line 2, col 6, Warning - Info object should contain `contact` object. (info-contact)]]></warning></testcase>
-<testcase time="0" name="org.spectral.info-description" classname="{documentWithoutExt}"><warning message="OpenAPI object info `description` must be present and non-empty string."><![CDATA[line 2, col 6, Warning - OpenAPI object info `description` must be present and non-empty string. (info-description)]]></warning></testcase>
+<testcase time="0" name="org.spectral.api-servers" classname="{documentWithoutExt}"><warning message="OpenAPI `servers` must be present and non-empty array." path="#"><![CDATA[line 1, col 1, Warning - OpenAPI `servers` must be present and non-empty array. (api-servers)]]></warning></testcase>
+<testcase time="0" name="org.spectral.oas3-schema" classname="{documentWithoutExt}"><error message="/info Property foo is not expected to be here" path="#/info"><![CDATA[line 2, col 6, Error - /info Property foo is not expected to be here (oas3-schema)]]></error></testcase>
+<testcase time="0" name="org.spectral.info-contact" classname="{documentWithoutExt}"><warning message="Info object should contain `contact` object." path="#/info"><![CDATA[line 2, col 6, Warning - Info object should contain `contact` object. (info-contact)]]></warning></testcase>
+<testcase time="0" name="org.spectral.info-description" classname="{documentWithoutExt}"><warning message="OpenAPI object info `description` must be present and non-empty string." path="#/info"><![CDATA[line 2, col 6, Warning - OpenAPI object info `description` must be present and non-empty string. (info-description)]]></warning></testcase>
 </testsuite>
 </testsuites>