diff --git a/tools/spectral/ipa/__tests__/IPA106CreateMethodResponseIsGetMethodResponse.test.js b/tools/spectral/ipa/__tests__/IPA106CreateMethodResponseIsGetMethodResponse.test.js index 3932c03c35..4ad3c40ce4 100644 --- a/tools/spectral/ipa/__tests__/IPA106CreateMethodResponseIsGetMethodResponse.test.js +++ b/tools/spectral/ipa/__tests__/IPA106CreateMethodResponseIsGetMethodResponse.test.js @@ -24,7 +24,7 @@ const componentSchemas = { testRule('xgen-IPA-106-create-method-response-is-get-method-response', [ { - name: 'valid create requests', + name: 'valid create responses', document: { paths: { '/resources': { @@ -110,13 +110,15 @@ testRule('xgen-IPA-106-create-method-response-is-get-method-response', [ // 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' }, + responses: { + 201: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + type: 'object', + properties: { + name: { type: 'string' }, + }, }, }, }, @@ -127,11 +129,13 @@ testRule('xgen-IPA-106-create-method-response-is-get-method-response', [ // Version mismatch but will be ignored '/versionMismatchResources': { post: { - requestBody: { - content: { - 'application/vnd.atlas.2024-01-05+json': { - schema: { - $ref: '#/components/schemas/ResourceSchema', + responses: { + 201: { + content: { + 'application/vnd.atlas.2024-01-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, }, }, }, @@ -159,7 +163,7 @@ testRule('xgen-IPA-106-create-method-response-is-get-method-response', [ errors: [], }, { - name: 'invalid create requests', + name: 'invalid create responses', document: { paths: { // Get without schema diff --git a/tools/spectral/ipa/__tests__/IPA107UpdateMethodResponseIsGetMethodResponse.test.js b/tools/spectral/ipa/__tests__/IPA107UpdateMethodResponseIsGetMethodResponse.test.js new file mode 100644 index 0000000000..8a1eae3e41 --- /dev/null +++ b/tools/spectral/ipa/__tests__/IPA107UpdateMethodResponseIsGetMethodResponse.test.js @@ -0,0 +1,506 @@ +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-107-update-method-response-is-get-method-response', [ + { + name: 'valid update responses', + document: { + paths: { + '/resources/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + patch: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + }, + // Multiple versions + '/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', + }, + }, + }, + }, + }, + }, + patch: { + 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/{id}': { + patch: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + }, + // Version mismatch but will be ignored + '/versionMismatchResources/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + patch: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-01-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + }, + // Non-200 will be ignored + '/resource/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + patch: { + responses: { + 202: { + requestBody: { + content: { + 'application/vnd.atlas.2024-01-05+json': { + schema: { + $ref: '#/components/schemas/OtherSchema', + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: componentSchemas, + }, + errors: [], + }, + { + name: 'invalid update responses', + document: { + paths: { + // Get without schema - Patch + '/resourceOne/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-01-05+json': {}, + }, + }, + }, + }, + patch: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-01-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + }, + // Get without schema - Put + '/resourceTwo/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-01-05+json': {}, + }, + }, + }, + }, + put: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-01-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + }, + // Get without schema ref + '/resourceThree/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-01-05+json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + patch: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-01-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + }, + // Schema mismatch + '/resourceFour/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-01-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + patch: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-01-05+json': { + schema: { + $ref: '#/components/schemas/OtherSchema', + }, + }, + }, + }, + }, + }, + }, + }, + components: componentSchemas, + }, + errors: [ + { + code: 'xgen-IPA-107-update-method-response-is-get-method-response', + message: + 'Could not validate that the Update method returns the same resource object as the Get method. The Get method does not have a schema.', + path: [ + 'paths', + '/resourceOne/{id}', + 'patch', + 'responses', + '200', + 'content', + 'application/vnd.atlas.2024-01-05+json', + ], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-update-method-response-is-get-method-response', + message: + 'Could not validate that the Update method returns the same resource object as the Get method. The Get method does not have a schema.', + path: [ + 'paths', + '/resourceTwo/{id}', + 'put', + 'responses', + '200', + 'content', + 'application/vnd.atlas.2024-01-05+json', + ], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-update-method-response-is-get-method-response', + message: + 'Could not validate that the Update method returns the same resource object as the Get method. The Get method does not have a schema reference.', + path: [ + 'paths', + '/resourceThree/{id}', + 'patch', + 'responses', + '200', + 'content', + 'application/vnd.atlas.2024-01-05+json', + ], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-update-method-response-is-get-method-response', + message: 'The schema in the Update method response must be the same schema as the response of the Get method.', + path: [ + 'paths', + '/resourceFour/{id}', + 'patch', + 'responses', + '200', + 'content', + 'application/vnd.atlas.2024-01-05+json', + ], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid with version mismatch', + document: { + paths: { + '/resource/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-01-05+json': { + schema: { + $ref: '#/components/schemas/OtherSchema', + }, + }, + }, + }, + }, + }, + patch: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + }, + }, + components: componentSchemas, + }, + errors: [ + { + code: 'xgen-IPA-107-update-method-response-is-get-method-response', + message: 'The schema in the Update method response must be the same schema as the response of the Get method.', + path: [ + 'paths', + '/resource/{id}', + 'patch', + 'responses', + '200', + 'content', + 'application/vnd.atlas.2024-08-05+json', + ], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid with exception', + document: { + paths: { + '/resource/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + patch: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-107-update-method-response-is-get-method-response': 'Exception reason', + }, + schema: { + type: 'object', + properties: { + completelyDifferent: { type: 'boolean' }, + }, + }, + }, + }, + }, + }, + }, + }, + '/resourceTwo/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + schema: { + $ref: '#/components/schemas/ResourceSchema', + }, + }, + }, + }, + }, + }, + patch: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-107-update-method-response-is-get-method-response': 'Exception reason', + }, + schema: { + $ref: '#/components/schemas/OtherSchema', + }, + }, + }, + }, + }, + }, + }, + }, + components: componentSchemas, + }, + errors: [], + }, +]); diff --git a/tools/spectral/ipa/rulesets/IPA-107.yaml b/tools/spectral/ipa/rulesets/IPA-107.yaml index 92661d29c5..46cb11f42a 100644 --- a/tools/spectral/ipa/rulesets/IPA-107.yaml +++ b/tools/spectral/ipa/rulesets/IPA-107.yaml @@ -4,6 +4,7 @@ functions: - IPA107UpdateMethodMustNotHaveQueryParams - IPA107UpdateResponseCodeShouldBe200OK + - IPA107UpdateMethodResponseIsGetMethodResponse rules: xgen-IPA-107-put-must-not-have-query-params: @@ -68,3 +69,20 @@ rules: given: '$.paths[*].patch' then: function: 'IPA107UpdateResponseCodeShouldBe200OK' + xgen-IPA-107-update-method-response-is-get-method-response: + description: >- + The response body of the Update method should consist of the same resource object returned by the Get method. + + ##### Implementation details + Rule checks for the following conditions: + - Applies only to single resource paths with JSON content types + - Ignores singleton resources and responses without a schema + - Validation ignores resources without a Get method + - Fails if the Get method doesn't have a schema reference or if the schemas don't match + - Paths with `x-xgen-IPA-exception` for this rule are excluded from validation + message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-107-update-method-response-is-get-method-response' + severity: warn + given: '$.paths[*][put,patch].responses.200.content' + then: + field: '@key' + function: 'IPA107UpdateMethodResponseIsGetMethodResponse' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index d5546e0ce5..17fad68f9c 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -313,6 +313,16 @@ The Update method response status code should be 200 OK. Validation checks the PATCH method for single resource paths and [singleton resources](https://go/ipa/113). - Operation objects with `x-xgen-IPA-exception` for this rule are excluded from validation +#### xgen-IPA-107-update-method-response-is-get-method-response + + ![warn](https://img.shields.io/badge/warning-yellow) +The response body of the Update method should consist of the same resource object returned by the Get method. +##### Implementation details Rule checks for the following conditions: + - Applies only to single resource paths with JSON content types + - Ignores singleton resources and responses without a schema + - Validation ignores resources without a Get method + - Fails if the Get method doesn't have a schema reference or if the schemas don't match + - Paths with `x-xgen-IPA-exception` for this rule are excluded from validation ### IPA-108 diff --git a/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodResponseIsGetMethodResponse.js b/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodResponseIsGetMethodResponse.js new file mode 100644 index 0000000000..5695aaae7a --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodResponseIsGetMethodResponse.js @@ -0,0 +1,95 @@ +import { isSingleResourceIdentifier } 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, getGETMethodResponseSchemaFromPathItem } from './utils/methodUtils.js'; + +const RULE_NAME = 'xgen-IPA-107-update-method-response-is-get-method-response'; +const ERROR_MESSAGE = + 'The schema in the Update method response must be the same schema as the response of the Get method.'; + +/** + * Update method (PUT, PATCH) responses should reference the same schema as the Get method. + * + * @param {object} input - An update operation 200 response content version + * @param {object} _ - Unused + * @param {{ path: string[], documentInventory: object}} context - The context object containing the path and document + */ +export default (input, _, { path, documentInventory }) => { + const oas = documentInventory.unresolved; + const resourcePath = path[1]; + const mediaType = input; + + if (!mediaType.endsWith('json') || !isSingleResourceIdentifier(resourcePath)) { + return; + } + + // Ignore if the Update method does not have a response schema + const updateMethodResponse = resolveObject(oas, path); + + if (!updateMethodResponse || !updateMethodResponse.schema) { + return; + } + + if (hasException(updateMethodResponse, RULE_NAME)) { + collectException(updateMethodResponse, RULE_NAME, path); + return; + } + + // Ignore if there is no matching Get method + const getMethodResponseContentPerMediaType = getGETMethodResponseSchemaFromPathItem( + oas.paths[resourcePath], + mediaType + ); + if (!getMethodResponseContentPerMediaType) { + return; + } + + const errors = checkViolationsAndReturnErrors(path, updateMethodResponse, getMethodResponseContentPerMediaType); + + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); + } + + collectAdoption(path, RULE_NAME); +}; + +function checkViolationsAndReturnErrors(path, updateMethodResponseContent, getMethodResponseContent) { + try { + // Error if the Get method does not have a schema + if (!getMethodResponseContent.schema) { + return [ + { + path, + message: `Could not validate that the Update method returns the same resource object as the Get method. The Get method does not have a schema.`, + }, + ]; + } + + const updateMethodSchemaRef = getSchemaRef(updateMethodResponseContent.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 Update 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 update method resource + if (getMethodSchemaRef !== updateMethodSchemaRef) { + return [{ path, message: ERROR_MESSAGE }]; + } + return []; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + } +} diff --git a/tools/spectral/ipa/rulesets/functions/utils/methodUtils.js b/tools/spectral/ipa/rulesets/functions/utils/methodUtils.js index 2acb191e51..49bae5477d 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/methodUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/methodUtils.js @@ -118,7 +118,7 @@ export function getResponseOfListMethodByMediaType(mediaType, pathForResourceCol * @param {string} mediaType The media type * @returns {Object|null} The schema object, or null if not found */ -function getGETMethodResponseSchemaFromPathItem(pathItem, mediaType) { +export function getGETMethodResponseSchemaFromPathItem(pathItem, mediaType) { if (!hasGetMethod(pathItem)) { return null; }