From 66243f7b8bb8a02bee28a2a0ca472ea4b7da0e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Sun, 18 Aug 2019 22:46:20 +0200 Subject: [PATCH 1/5] fix: do not generate non-existing paths --- src/__tests__/linter.test.ts | 2 +- src/cli/commands/__tests__/lint.test.ts | 6 +++--- src/linter.ts | 13 ++++++++++++- src/rulesets/oas/__tests__/contact-properties.ts | 6 +++--- src/rulesets/oas/__tests__/info-contact.ts | 2 +- src/rulesets/oas/__tests__/info-description.ts | 2 +- src/rulesets/oas/__tests__/info-license.ts | 2 +- src/rulesets/oas/__tests__/license-url.ts | 2 +- src/rulesets/oas/__tests__/openapi-tags.ts | 2 +- .../oas/__tests__/operation-default-response.ts | 2 +- src/rulesets/oas/__tests__/operation-description.ts | 2 +- src/rulesets/oas/__tests__/operation-operationId.ts | 2 +- src/rulesets/oas/__tests__/operation-tags.ts | 2 +- src/rulesets/oas/__tests__/parameter-description.ts | 6 +++--- src/rulesets/oas/__tests__/tag-description.ts | 2 +- src/rulesets/oas2/__tests__/api-host.ts | 2 +- src/rulesets/oas2/__tests__/api-schemes.ts | 2 +- src/rulesets/oas2/__tests__/model-description.ts | 2 +- src/rulesets/oas2/__tests__/valid-example.ts | 2 +- src/rulesets/oas3/__tests__/api-servers.ts | 2 +- src/rulesets/oas3/__tests__/model-description.ts | 2 +- 21 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/__tests__/linter.test.ts b/src/__tests__/linter.test.ts index 0383301e9..f9a22582d 100644 --- a/src/__tests__/linter.test.ts +++ b/src/__tests__/linter.test.ts @@ -106,7 +106,7 @@ describe('linter', () => { code: 'rule1', message, severity: DiagnosticSeverity.Warning, - path: ['responses', '404', 'description'], + path: ['responses', '404'], range: { end: { line: 6, diff --git a/src/cli/commands/__tests__/lint.test.ts b/src/cli/commands/__tests__/lint.test.ts index 39f11bc2e..5aba78236 100644 --- a/src/cli/commands/__tests__/lint.test.ts +++ b/src/cli/commands/__tests__/lint.test.ts @@ -341,7 +341,7 @@ describe('lint', () => { expect.objectContaining({ code: 'info-description', message: 'OpenAPI object info `description` must be present and non-empty string.', - path: ['info', 'description'], // todo: relative path or absolute path? there is no such path in linted ref, but there is such in spec when working on resolved file + path: ['info', 'description'], // todo: relative path or absolute path? there is no such path in linted file, but there is such in spec when working on resolved file range: { end: { character: 22, @@ -357,7 +357,7 @@ describe('lint', () => { expect.objectContaining({ code: 'api-schemes', message: 'OpenAPI host `schemes` must be present and non-empty array.', - path: ['schemes'], + path: [], range: expect.any(Object), source: expect.stringContaining('src/cli/commands/__tests__/__fixtures__/draft-ref.oas2.json'), }), @@ -404,7 +404,7 @@ describe('lint', () => { expect.objectContaining({ code: 'api-schemes', message: 'OpenAPI host `schemes` must be present and non-empty array.', - path: ['schemes'], + path: [], range: expect.any(Object), source: expect.stringContaining('src/cli/commands/__tests__/__fixtures__/draft-nested-ref.oas2.json'), }), diff --git a/src/linter.ts b/src/linter.ts index 0c5065705..9724150a1 100644 --- a/src/linter.ts +++ b/src/linter.ts @@ -95,7 +95,8 @@ export const lintNode = ( results = results.concat( targetResults.map(result => { - const path = (result.path || targetPath).map(segment => decodePointerFragment(String(segment))); + const escapedJsonPath = (result.path || targetPath).map(segment => decodePointerFragment(String(segment))); + const path = getRealJsonPath(resolved.result, escapedJsonPath); const location = resolved.getLocationForJsonPath(path, true); return { @@ -205,3 +206,13 @@ function keyAndOptionalPattern(key: string | number, pattern: string, value: any value, }; } + +function getRealJsonPath(data: unknown, path: JsonPath) { + if (data === null || typeof data !== 'object') return []; + + while (path.length > 0 && !has(data, path)) { + path.pop(); + } + + return path; +} diff --git a/src/rulesets/oas/__tests__/contact-properties.ts b/src/rulesets/oas/__tests__/contact-properties.ts index 7a4e018c3..0a0eee555 100644 --- a/src/rulesets/oas/__tests__/contact-properties.ts +++ b/src/rulesets/oas/__tests__/contact-properties.ts @@ -36,7 +36,7 @@ describe('contact-properties', () => { { code: 'contact-properties', message: 'Contact object should have `name`, `url` and `email`.', - path: ['info', 'contact', 'name'], + path: ['info', 'contact'], range: { end: { character: 17, @@ -53,7 +53,7 @@ describe('contact-properties', () => { { code: 'contact-properties', message: 'Contact object should have `name`, `url` and `email`.', - path: ['info', 'contact', 'url'], + path: ['info', 'contact'], range: { end: { character: 17, @@ -69,7 +69,7 @@ describe('contact-properties', () => { { code: 'contact-properties', message: 'Contact object should have `name`, `url` and `email`.', - path: ['info', 'contact', 'email'], + path: ['info', 'contact'], range: { end: { character: 17, diff --git a/src/rulesets/oas/__tests__/info-contact.ts b/src/rulesets/oas/__tests__/info-contact.ts index bbb55b520..d87a8664c 100644 --- a/src/rulesets/oas/__tests__/info-contact.ts +++ b/src/rulesets/oas/__tests__/info-contact.ts @@ -30,7 +30,7 @@ describe('info-contact', () => { { code: 'info-contact', message: 'Info object should contain `contact` object.', - path: ['info', 'contact'], + path: ['info'], range: { end: { character: 20, diff --git a/src/rulesets/oas/__tests__/info-description.ts b/src/rulesets/oas/__tests__/info-description.ts index 51b418343..c265f7c95 100644 --- a/src/rulesets/oas/__tests__/info-description.ts +++ b/src/rulesets/oas/__tests__/info-description.ts @@ -30,7 +30,7 @@ describe('info-description', () => { { code: 'info-description', message: 'OpenAPI object info `description` must be present and non-empty string.', - path: ['info', 'description'], + path: ['info'], range: { end: { character: 28, diff --git a/src/rulesets/oas/__tests__/info-license.ts b/src/rulesets/oas/__tests__/info-license.ts index 1f3d63449..0474605b6 100644 --- a/src/rulesets/oas/__tests__/info-license.ts +++ b/src/rulesets/oas/__tests__/info-license.ts @@ -34,7 +34,7 @@ describe('info-license', () => { { code: 'info-license', message: 'OpenAPI object info `license` must be present and non-empty string.', - path: ['info', 'license'], + path: ['info'], range: { end: { character: 28, diff --git a/src/rulesets/oas/__tests__/license-url.ts b/src/rulesets/oas/__tests__/license-url.ts index b73a64600..6e158be65 100644 --- a/src/rulesets/oas/__tests__/license-url.ts +++ b/src/rulesets/oas/__tests__/license-url.ts @@ -34,7 +34,7 @@ describe('license-url', () => { { code: 'license-url', message: 'License object should include `url`.', - path: ['info', 'license', 'url'], + path: ['info', 'license'], range: { end: { character: 19, diff --git a/src/rulesets/oas/__tests__/openapi-tags.ts b/src/rulesets/oas/__tests__/openapi-tags.ts index 3230fda81..dc35d97ee 100644 --- a/src/rulesets/oas/__tests__/openapi-tags.ts +++ b/src/rulesets/oas/__tests__/openapi-tags.ts @@ -29,7 +29,7 @@ describe('openapi-tags', () => { { code: 'openapi-tags', message: 'OpenAPI object should have non-empty `tags` array.', - path: ['tags'], + path: [], range: { end: { character: 13, diff --git a/src/rulesets/oas/__tests__/operation-default-response.ts b/src/rulesets/oas/__tests__/operation-default-response.ts index a1c1ebed1..fef8a430f 100644 --- a/src/rulesets/oas/__tests__/operation-default-response.ts +++ b/src/rulesets/oas/__tests__/operation-default-response.ts @@ -44,7 +44,7 @@ describe('operation-default-response', () => { { code: 'operation-default-response', message: 'Operations must have a default response.', - path: ['paths', '/path', '/get', 'responses', 'default'], + path: ['paths', '/path', '/get', 'responses'], range: { end: { character: 19, diff --git a/src/rulesets/oas/__tests__/operation-description.ts b/src/rulesets/oas/__tests__/operation-description.ts index 4db107d23..2ccb9a904 100644 --- a/src/rulesets/oas/__tests__/operation-description.ts +++ b/src/rulesets/oas/__tests__/operation-description.ts @@ -38,7 +38,7 @@ describe('operation-description', () => { { code: 'operation-description', message: 'Operation `description` must be present and non-empty string.', - path: ['paths', '/todos', 'get', 'description'], + path: ['paths', '/todos', 'get'], range: { end: { character: 15, diff --git a/src/rulesets/oas/__tests__/operation-operationId.ts b/src/rulesets/oas/__tests__/operation-operationId.ts index 4f721f88e..dc838f662 100644 --- a/src/rulesets/oas/__tests__/operation-operationId.ts +++ b/src/rulesets/oas/__tests__/operation-operationId.ts @@ -38,7 +38,7 @@ describe('operation-operationId', () => { { code: 'operation-operationId', message: 'Operation should have an `operationId`.', - path: ['paths', '/todos', 'get', 'operationId'], + path: ['paths', '/todos', 'get'], range: { end: { character: 15, diff --git a/src/rulesets/oas/__tests__/operation-tags.ts b/src/rulesets/oas/__tests__/operation-tags.ts index 2406782fd..145ae7864 100644 --- a/src/rulesets/oas/__tests__/operation-tags.ts +++ b/src/rulesets/oas/__tests__/operation-tags.ts @@ -38,7 +38,7 @@ describe('operation-tags', () => { { code: 'operation-tags', message: 'Operation should have non-empty `tags` array.', - path: ['paths', '/todos', 'get', 'tags'], + path: ['paths', '/todos', 'get'], range: { end: { character: 15, diff --git a/src/rulesets/oas/__tests__/parameter-description.ts b/src/rulesets/oas/__tests__/parameter-description.ts index 8cd481127..a22e35422 100644 --- a/src/rulesets/oas/__tests__/parameter-description.ts +++ b/src/rulesets/oas/__tests__/parameter-description.ts @@ -81,7 +81,7 @@ describe('parameter-description', () => { expect.objectContaining({ code: 'parameter-description', message: 'Parameter objects should have a `description`.', - path: ['parameters', 'limit', 'description'], + path: ['parameters', 'limit'], severity: 1, }), ]); @@ -106,7 +106,7 @@ describe('parameter-description', () => { expect.objectContaining({ code: 'parameter-description', message: 'Parameter objects should have a `description`.', - path: ['paths', '/todos', 'parameters', '0', 'description'], + path: ['paths', '/todos', 'parameters', '0'], severity: 1, }), ]); @@ -133,7 +133,7 @@ describe('parameter-description', () => { expect.objectContaining({ code: 'parameter-description', message: 'Parameter objects should have a `description`.', - path: ['paths', '/todos', 'get', 'parameters', '0', 'description'], + path: ['paths', '/todos', 'get', 'parameters', '0'], severity: 1, }), ]); diff --git a/src/rulesets/oas/__tests__/tag-description.ts b/src/rulesets/oas/__tests__/tag-description.ts index e010d2b80..2c213cd77 100644 --- a/src/rulesets/oas/__tests__/tag-description.ts +++ b/src/rulesets/oas/__tests__/tag-description.ts @@ -30,7 +30,7 @@ describe('tag-description', () => { { code: 'tag-description', message: 'Tag object should have a `description`.', - path: ['tags', '0', 'description'], + path: ['tags', '0'], range: { end: { character: 19, diff --git a/src/rulesets/oas2/__tests__/api-host.ts b/src/rulesets/oas2/__tests__/api-host.ts index 142c3b648..c4be44ddc 100644 --- a/src/rulesets/oas2/__tests__/api-host.ts +++ b/src/rulesets/oas2/__tests__/api-host.ts @@ -30,7 +30,7 @@ describe('api-host', () => { { code: 'api-host', message: 'OpenAPI `host` must be present and non-empty string.', - path: ['host'], + path: [], range: { end: { character: 13, diff --git a/src/rulesets/oas2/__tests__/api-schemes.ts b/src/rulesets/oas2/__tests__/api-schemes.ts index d8a698fa5..41cdaf380 100644 --- a/src/rulesets/oas2/__tests__/api-schemes.ts +++ b/src/rulesets/oas2/__tests__/api-schemes.ts @@ -29,7 +29,7 @@ describe('api-schemes', () => { { code: 'api-schemes', message: 'OpenAPI host `schemes` must be present and non-empty array.', - path: ['schemes'], + path: [], range: { end: { character: 13, diff --git a/src/rulesets/oas2/__tests__/model-description.ts b/src/rulesets/oas2/__tests__/model-description.ts index 791784423..6dd8e2598 100644 --- a/src/rulesets/oas2/__tests__/model-description.ts +++ b/src/rulesets/oas2/__tests__/model-description.ts @@ -36,7 +36,7 @@ describe('model-description', () => { { code: 'model-description', message: 'Definition `description` must be present and non-empty string.', - path: ['definitions', 'user', 'description'], + path: ['definitions', 'user'], range: { end: { character: 14, diff --git a/src/rulesets/oas2/__tests__/valid-example.ts b/src/rulesets/oas2/__tests__/valid-example.ts index a58f98e04..d62ebcec0 100644 --- a/src/rulesets/oas2/__tests__/valid-example.ts +++ b/src/rulesets/oas2/__tests__/valid-example.ts @@ -156,7 +156,7 @@ describe('valid-example', () => { expect.objectContaining({ code: 'valid-example', message: '"self" property type should be array', - path: ['definitions', 'halRoot', '_links', 'self'], + path: ['definitions', 'halRoot', 'example', '_links', 'self'], }), ]); }); diff --git a/src/rulesets/oas3/__tests__/api-servers.ts b/src/rulesets/oas3/__tests__/api-servers.ts index 660228ef6..785bad396 100644 --- a/src/rulesets/oas3/__tests__/api-servers.ts +++ b/src/rulesets/oas3/__tests__/api-servers.ts @@ -29,7 +29,7 @@ describe('api-servers', () => { { code: 'api-servers', message: 'OpenAPI `servers` must be present and non-empty array.', - path: ['servers'], + path: [], range: { end: { character: 13, diff --git a/src/rulesets/oas3/__tests__/model-description.ts b/src/rulesets/oas3/__tests__/model-description.ts index 7e00e66b0..8008aa1b7 100644 --- a/src/rulesets/oas3/__tests__/model-description.ts +++ b/src/rulesets/oas3/__tests__/model-description.ts @@ -40,7 +40,7 @@ describe('model-description', () => { { code: 'model-description', message: 'Model `description` must be present and non-empty string.', - path: ['components', 'schemas', 'user', 'description'], + path: ['components', 'schemas', 'user'], range: { end: { character: 16, From 20acd8e316917e8f27b03dbd43aec20a203724bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Sun, 18 Aug 2019 23:41:03 +0200 Subject: [PATCH 2/5] fix: include example path in valid-example --- src/__tests__/linter.test.ts | 8 ++++---- src/functions/__tests__/schema-path.test.ts | 4 ++-- src/functions/schema-path.ts | 7 +++++++ src/rulesets/oas2/__tests__/valid-example.ts | 3 ++- src/rulesets/oas3/__tests__/valid-example.ts | 6 +++--- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/__tests__/linter.test.ts b/src/__tests__/linter.test.ts index f9a22582d..1993bcf7d 100644 --- a/src/__tests__/linter.test.ts +++ b/src/__tests__/linter.test.ts @@ -565,8 +565,8 @@ responses:: !!foo }), expect.objectContaining({ code: 'valid-example', - message: '"foo" property type should be number', - path: ['components', 'schemas', 'foo'], + message: '"example" property type should be number', + path: ['components', 'schemas', 'foo', 'example'], }), expect.objectContaining({ code: 'oas3-schema', @@ -586,8 +586,8 @@ responses:: !!foo expect.arrayContaining([ expect.objectContaining({ code: 'valid-example', - message: '"schema" property can\'t resolve reference #/parameters/missing from id #', - path: ['paths', '/todos/{todoId}', 'put', 'parameters', '1', 'schema'], + message: '"example" property can\'t resolve reference #/parameters/missing from id #', + path: ['paths', '/todos/{todoId}', 'put', 'parameters', '1', 'schema', 'example'], }), ]), ); diff --git a/src/functions/__tests__/schema-path.test.ts b/src/functions/__tests__/schema-path.test.ts index d14f6aa06..67d199c1c 100644 --- a/src/functions/__tests__/schema-path.test.ts +++ b/src/functions/__tests__/schema-path.test.ts @@ -61,7 +61,7 @@ describe('schema', () => { }; expect(runSchemaPath(target, fieldToCheck, path)).toEqual([ { - path: [], + path: ['example'], message: "should have required property 'url'", }, ]); @@ -79,7 +79,7 @@ describe('schema', () => { expect(runSchemaPath(target, fieldToCheck, path)).toEqual([ { message: 'format should match format "url"', - path: [], + path: ['example'], }, ]); }); diff --git a/src/functions/schema-path.ts b/src/functions/schema-path.ts index e3561057e..9f4c3c918 100644 --- a/src/functions/schema-path.ts +++ b/src/functions/schema-path.ts @@ -29,5 +29,12 @@ export const schemaPath: IFunction = (targetVal, opts, paths console.error(error); } + if (opts.field) { + paths.given.push(opts.field); + if (paths.target) { + paths.target.push(opts.field); + } + } + return schema(relevantObject, { schema: schemaObject }, paths, otherValues); }; diff --git a/src/rulesets/oas2/__tests__/valid-example.ts b/src/rulesets/oas2/__tests__/valid-example.ts index d62ebcec0..45ba3e8ae 100644 --- a/src/rulesets/oas2/__tests__/valid-example.ts +++ b/src/rulesets/oas2/__tests__/valid-example.ts @@ -222,7 +222,7 @@ describe('valid-example', () => { expect(results).toEqual([ expect.objectContaining({ code: 'valid-example', - message: '"c" property type should be string', + message: '"example" property type should be string', path: [ 'paths', '/pet', @@ -236,6 +236,7 @@ describe('valid-example', () => { 'b', 'properties', 'c', + 'example', ], range: expect.any(Object), severity: DiagnosticSeverity.Warning, diff --git a/src/rulesets/oas3/__tests__/valid-example.ts b/src/rulesets/oas3/__tests__/valid-example.ts index 6b7e0170f..4369b4eac 100644 --- a/src/rulesets/oas3/__tests__/valid-example.ts +++ b/src/rulesets/oas3/__tests__/valid-example.ts @@ -125,8 +125,8 @@ describe('valid-example', () => { expect(results).toEqual([ expect.objectContaining({ code: 'valid-example', - message: '"schema" property format should match format "email"', - path: ['paths', '/pet', 'post', 'requestBody', 'content', '*/*', 'schema'], + message: '"example" property format should match format "email"', + path: ['paths', '/pet', 'post', 'requestBody', 'content', '*/*', 'schema', 'example'], range: expect.any(Object), severity: DiagnosticSeverity.Warning, }), @@ -169,7 +169,7 @@ describe('valid-example', () => { expect(results).toEqual([ expect.objectContaining({ code: 'valid-example', - message: `"ip_address" property format should match format "${format}"`, + message: `"example" property format should match format "${format}"`, // hm, ip_address is likely to be more meaningful no? }), ]); }, From c2879766f4586cb8a3c3f4371f6c2e8cbc05dcf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Tue, 20 Aug 2019 09:36:37 +0200 Subject: [PATCH 3/5] feat: nicer messages for valid-example rule --- src/__tests__/linter.test.ts | 4 ++-- src/functions/__tests__/schema-path.test.ts | 4 ++-- src/functions/schema-path.ts | 22 ++++++++++++++++---- src/rulesets/oas2/__tests__/valid-example.ts | 4 ++-- src/rulesets/oas2/index.json | 2 +- src/rulesets/oas3/__tests__/valid-example.ts | 4 ++-- src/rulesets/oas3/index.json | 2 +- 7 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/__tests__/linter.test.ts b/src/__tests__/linter.test.ts index 1993bcf7d..1e3746c9c 100644 --- a/src/__tests__/linter.test.ts +++ b/src/__tests__/linter.test.ts @@ -565,7 +565,7 @@ responses:: !!foo }), expect.objectContaining({ code: 'valid-example', - message: '"example" property type should be number', + message: '"foo.example" property type should be number', path: ['components', 'schemas', 'foo', 'example'], }), expect.objectContaining({ @@ -586,7 +586,7 @@ responses:: !!foo expect.arrayContaining([ expect.objectContaining({ code: 'valid-example', - message: '"example" property can\'t resolve reference #/parameters/missing from id #', + message: '"schema.example" property can\'t resolve reference #/parameters/missing from id #', path: ['paths', '/todos/{todoId}', 'put', 'parameters', '1', 'schema', 'example'], }), ]), diff --git a/src/functions/__tests__/schema-path.test.ts b/src/functions/__tests__/schema-path.test.ts index 67d199c1c..9fa457f78 100644 --- a/src/functions/__tests__/schema-path.test.ts +++ b/src/functions/__tests__/schema-path.test.ts @@ -62,7 +62,7 @@ describe('schema', () => { expect(runSchemaPath(target, fieldToCheck, path)).toEqual([ { path: ['example'], - message: "should have required property 'url'", + message: `"example" property should have required property 'url'`, }, ]); }); @@ -78,7 +78,7 @@ describe('schema', () => { expect(runSchemaPath(target, fieldToCheck, path)).toEqual([ { - message: 'format should match format "url"', + message: '"example" property format should match format "url"', path: ['example'], }, ]); diff --git a/src/functions/schema-path.ts b/src/functions/schema-path.ts index 9f4c3c918..ae11900a6 100644 --- a/src/functions/schema-path.ts +++ b/src/functions/schema-path.ts @@ -20,6 +20,10 @@ export const schemaPath: IFunction = (targetVal, opts, paths // The subsection of the targetVal which contains the good bit const relevantObject = opts.field ? object[opts.field] : object; if (!relevantObject) return []; + const { target, given } = paths; + const lastItem = target + ? target.length > 0 && target[target.length - 1] + : given.length > 0 && given[given.length - 1]; // The subsection of the targetValue which contains the schema for us to validate the good bit against let schemaObject; @@ -30,11 +34,21 @@ export const schemaPath: IFunction = (targetVal, opts, paths } if (opts.field) { - paths.given.push(opts.field); - if (paths.target) { - paths.target.push(opts.field); + given.push(opts.field); + if (target) { + target.push(opts.field); } } - return schema(relevantObject, { schema: schemaObject }, paths, otherValues); + const errors = schema(relevantObject, { schema: schemaObject }, paths, otherValues); + return ( + errors && + errors.map(error => { + const propertyPath = [lastItem, opts.field].filter(Boolean).join('.'); + return { + ...error, + message: `${propertyPath ? `"${propertyPath}"` : ''} property ${error.message}`, + }; + }) + ); }; diff --git a/src/rulesets/oas2/__tests__/valid-example.ts b/src/rulesets/oas2/__tests__/valid-example.ts index 45ba3e8ae..62aa78589 100644 --- a/src/rulesets/oas2/__tests__/valid-example.ts +++ b/src/rulesets/oas2/__tests__/valid-example.ts @@ -155,7 +155,7 @@ describe('valid-example', () => { expect(results).toEqual([ expect.objectContaining({ code: 'valid-example', - message: '"self" property type should be array', + message: '"halRoot.example" property type should be array', path: ['definitions', 'halRoot', 'example', '_links', 'self'], }), ]); @@ -222,7 +222,7 @@ describe('valid-example', () => { expect(results).toEqual([ expect.objectContaining({ code: 'valid-example', - message: '"example" property type should be string', + message: '"c.example" property type should be string', path: [ 'paths', '/pet', diff --git a/src/rulesets/oas2/index.json b/src/rulesets/oas2/index.json index 38a03a534..9729fc45e 100644 --- a/src/rulesets/oas2/index.json +++ b/src/rulesets/oas2/index.json @@ -92,7 +92,7 @@ }, "valid-example": { "description": "Examples must be valid against their defined schema.", - "message": "\"{{property}}\" property {{error}}", + "message": "{{error}}", "recommended": true, "type": "validation", "given": "$..[?(@.example)]", diff --git a/src/rulesets/oas3/__tests__/valid-example.ts b/src/rulesets/oas3/__tests__/valid-example.ts index 4369b4eac..de23bd2ff 100644 --- a/src/rulesets/oas3/__tests__/valid-example.ts +++ b/src/rulesets/oas3/__tests__/valid-example.ts @@ -125,7 +125,7 @@ describe('valid-example', () => { expect(results).toEqual([ expect.objectContaining({ code: 'valid-example', - message: '"example" property format should match format "email"', + message: '"schema.example" property format should match format "email"', path: ['paths', '/pet', 'post', 'requestBody', 'content', '*/*', 'schema', 'example'], range: expect.any(Object), severity: DiagnosticSeverity.Warning, @@ -169,7 +169,7 @@ describe('valid-example', () => { expect(results).toEqual([ expect.objectContaining({ code: 'valid-example', - message: `"example" property format should match format "${format}"`, // hm, ip_address is likely to be more meaningful no? + message: `"ip_address.example" property format should match format "${format}"`, // hm, ip_address is likely to be more meaningful no? }), ]); }, diff --git a/src/rulesets/oas3/index.json b/src/rulesets/oas3/index.json index e2b63730c..0dbfaee5d 100644 --- a/src/rulesets/oas3/index.json +++ b/src/rulesets/oas3/index.json @@ -80,7 +80,7 @@ }, "valid-example": { "description": "Examples must be valid against their defined schema.", - "message": "\"{{property}}\" property {{error}}", + "message": "{{error}}", "recommended": true, "type": "validation", "given": "$..[?(@.example)]", From a1336fca1913d67ec3cbb6687cd17db03f70a08c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Tue, 20 Aug 2019 10:01:40 +0200 Subject: [PATCH 4/5] test: borrow mocking from feat/custom-functions branch --- karma.conf.ts | 5 +- setupKarma.ts | 44 +++++++++ src/__tests__/spectral.jest.test.ts | 47 ---------- src/__tests__/spectral.karma.test.ts | 98 +++------------------ src/__tests__/spectral.test.ts | 53 ++++++++++- src/rulesets/__tests__/reader.jest.test.ts | 26 ------ src/rulesets/__tests__/reader.karma.test.ts | 74 ---------------- src/rulesets/__tests__/reader.test.ts | 29 ++++++ tsconfig.json | 2 +- tsconfig.karma.json | 17 ++++ 10 files changed, 156 insertions(+), 239 deletions(-) create mode 100644 setupKarma.ts delete mode 100644 src/rulesets/__tests__/reader.karma.test.ts create mode 100644 src/rulesets/__tests__/reader.test.ts create mode 100644 tsconfig.karma.json diff --git a/karma.conf.ts b/karma.conf.ts index 8ab0dda99..80c41c848 100644 --- a/karma.conf.ts +++ b/karma.conf.ts @@ -11,7 +11,7 @@ module.exports = (config: any) => { frameworks: ['jasmine', 'karma-typescript'], // list of files / patterns to load in the browser - files: ['./karma-jest.ts', 'src/**/*.ts'], + files: ['./karma-jest.ts', './setupKarma.ts', 'src/**/*.ts'], // list of files / patterns to exclude // unit tests are excluded because most of them make use of jest.mock @@ -22,10 +22,11 @@ module.exports = (config: any) => { preprocessors: { 'src/**/*.ts': ['karma-typescript'], './karma-jest.ts': ['karma-typescript'], + './setupKarma.ts': ['karma-typescript'], }, karmaTypescriptConfig: { - ...require('./tsconfig.json'), + ...require('./tsconfig.karma.json'), bundlerOptions: { resolve: { alias: { diff --git a/setupKarma.ts b/setupKarma.ts new file mode 100644 index 000000000..ce8251305 --- /dev/null +++ b/setupKarma.ts @@ -0,0 +1,44 @@ +import { FetchMockSandbox } from 'fetch-mock'; + +const oasRuleset = JSON.parse(JSON.stringify(require('./rulesets/oas/index.json'))); +const oas2Ruleset = JSON.parse(JSON.stringify(require('./rulesets/oas2/index.json'))); +const oas2Schema = JSON.parse(JSON.stringify(require('./rulesets/oas2/schemas/main.json'))); +const oas3Ruleset = JSON.parse(JSON.stringify(require('./rulesets/oas3/index.json'))); +const oas3Schema = JSON.parse(JSON.stringify(require('./rulesets/oas3/schemas/main.json'))); + +const { fetch } = window; +let fetchMock: FetchMockSandbox; + +beforeEach(() => { + fetchMock = require('fetch-mock').sandbox(); + window.fetch = fetchMock; + + fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas/index.json', { + status: 200, + body: JSON.parse(JSON.stringify(oasRuleset)), + }); + + fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas2/index.json', { + status: 200, + body: JSON.parse(JSON.stringify(oas2Ruleset)), + }); + + fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas3/index.json', { + status: 200, + body: JSON.parse(JSON.stringify(oas3Ruleset)), + }); + + fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas2/schemas/main.json', { + status: 200, + body: JSON.parse(JSON.stringify(oas2Schema)), + }); + + fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas3/schemas/main.json', { + status: 200, + body: JSON.parse(JSON.stringify(oas3Schema)), + }); +}); + +afterEach(() => { + window.fetch = fetch; +}); diff --git a/src/__tests__/spectral.jest.test.ts b/src/__tests__/spectral.jest.test.ts index 9abe711e3..ee02dde23 100644 --- a/src/__tests__/spectral.jest.test.ts +++ b/src/__tests__/spectral.jest.test.ts @@ -4,8 +4,6 @@ import * as path from 'path'; import { Spectral } from '../spectral'; const oasRuleset = require('../rulesets/oas/index.json'); -const oas2Ruleset = require('../rulesets/oas2/index.json'); -const oas3Ruleset = require('../rulesets/oas3/index.json'); const customOASRuleset = require('./__fixtures__/custom-oas-ruleset.json'); describe('Spectral', () => { @@ -14,51 +12,6 @@ describe('Spectral', () => { }); describe('loadRuleset', () => { - test('should support loading built-in rulesets', async () => { - const s = new Spectral(); - await s.loadRuleset('spectral:oas2'); - - expect(s.rules).toEqual( - [...Object.entries(oasRuleset.rules), ...Object.entries(oas2Ruleset.rules)].reduce>( - (oasRules, [name, rule]) => { - oasRules[name] = { - name, - ...rule, - formats: expect.arrayContaining([expect.any(String)]), - severity: expect.any(Number), - then: expect.any(Object), - }; - - return oasRules; - }, - {}, - ), - ); - }); - - test('should support loading multiple built-in rulesets', async () => { - const s = new Spectral(); - await s.loadRuleset('spectral:oas2', 'spectral:oas3'); - - expect(s.rules).toEqual( - [ - ...Object.entries(oasRuleset.rules), - ...Object.entries(oas2Ruleset.rules), - ...Object.entries(oas3Ruleset.rules), - ].reduce>((oasRules, [name, rule]) => { - oasRules[name] = { - name, - ...rule, - formats: expect.arrayContaining([expect.any(String)]), - severity: expect.any(Number), - then: expect.any(Object), - }; - - return oasRules; - }, {}), - ); - }); - test('should support loading rulesets from filesystem', async () => { const s = new Spectral(); await s.loadRuleset(path.join(__dirname, '__fixtures__/custom-oas-ruleset.json')); diff --git a/src/__tests__/spectral.karma.test.ts b/src/__tests__/spectral.karma.test.ts index 73d5d44d6..168e6016f 100644 --- a/src/__tests__/spectral.karma.test.ts +++ b/src/__tests__/spectral.karma.test.ts @@ -1,98 +1,20 @@ -import { DiagnosticSeverity, Dictionary } from '@stoplight/types'; +import { DiagnosticSeverity } from '@stoplight/types'; import { FetchMockSandbox } from 'fetch-mock'; import { Spectral } from '../spectral'; -const { fetch } = window; - -const oasRuleset = require('../rulesets/oas/index.json'); -const oas2Ruleset = require('../rulesets/oas2/index.json'); -const oas2Schema = require('../rulesets/oas2/schemas/main.json'); -const oas3Ruleset = require('../rulesets/oas3/index.json'); -const oas3Schema = require('../rulesets/oas3/schemas/main.json'); - describe('Spectral', () => { - describe('loadRuleset', () => { - let fetchMock: FetchMockSandbox; - - beforeEach(() => { - fetchMock = require('fetch-mock').sandbox(); - window.fetch = fetchMock; - - fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas/index.json', { - status: 200, - body: oasRuleset, - }); - - fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas2/index.json', { - status: 200, - body: oas2Ruleset, - }); - - fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas3/index.json', { - status: 200, - body: oas3Ruleset, - }); - - fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas2/schemas/main.json', { - status: 200, - body: oas2Schema, - }); - - fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas3/schemas/main.json', { - status: 200, - body: oas3Schema, - }); - }); - - afterEach(() => { - window.fetch = fetch; - }); + let fetchMock: FetchMockSandbox; - test('should support loading built-in rulesets', async () => { - const s = new Spectral(); - await s.loadRuleset('spectral:oas2'); - - expect(s.rules).toEqual( - [...Object.entries(oasRuleset.rules), ...Object.entries(oas2Ruleset.rules)].reduce>( - (oasRules, [name, rule]) => { - oasRules[name] = { - name, - ...rule, - formats: expect.arrayContaining([expect.any(String)]), - severity: expect.any(Number), - then: expect.any(Object), - }; - - return oasRules; - }, - {}, - ), - ); - }); - - test('should support loading multiple built-in rulesets', async () => { - const s = new Spectral(); - await s.loadRuleset('spectral:oas2', 'spectral:oas3'); - - expect(s.rules).toEqual( - [ - ...Object.entries(oasRuleset.rules), - ...Object.entries(oas2Ruleset.rules), - ...Object.entries(oas3Ruleset.rules), - ].reduce>((oasRules, [name, rule]) => { - oasRules[name] = { - name, - ...rule, - formats: expect.arrayContaining([expect.any(String)]), - severity: expect.any(Number), - then: expect.any(Object), - }; + beforeEach(() => { + fetchMock = require('fetch-mock').sandbox(); + window.fetch = fetchMock; + }); - return oasRules; - }, {}), - ); - }); + afterEach(() => { + window.fetch = fetch; + }); + describe('loadRuleset', () => { test('should support loading rulesets over http', async () => { const ruleset = { rules: { diff --git a/src/__tests__/spectral.test.ts b/src/__tests__/spectral.test.ts index 49d8b212b..a925a5a44 100644 --- a/src/__tests__/spectral.test.ts +++ b/src/__tests__/spectral.test.ts @@ -1,10 +1,61 @@ -import { DiagnosticSeverity } from '@stoplight/types'; +import { DiagnosticSeverity, Dictionary } from '@stoplight/types'; import { isParsedResult, Spectral } from '../spectral'; import { IParsedResult, RuleFunction } from '../types'; const merge = require('lodash/merge'); +const oasRuleset = JSON.parse(JSON.stringify(require('../rulesets/oas/index.json'))); +const oas2Ruleset = JSON.parse(JSON.stringify(require('../rulesets/oas2/index.json'))); +const oas3Ruleset = JSON.parse(JSON.stringify(require('../rulesets/oas3/index.json'))); + describe('spectral', () => { + describe('loadRuleset', () => { + test('should support loading built-in rulesets', async () => { + const s = new Spectral(); + await s.loadRuleset('spectral:oas2'); + + expect(s.rules).toEqual( + [...Object.entries(oasRuleset.rules), ...Object.entries(oas2Ruleset.rules)].reduce>( + (oasRules, [name, rule]) => { + oasRules[name] = { + name, + ...rule, + formats: expect.arrayContaining([expect.any(String)]), + severity: expect.any(Number), + then: expect.any(Object), + }; + + return oasRules; + }, + {}, + ), + ); + }); + + test('should support loading multiple built-in rulesets', async () => { + const s = new Spectral(); + await s.loadRuleset('spectral:oas2', 'spectral:oas3'); + + expect(s.rules).toEqual( + [ + ...Object.entries(oasRuleset.rules), + ...Object.entries(oas2Ruleset.rules), + ...Object.entries(oas3Ruleset.rules), + ].reduce>((oasRules, [name, rule]) => { + oasRules[name] = { + name, + ...rule, + formats: expect.arrayContaining([expect.any(String)]), + severity: expect.any(Number), + then: expect.any(Object), + }; + + return oasRules; + }, {}), + ); + }); + }); + describe('addRules & mergeRules', () => { test('should not mutate the passing in rules object', () => { const givenCustomRuleSet = { diff --git a/src/rulesets/__tests__/reader.jest.test.ts b/src/rulesets/__tests__/reader.jest.test.ts index 050134673..06f579029 100644 --- a/src/rulesets/__tests__/reader.jest.test.ts +++ b/src/rulesets/__tests__/reader.jest.test.ts @@ -341,32 +341,6 @@ describe('Rulesets reader', () => { }); }); - it('should resolve oas2-schema', async () => { - const rules = await readRulesFromRulesets('spectral:oas2'); - expect(rules['oas2-schema']).not.toHaveProperty('then.functionOptions.schema.$ref'); - expect(rules['oas2-schema']).toHaveProperty( - 'then.functionOptions.schema', - expect.objectContaining({ - title: 'A JSON Schema for Swagger 2.0 API.', - id: 'http://swagger.io/v2/schema.json#', - $schema: 'http://json-schema.org/draft-04/schema#', - }), - ); - }); - - it('should resolve oas3-schema', async () => { - const rules = await readRulesFromRulesets('spectral:oas3'); - expect(rules['oas3-schema']).not.toHaveProperty('then.functionOptions.schema.$ref'); - expect(rules['oas3-schema']).toHaveProperty( - 'then.functionOptions.schema', - expect.objectContaining({ - id: 'https://spec.openapis.org/oas/3.0/schema/2019-04-02', - $schema: 'http://json-schema.org/draft-04/schema#', - description: 'Validation schema for OpenAPI Specification 3.0.X.', - }), - ); - }); - it('given non-existent ruleset should output error', () => { nock('https://unpkg.com') .get('/oneParentRuleset') diff --git a/src/rulesets/__tests__/reader.karma.test.ts b/src/rulesets/__tests__/reader.karma.test.ts deleted file mode 100644 index c4bdfb5a9..000000000 --- a/src/rulesets/__tests__/reader.karma.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { FetchMockSandbox } from 'fetch-mock'; -import { readRulesFromRulesets } from '../reader'; - -const { fetch } = window; - -const oasRuleset = require('../oas/index.json'); -const oas2Ruleset = require('../oas2/index.json'); -const oas2Schema = require('../oas2/schemas/main.json'); -const oas3Ruleset = require('../oas3/index.json'); -const oas3Schema = require('../oas3/schemas/main.json'); - -describe('Rulesets reader', () => { - let fetchMock: FetchMockSandbox; - - beforeEach(() => { - fetchMock = require('fetch-mock').sandbox(); - window.fetch = fetchMock; - - fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas/index.json', { - status: 200, - body: oasRuleset, - }); - - fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas2/index.json', { - status: 200, - body: oas2Ruleset, - }); - - fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas3/index.json', { - status: 200, - body: oas3Ruleset, - }); - - fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas2/schemas/main.json', { - status: 200, - body: oas2Schema, - }); - - fetchMock.get('https://unpkg.com/@stoplight/spectral/rulesets/oas3/schemas/main.json', { - status: 200, - body: oas3Schema, - }); - }); - - afterEach(() => { - window.fetch = fetch; - }); - - it('should resolve oas2-schema', async () => { - const rules = await readRulesFromRulesets('spectral:oas2'); - expect(rules['oas2-schema']).not.toHaveProperty('then.functionOptions.schema.$ref'); - expect(rules['oas2-schema']).toHaveProperty( - 'then.functionOptions.schema', - expect.objectContaining({ - title: 'A JSON Schema for Swagger 2.0 API.', - id: 'http://swagger.io/v2/schema.json#', - $schema: 'http://json-schema.org/draft-04/schema#', - }), - ); - }); - - it('should resolve oas3-schema', async () => { - const rules = await readRulesFromRulesets('spectral:oas3'); - expect(rules['oas3-schema']).not.toHaveProperty('then.functionOptions.schema.$ref'); - expect(rules['oas3-schema']).toHaveProperty( - 'then.functionOptions.schema', - expect.objectContaining({ - id: 'https://spec.openapis.org/oas/3.0/schema/2019-04-02', - $schema: 'http://json-schema.org/draft-04/schema#', - description: 'Validation schema for OpenAPI Specification 3.0.X.', - }), - ); - }); -}); diff --git a/src/rulesets/__tests__/reader.test.ts b/src/rulesets/__tests__/reader.test.ts new file mode 100644 index 000000000..1541c474a --- /dev/null +++ b/src/rulesets/__tests__/reader.test.ts @@ -0,0 +1,29 @@ +import { readRulesFromRulesets } from '../reader'; + +describe('Rulesets reader', () => { + it('should resolve oas2-schema', async () => { + const rules = await readRulesFromRulesets('spectral:oas2'); + expect(rules['oas2-schema']).not.toHaveProperty('then.functionOptions.schema.$ref'); + expect(rules['oas2-schema']).toHaveProperty( + 'then.functionOptions.schema', + expect.objectContaining({ + title: 'A JSON Schema for Swagger 2.0 API.', + id: 'http://swagger.io/v2/schema.json#', + $schema: 'http://json-schema.org/draft-04/schema#', + }), + ); + }); + + it('should resolve oas3-schema', async () => { + const rules = await readRulesFromRulesets('spectral:oas3'); + expect(rules['oas3-schema']).not.toHaveProperty('then.functionOptions.schema.$ref'); + expect(rules['oas3-schema']).toHaveProperty( + 'then.functionOptions.schema', + expect.objectContaining({ + id: 'https://spec.openapis.org/oas/3.0/schema/2019-04-02', + $schema: 'http://json-schema.org/draft-04/schema#', + description: 'Validation schema for OpenAPI Specification 3.0.X.', + }), + ); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 65490e425..790ac656d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["src","karma*.ts"], + "include": ["src"], "compilerOptions": { "moduleResolution": "node", "target": "es6", diff --git a/tsconfig.karma.json b/tsconfig.karma.json new file mode 100644 index 000000000..a62ab772e --- /dev/null +++ b/tsconfig.karma.json @@ -0,0 +1,17 @@ +{ + "include": ["src", "./karma*.ts", "./setupKarma.ts"], + "compilerOptions": { + "moduleResolution": "node", + "target": "es6", + "module": "commonjs", + "lib": ["es2015", "es2016", "es2017", "dom"], + "strict": true, + "pretty": true, + "strictNullChecks": true, + "suppressImplicitAnyIndexErrors": true, + "noUnusedLocals": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "importHelpers": true + } +} From 9b7fc2aa0a84a48b3e2b71d792da4fd8ee064c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Tue, 20 Aug 2019 10:05:19 +0200 Subject: [PATCH 5/5] build: add build to karma --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 87faceb63..ac82133b3 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "test.prod": "yarn build && yarn lint && yarn test.karma && yarn test --coverage --maxWorkers=2", "test.update": "yarn test --updateSnapshot", "test.watch": "yarn test --watch", + "pretest.karma": "yarn build", "test.karma": "karma start", "inline-version": "./scripts/inline-version.js", "schema.update": "yarn typescript-json-schema --id \"http://stoplight.io/schemas/rule.schema.json\" --required tsconfig.json IRule --out ./src/meta/rule.schema.json"