From 300ca40662c4d8294084c1b2a827a8fe0a1e4621 Mon Sep 17 00:00:00 2001 From: Andriy Leliv Date: Thu, 27 Jan 2022 18:41:04 +0200 Subject: [PATCH] feat: response-contains-property built-in rule --- .../response-contains-property.test.ts | 92 +++++++++++++++++++ packages/core/src/rules/oas2/index.ts | 2 + .../rules/oas2/response-contains-property.ts | 23 +++++ .../response-contains-property.test.ts | 77 ++++++++++++++++ packages/core/src/rules/oas3/index.ts | 2 + .../rules/oas3/response-contains-property.ts | 25 +++++ 6 files changed, 221 insertions(+) create mode 100644 packages/core/src/rules/oas2/__tests__/response-contains-property.test.ts create mode 100644 packages/core/src/rules/oas2/response-contains-property.ts create mode 100644 packages/core/src/rules/oas3/__tests__/response-contains-property.test.ts create mode 100644 packages/core/src/rules/oas3/response-contains-property.ts diff --git a/packages/core/src/rules/oas2/__tests__/response-contains-property.test.ts b/packages/core/src/rules/oas2/__tests__/response-contains-property.test.ts new file mode 100644 index 0000000000..c9efd762a8 --- /dev/null +++ b/packages/core/src/rules/oas2/__tests__/response-contains-property.test.ts @@ -0,0 +1,92 @@ +import { outdent } from 'outdent'; +import { lintDocument } from '../../../lint'; +import { parseYamlToDocument, makeConfig } from '../../../../__tests__/utils'; +import { BaseResolver } from '../../../resolve'; + +describe('Oas2 response-contains-property', () => { + it('oas2-response-contains-property: should report on response object when not contains properties', async () => { + const document = parseYamlToDocument( + outdent` + swagger: '2.0' + schemes: + - https + basePath: /v2 + paths: + '/accounts/{accountId}': + get: + description: Retrieve a sub account under the master account. + operationId: account + responses: + '200': + description: Account object returned + schema: + type: object + properties: + created_at: + description: Account creation date/time + format: date-time + type: string + owner_email: + description: Account Owner email + type: string + '404': + description: User not found + `, + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: makeConfig({ + 'response-contains-property': { + severity: 'error', + mustExist: ['id'], + }, + }), + }); + expect(results).toMatchInlineSnapshot(` + Array [ + Object { + "location": Array [ + Object { + "pointer": "#/paths/~1accounts~1{accountId}/get/responses/200/schema/properties", + "reportOnKey": false, + "source": Source { + "absoluteRef": "", + "body": "swagger: '2.0' + schemes: + - https + basePath: /v2 + paths: + '/accounts/{accountId}': + get: + description: Retrieve a sub account under the master account. + operationId: account + responses: + '200': + description: Account object returned + schema: + type: object + properties: + created_at: + description: Account creation date/time + format: date-time + type: string + owner_email: + description: Account Owner email + type: string + '404': + description: User not found", + "mimeType": undefined, + }, + }, + ], + "message": "Response object must have a top-level \\"id\\" property.", + "ruleId": "response-contains-property", + "severity": "error", + "suggest": Array [], + }, + ] + `); + }); +}); diff --git a/packages/core/src/rules/oas2/index.ts b/packages/core/src/rules/oas2/index.ts index 576d4ef6f0..aa289b67f7 100644 --- a/packages/core/src/rules/oas2/index.ts +++ b/packages/core/src/rules/oas2/index.ts @@ -36,6 +36,7 @@ import { PathExcludesPatterns } from '../common/path-excludes-patterns'; import { RequestMimeType } from './request-mime-type'; import { ResponseMimeType } from './response-mime-type'; import { PathSegmentPlural } from '../common/path-segment-plural'; +import { ResponseContainsProperty } from './response-contains-property'; export const rules = { spec: OasSpec as Oas2Rule, @@ -76,6 +77,7 @@ export const rules = { 'request-mime-type': RequestMimeType as Oas2Rule, 'response-mime-type': ResponseMimeType as Oas2Rule, 'path-segment-plural': PathSegmentPlural as Oas2Rule, + 'response-contains-property': ResponseContainsProperty as Oas2Rule, }; export const preprocessors = {}; diff --git a/packages/core/src/rules/oas2/response-contains-property.ts b/packages/core/src/rules/oas2/response-contains-property.ts new file mode 100644 index 0000000000..ade8848dda --- /dev/null +++ b/packages/core/src/rules/oas2/response-contains-property.ts @@ -0,0 +1,23 @@ +import { Oas2Rule } from '../../visitors'; + +export const ResponseContainsProperty: Oas2Rule = (options) => { + const mustExist = options.mustExist || []; + return { + Response: { + skip: (_response, key) => { + return !['200', '201', '202'].includes(key.toString()); + }, + Schema (schema, { report, location }) { + if (schema.type !== 'object') return; + for (let element of mustExist) { + if (!schema.properties?.[element]) { + report({ + message: `Response object must have a top-level "${element}" property.`, + location: location.child('properties'), + }); + } + } + } + } + } +}; diff --git a/packages/core/src/rules/oas3/__tests__/response-contains-property.test.ts b/packages/core/src/rules/oas3/__tests__/response-contains-property.test.ts new file mode 100644 index 0000000000..4fb670e58c --- /dev/null +++ b/packages/core/src/rules/oas3/__tests__/response-contains-property.test.ts @@ -0,0 +1,77 @@ +import { outdent } from 'outdent'; +import { lintDocument } from '../../../lint'; +import { parseYamlToDocument, makeConfig } from '../../../../__tests__/utils'; +import { BaseResolver } from '../../../resolve'; + +describe('Oas3 response-contains-property', () => { + it('oas3-response-contains-property: should report on response object when not contains properties', async () => { + const document = parseYamlToDocument( + outdent` + openapi: 3.0.3 + info: + version: 3.0.0 + paths: + /store/subscribe: + post: + responses: + '201': + content: + application/json: + schema: + type: object + properties: + subscriptionId: + type: string + example: AAA-123-BBB-456 + `, + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: makeConfig({ + 'response-contains-property': { + severity: 'error', + mustExist: ['id'], + }, + }), + }); + + expect(results).toMatchInlineSnapshot(` + Array [ + Object { + "location": Array [ + Object { + "pointer": "#/paths/~1store~1subscribe/post/responses/201/content/application~1json/schema/properties", + "reportOnKey": false, + "source": Source { + "absoluteRef": "", + "body": "openapi: 3.0.3 + info: + version: 3.0.0 + paths: + /store/subscribe: + post: + responses: + '201': + content: + application/json: + schema: + type: object + properties: + subscriptionId: + type: string + example: AAA-123-BBB-456", + "mimeType": undefined, + }, + }, + ], + "message": "Response object must have a top-level \\"id\\" property.", + "ruleId": "response-contains-property", + "severity": "error", + "suggest": Array [], + }, + ] + `); + }); +}); diff --git a/packages/core/src/rules/oas3/index.ts b/packages/core/src/rules/oas3/index.ts index 74518d9281..a428db4a2d 100644 --- a/packages/core/src/rules/oas3/index.ts +++ b/packages/core/src/rules/oas3/index.ts @@ -44,6 +44,7 @@ import { PathSegmentPlural } from '../common/path-segment-plural'; import { PathExcludesPatterns } from '../common/path-excludes-patterns'; import { NoInvalidSchemaExamples } from '../common/no-invalid-schema-examples'; import { NoInvalidParameterExamples } from '../common/no-invalid-parameter-examples'; +import { ResponseContainsProperty } from './response-contains-property'; export const rules = { spec: OasSpec, @@ -92,6 +93,7 @@ export const rules = { 'path-segment-plural': PathSegmentPlural, 'no-invalid-schema-examples': NoInvalidSchemaExamples, 'no-invalid-parameter-examples': NoInvalidParameterExamples, + 'response-contains-property': ResponseContainsProperty, } as Oas3RuleSet; export const preprocessors = {}; diff --git a/packages/core/src/rules/oas3/response-contains-property.ts b/packages/core/src/rules/oas3/response-contains-property.ts new file mode 100644 index 0000000000..403e1162ce --- /dev/null +++ b/packages/core/src/rules/oas3/response-contains-property.ts @@ -0,0 +1,25 @@ +import { Oas3Rule } from '../../visitors'; + +export const ResponseContainsProperty: Oas3Rule = (options) => { + const mustExist = options.mustExist || []; + return { + Response: { + skip: (_response, key) => { + return !['200', '201', '202'].includes(key.toString()); + }, + MediaType: { + Schema (schema, { report, location }) { + if (schema.type !== 'object') return; + for (let element of mustExist) { + if (!schema.properties?.[element]) { + report({ + message: `Response object must have a top-level "${element}" property.`, + location: location.child('properties'), + }); + } + } + } + } + } + } +};