diff --git a/tools/spectral/ipa/__tests__/createMethodResponseIsGetMethodResponse.test.js b/tools/spectral/ipa/__tests__/createMethodResponseIsGetMethodResponse.test.js new file mode 100644 index 0000000000..3932c03c35 --- /dev/null +++ b/tools/spectral/ipa/__tests__/createMethodResponseIsGetMethodResponse.test.js @@ -0,0 +1,350 @@ +import testRule from './__helpers__/testRule'; +import { DiagnosticSeverity } from '@stoplight/types'; + +const componentSchemas = { + schemas: { + ResourceSchema: { + type: 'object', + properties: { + id: { type: 'string', readOnly: true }, + name: { type: 'string' }, + description: { type: 'string' }, + createdAt: { type: 'string', readOnly: true }, + }, + }, + OtherSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + }, + }, + }, +}; + +testRule('xgen-IPA-106-create-method-response-is-get-method-response', [ + { + name: 'valid create requests', + document: { + paths: { + '/resources': { + post: { + responses: { + 201: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + }, + '/resources/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + }, + // Multiple versions + '/versionedResources': { + post: { + responses: { + 201: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + 'application/vnd.atlas.2024-01-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + }, + '/versionedResources/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + 'application/vnd.atlas.2024-01-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + }, + }, + components: componentSchemas, + }, + errors: [], + }, + { + name: 'rule ignores inapplicable cases', + document: { + paths: { + // Path not ending in collection + '/not/a/collection/resource': { + post: { + requestBody: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + // Version mismatch but will be ignored + '/versionMismatchResources': { + post: { + requestBody: { + content: { + 'application/vnd.atlas.2024-01-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + '/versionMismatchResources/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + }, + }, + components: componentSchemas, + }, + errors: [], + }, + { + name: 'invalid create requests', + document: { + paths: { + // Get without schema + '/resourcesOne': { + post: { + responses: { + 201: { + content: { + 'application/vnd.atlas.2024-01-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + }, + '/resourcesOne/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-01-05+json': {}, + }, + }, + }, + }, + }, + // Get without schema ref + '/resourcesTwo': { + post: { + responses: { + 201: { + content: { + 'application/vnd.atlas.2024-01-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + }, + '/resourcesTwo/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-01-05+json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + components: componentSchemas, + }, + errors: [ + { + code: 'xgen-IPA-106-create-method-response-is-get-method-response', + message: + 'Could not validate that the Create method returns the same resource object as the Get method. The Get method does not have a schema.', + path: [ + 'paths', + '/resourcesOne', + 'post', + 'responses', + '201', + 'content', + 'application/vnd.atlas.2024-01-05+json', + ], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-106-create-method-response-is-get-method-response', + message: + 'Could not validate that the Create method returns the same resource object as the Get method. The Get method does not have a schema reference.', + path: [ + 'paths', + '/resourcesTwo', + 'post', + 'responses', + '201', + 'content', + 'application/vnd.atlas.2024-01-05+json', + ], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid with version mismatch', + document: { + paths: { + '/resources': { + post: { + responses: { + 201: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + }, + '/resources/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-01-05+json': { + schema: { + $ref: '#/components/schemas/OtherSchema', + }, + }, + }, + }, + }, + }, + }, + }, + components: componentSchemas, + }, + errors: [ + { + code: 'xgen-IPA-106-create-method-response-is-get-method-response', + message: 'The schema in the Create method response must be the same schema as the response of the Get method.', + path: ['paths', '/resources', 'post', 'responses', '201', 'content', 'application/vnd.atlas.2024-08-05+json'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid with exception', + document: { + paths: { + '/resources': { + post: { + responses: { + 201: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-106-create-method-response-is-get-method-response': 'Exception reason', + }, + schema: { + type: 'object', + properties: { + completelyDifferent: { type: 'boolean' }, + }, + }, + }, + }, + }, + }, + }, + }, + '/resources/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + }, + }, + components: componentSchemas, + }, + errors: [], + }, +]); diff --git a/tools/spectral/ipa/rulesets/IPA-105.yaml b/tools/spectral/ipa/rulesets/IPA-105.yaml index 3cf2ce6ca4..26fc3d7b1f 100644 --- a/tools/spectral/ipa/rulesets/IPA-105.yaml +++ b/tools/spectral/ipa/rulesets/IPA-105.yaml @@ -36,7 +36,7 @@ rules: ##### Implementation details - Validation checks that the List method response contains items property with reference to the same schema as the Get method response. + Validation checks that the List method 200 OK response contains items property with reference to the same schema as the Get method response. - Validation applies to List methods for resource collections only - Validation applies to json response content only @@ -46,7 +46,7 @@ rules: - Paths with `x-xgen-IPA-exception` for this rule are excluded from validation message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-105-list-method-response-is-get-method-response' severity: warn - given: '$.paths[*].get.responses[*].content' + given: '$.paths[*].get.responses.200.content' then: field: '@key' function: 'listMethodResponseIsGetMethodResponse' diff --git a/tools/spectral/ipa/rulesets/IPA-106.yaml b/tools/spectral/ipa/rulesets/IPA-106.yaml index 8dae36f453..1f3239787c 100644 --- a/tools/spectral/ipa/rulesets/IPA-106.yaml +++ b/tools/spectral/ipa/rulesets/IPA-106.yaml @@ -7,6 +7,7 @@ functions: - createMethodRequestBodyIsGetResponse - createMethodRequestHasNoReadonlyFields - createMethodResponseCodeIs201Created + - createMethodResponseIsGetMethodResponse rules: xgen-IPA-106-create-method-request-body-is-request-suffixed-object: @@ -76,3 +77,22 @@ rules: given: '$.paths[*].post' then: function: 'createMethodResponseCodeIs201Created' + xgen-IPA-106-create-method-response-is-get-method-response: + description: >- + The response body of the Create method should consist of the same resource object returned by the Get method. + + ##### Implementation details + + Validation checks that the Create method 201 Created response contains reference to the same schema as the Get method response. + + - Validation applies to Create methods for resource collections only + - Validation applies to json response content only + - Validation ignores responses without schema + - Validation ignores resources without a Get method + - Paths with `x-xgen-IPA-exception` for this rule are excluded from validation + message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-106-create-method-response-is-get-method-response' + severity: warn + given: '$.paths[*].post.responses.201.content' + then: + field: '@key' + function: 'createMethodResponseIsGetMethodResponse' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index 41c008c0b2..1fb6f0290a 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -106,7 +106,7 @@ APIs must provide a List method for resources. ![warn](https://img.shields.io/badge/warning-yellow) The response body of the List method should consist of the same resource object returned by the Get method. ##### Implementation details -Validation checks that the List method response contains items property with reference to the same schema as the Get method response. +Validation checks that the List method 200 OK response contains items property with reference to the same schema as the Get method response. - Validation applies to List methods for resource collections only - Validation applies to json response content only @@ -152,6 +152,18 @@ Validation checks the POST method for resource collection paths. Create methods must return a 201 Created response code. ##### Implementation details Validation checks the POST method for resource collection paths. +#### xgen-IPA-106-create-method-response-is-get-method-response + + ![warn](https://img.shields.io/badge/warning-yellow) +The response body of the Create method should consist of the same resource object returned by the Get method. +##### Implementation details +Validation checks that the Create method 201 Created response contains reference to the same schema as the Get method response. + + - Validation applies to Create methods for resource collections only + - Validation applies to json response content only + - Validation ignores responses without schema + - Validation ignores resources without a Get method + - Paths with `x-xgen-IPA-exception` for this rule are excluded from validation ### IPA-107 diff --git a/tools/spectral/ipa/rulesets/functions/createMethodResponseIsGetMethodResponse.js b/tools/spectral/ipa/rulesets/functions/createMethodResponseIsGetMethodResponse.js new file mode 100644 index 0000000000..f3e1aea113 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/createMethodResponseIsGetMethodResponse.js @@ -0,0 +1,93 @@ +import { + getResourcePathItems, + isResourceCollectionIdentifier, + isSingletonResource, +} from './utils/resourceEvaluation.js'; +import { resolveObject } from './utils/componentUtils.js'; +import { hasException } from './utils/exceptions.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; +import { getSchemaRef, getResponseOfGetMethodByMediaType } from './utils/methodUtils.js'; + +const RULE_NAME = 'xgen-IPA-106-create-method-response-is-get-method-response'; +const ERROR_MESSAGE = + 'The schema in the Create method response must be the same schema as the response of the Get method.'; + +export default (input, _, { path, documentInventory }) => { + const oas = documentInventory.unresolved; + const resourcePath = path[1]; + const mediaType = input; + + if ( + !mediaType.endsWith('json') || + !isResourceCollectionIdentifier(resourcePath) || + (isResourceCollectionIdentifier(resourcePath) && isSingletonResource(getResourcePathItems(resourcePath, oas.paths))) + ) { + return; + } + + // Ignore if the List method does not have a response schema + const createMethodResponse = resolveObject(oas, path); + + if (!createMethodResponse || !createMethodResponse.schema) { + return; + } + + if (hasException(createMethodResponse, RULE_NAME)) { + collectException(createMethodResponse, RULE_NAME, path); + return; + } + + // Ignore if there is no matching Get method + const getMethodResponseContentPerMediaType = getResponseOfGetMethodByMediaType(mediaType, resourcePath, oas); + if (!getMethodResponseContentPerMediaType) { + return; + } + + const errors = checkViolationsAndReturnErrors(path, createMethodResponse, getMethodResponseContentPerMediaType); + + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); + } + + collectAdoption(path, RULE_NAME); +}; + +function checkViolationsAndReturnErrors(path, createMethodResponseContent, getMethodResponseContent) { + try { + // Error if the Get method does not have a schema + if (!getMethodResponseContent.schema) { + return [ + { + path, + message: `Could not validate that the Create method returns the same resource object as the Get method. The Get method does not have a schema.`, + }, + ]; + } + + const createMethodSchemaRef = getSchemaRef(createMethodResponseContent.schema); + const getMethodSchemaRef = getSchemaRef(getMethodResponseContent.schema); + + // Error if the Get method does not have a schema ref + if (!getMethodSchemaRef) { + return [ + { + path, + message: `Could not validate that the Create method returns the same resource object as the Get method. The Get method does not have a schema reference.`, + }, + ]; + } + + // Error if the get method resource is not the same as the create method resource + if (getMethodSchemaRef !== createMethodSchemaRef) { + return [{ path, message: ERROR_MESSAGE }]; + } + return []; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + } +} diff --git a/tools/spectral/ipa/rulesets/functions/listMethodResponseIsGetMethodResponse.js b/tools/spectral/ipa/rulesets/functions/listMethodResponseIsGetMethodResponse.js index 7f5554918c..f40c58e86f 100644 --- a/tools/spectral/ipa/rulesets/functions/listMethodResponseIsGetMethodResponse.js +++ b/tools/spectral/ipa/rulesets/functions/listMethodResponseIsGetMethodResponse.js @@ -21,11 +21,9 @@ const ERROR_MESSAGE = export default (input, _, { path, documentInventory }) => { const oas = documentInventory.unresolved; const resourcePath = path[1]; - const responseCode = path[4]; const mediaType = input; if ( - responseCode !== '200' || !mediaType.endsWith('json') || !isResourceCollectionIdentifier(resourcePath) || (isResourceCollectionIdentifier(resourcePath) && isSingletonResource(getResourcePathItems(resourcePath, oas.paths)))