diff --git a/tools/spectral/ipa/__tests__/createMethodResponseCodeIs201Created.test.js b/tools/spectral/ipa/__tests__/createMethodResponseCodeIs201Created.test.js new file mode 100644 index 0000000000..2cfc382847 --- /dev/null +++ b/tools/spectral/ipa/__tests__/createMethodResponseCodeIs201Created.test.js @@ -0,0 +1,117 @@ +import testRule from './__helpers__/testRule'; +import { DiagnosticSeverity } from '@stoplight/types'; + +testRule('xgen-IPA-106-create-method-response-code-is-201', [ + { + name: 'valid methods', + document: { + paths: { + '/resource': { + post: { + responses: { + 201: {}, + 400: {}, + 500: {}, + }, + }, + }, + '/resource/{id}/subresource': { + post: { + responses: { + 201: {}, + 400: {}, + 500: {}, + }, + }, + }, + '/resource/{id}:customMethod': { + post: { + responses: { + 200: {}, + 400: {}, + 500: {}, + }, + }, + }, + }, + }, + errors: [], + }, + { + name: 'invalid methods', + document: { + paths: { + '/resourceOne': { + post: { + responses: { + 200: {}, + 400: {}, + 500: {}, + }, + }, + }, + '/resourceTwo': { + post: { + responses: { + 400: {}, + 500: {}, + }, + }, + }, + '/resourceThree': { + post: { + responses: { + 201: {}, + 200: {}, + 400: {}, + 500: {}, + }, + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-106-create-method-response-code-is-201', + message: + 'The Create method must return a 201 Created response. This method either lacks a 201 Created response or defines a different 2xx status code. http://go/ipa/106', + path: ['paths', '/resourceOne', 'post'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-106-create-method-response-code-is-201', + message: + 'The Create method must return a 201 Created response. This method either lacks a 201 Created response or defines a different 2xx status code. http://go/ipa/106', + path: ['paths', '/resourceTwo', 'post'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-106-create-method-response-code-is-201', + message: + 'The Create method must return a 201 Created response. This method either lacks a 201 Created response or defines a different 2xx status code. http://go/ipa/106', + path: ['paths', '/resourceThree', 'post'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid method with exception', + document: { + paths: { + '/resourceOne': { + post: { + responses: { + 200: {}, + 400: {}, + 500: {}, + }, + 'x-xgen-IPA-exception': { + 'xgen-IPA-106-create-method-response-code-is-201': 'Reason', + }, + }, + }, + }, + }, + errors: [], + }, +]); diff --git a/tools/spectral/ipa/rulesets/IPA-106.yaml b/tools/spectral/ipa/rulesets/IPA-106.yaml index d6fcf29c03..40734d725d 100644 --- a/tools/spectral/ipa/rulesets/IPA-106.yaml +++ b/tools/spectral/ipa/rulesets/IPA-106.yaml @@ -6,6 +6,7 @@ functions: - createMethodShouldNotHaveQueryParameters - createMethodRequestBodyIsGetResponse - createMethodRequestHasNoReadonlyFields + - createMethodResponseCodeIs201Created rules: xgen-IPA-106-create-method-request-body-is-request-suffixed-object: @@ -43,3 +44,10 @@ rules: then: field: '@key' function: 'createMethodRequestHasNoReadonlyFields' + xgen-IPA-106-create-method-response-code-is-201: + description: 'Create methods must return a 201 Created response code. http://go/ipa/106' + message: '{{error}} http://go/ipa/106' + severity: warn + given: '$.paths[*].post' + then: + function: 'createMethodResponseCodeIs201Created' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index cdb830ceed..386458bb6d 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -61,6 +61,7 @@ For rule definitions, see [IPA-106.yaml](https://github.com/mongodb/openapi/blob readOnly/writeOnly properties will be ignored. http://go/ipa/106 | warn | | xgen-IPA-106-create-method-request-has-no-readonly-fields | Create method Request object must not include fields with readOnly:true. http://go/ipa/106 | warn | +| xgen-IPA-106-create-method-response-code-is-201 | Create methods must return a 201 Created response code. http://go/ipa/106 | warn | ### IPA-108 diff --git a/tools/spectral/ipa/rulesets/functions/createMethodResponseCodeIs201Created.js b/tools/spectral/ipa/rulesets/functions/createMethodResponseCodeIs201Created.js new file mode 100644 index 0000000000..b079469536 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/createMethodResponseCodeIs201Created.js @@ -0,0 +1,59 @@ +import { + getResourcePathItems, + isCustomMethodIdentifier, + isResourceCollectionIdentifier, + isSingletonResource, +} from './utils/resourceEvaluation.js'; +import { hasException } from './utils/exceptions.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; + +const RULE_NAME = 'xgen-IPA-106-create-method-response-code-is-201'; +const ERROR_MESSAGE = + 'The Create method must return a 201 Created response. This method either lacks a 201 Created response or defines a different 2xx status code.'; + +export default (input, _, { path, documentInventory }) => { + const oas = documentInventory.resolved; + const resourcePath = path[1]; + const resourcePaths = getResourcePathItems(resourcePath, oas.paths); + + const isResourceCollection = isResourceCollectionIdentifier(resourcePath) && !isSingletonResource(resourcePaths); + if (isCustomMethodIdentifier(resourcePath) || !isResourceCollection) { + return; + } + + if (hasException(input, RULE_NAME)) { + collectException(input, RULE_NAME, path); + return; + } + + const errors = checkViolationsAndReturnErrors(input, path); + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); + } + collectAdoption(path, RULE_NAME); +}; + +function checkViolationsAndReturnErrors(input, path) { + try { + const responses = input.responses; + + // If there is no 201 response, return a violation + if (!responses || !responses['201']) { + return [{ path, message: ERROR_MESSAGE }]; + } + + // If there are other 2xx responses that are not 201, return a violation + if (Object.keys(responses).some((key) => key.startsWith('2') && key !== '201')) { + return [{ path, message: ERROR_MESSAGE }]; + } + + return []; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + } +}