diff --git a/tools/spectral/ipa/__tests__/IPA107UpdateMethodRequestBodyIsGetResponse.test.js b/tools/spectral/ipa/__tests__/IPA107UpdateMethodRequestBodyIsGetResponse.test.js new file mode 100644 index 0000000000..c75f790c90 --- /dev/null +++ b/tools/spectral/ipa/__tests__/IPA107UpdateMethodRequestBodyIsGetResponse.test.js @@ -0,0 +1,666 @@ +import testRule from './__helpers__/testRule'; +import { DiagnosticSeverity } from '@stoplight/types'; + +const componentSchemas = { + schemas: { + SchemaOne: { + type: 'string', + }, + SchemaTwoRequest: { + type: 'object', + properties: { + name: { + type: 'string', + writeOnly: true, + }, + otherThing: { + type: 'string', + }, + }, + }, + SchemaThree: { + type: 'object', + properties: { + name: { + type: 'string', + }, + someArray: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }, + SchemaTwoResponse: { + type: 'object', + properties: { + name: { + type: 'string', + readOnly: true, + }, + otherThing: { + type: 'string', + }, + }, + }, + SchemaCircularOne: { + type: 'object', + properties: { + thing: { + $ref: '#/components/schemas/SchemaCircularTwo', + }, + }, + }, + SchemaCircularTwo: { + type: 'object', + properties: { + otherThing: { + $ref: '#/components/schemas/SchemaCircularOne', + }, + }, + }, + }, +}; + +const animals = { + schemas: { + Animal: { + type: 'object', + oneOf: [ + { + $ref: '#/components/schemas/Dog', + }, + { + $ref: '#/components/schemas/Cat', + }, + ], + }, + Dog: { + allOf: [ + { + $ref: '#/components/schemas/Animal', + }, + ], + }, + Cat: { + allOf: [ + { + $ref: '#/components/schemas/Animal', + }, + ], + }, + }, +}; + +testRule('xgen-IPA-107-update-method-request-body-is-get-method-response', [ + { + name: 'valid methods', + document: { + components: componentSchemas, + paths: { + '/resource/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaOne', + }, + }, + }, + }, + }, + }, + patch: { + requestBody: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + type: 'string', + }, + }, + 'application/vnd.atlas.2024-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaOne', + }, + }, + }, + }, + }, + put: { + requestBody: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + type: 'string', + }, + }, + 'application/vnd.atlas.2024-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaOne', + }, + }, + }, + }, + }, + }, + '/resourceTwo/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaTwoResponse', + }, + }, + 'application/vnd.atlas.2024-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaTwoResponse', + }, + }, + }, + }, + }, + }, + patch: { + requestBody: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaTwoRequest', + }, + }, + 'application/vnd.atlas.2024-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaTwoRequest', + }, + }, + }, + }, + }, + }, + '/resourceThree/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaThree', + }, + }, + }, + }, + }, + }, + patch: { + requestBody: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaThree', + }, + }, + 'application/vnd.atlas.2024-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaThree', + }, + }, + }, + }, + }, + }, + '/resource/{id}:customMethod': { + patch: { + requestBody: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaOne', + }, + }, + 'application/vnd.atlas.2024-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaOne', + }, + }, + }, + }, + }, + }, + }, + }, + errors: [], + }, + { + name: 'invalid methods', + document: { + components: componentSchemas, + paths: { + '/resource/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaTwoRequest', + }, + }, + }, + }, + }, + }, + patch: { + requestBody: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaOne', + }, + }, + 'application/vnd.atlas.2024-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaOne', + }, + }, + }, + }, + }, + put: { + requestBody: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaOne', + }, + }, + 'application/vnd.atlas.2024-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaOne', + }, + }, + }, + }, + }, + }, + '/resourceTwo/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaThree', + }, + }, + }, + }, + }, + }, + patch: { + requestBody: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaTwoResponse', + }, + }, + 'application/vnd.atlas.2024-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaTwoResponse', + }, + }, + }, + }, + }, + }, + '/resourceThree/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaThree', + }, + }, + }, + }, + }, + }, + patch: { + requestBody: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaOne', + }, + }, + 'application/vnd.atlas.2024-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaThree', + }, + }, + }, + }, + }, + }, + '/resourceFour/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2023-01-01+json': {}, + }, + }, + }, + }, + patch: { + requestBody: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaOne', + }, + }, + }, + }, + }, + }, + '/resourceCircular/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaCircularTwo', + }, + }, + }, + }, + }, + }, + patch: { + requestBody: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaCircularOne', + }, + }, + 'application/vnd.atlas.2024-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaCircularOne', + }, + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-107-update-method-request-body-is-get-method-response', + message: + 'The request body schema properties of the Update method must match the response body schema properties of the Get method.', + path: ['paths', '/resource/{id}', 'patch', 'requestBody', 'content', 'application/vnd.atlas.2023-01-01+json'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-update-method-request-body-is-get-method-response', + message: + 'The request body schema properties of the Update method must match the response body schema properties of the Get method.', + path: ['paths', '/resource/{id}', 'patch', 'requestBody', 'content', 'application/vnd.atlas.2024-01-01+json'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-update-method-request-body-is-get-method-response', + message: + 'The request body schema properties of the Update method must match the response body schema properties of the Get method.', + path: ['paths', '/resource/{id}', 'put', 'requestBody', 'content', 'application/vnd.atlas.2023-01-01+json'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-update-method-request-body-is-get-method-response', + message: + 'The request body schema properties of the Update method must match the response body schema properties of the Get method.', + path: ['paths', '/resource/{id}', 'put', 'requestBody', 'content', 'application/vnd.atlas.2024-01-01+json'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-update-method-request-body-is-get-method-response', + message: + 'The request body schema properties of the Update method must match the response body schema properties of the Get method.', + path: [ + 'paths', + '/resourceTwo/{id}', + 'patch', + 'requestBody', + 'content', + 'application/vnd.atlas.2023-01-01+json', + ], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-update-method-request-body-is-get-method-response', + message: + 'The request body schema properties of the Update method must match the response body schema properties of the Get method.', + path: [ + 'paths', + '/resourceTwo/{id}', + 'patch', + 'requestBody', + 'content', + 'application/vnd.atlas.2024-01-01+json', + ], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-update-method-request-body-is-get-method-response', + message: + 'The request body schema properties of the Update method must match the response body schema properties of the Get method.', + path: [ + 'paths', + '/resourceThree/{id}', + 'patch', + 'requestBody', + 'content', + 'application/vnd.atlas.2023-01-01+json', + ], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-update-method-request-body-is-get-method-response', + message: + 'Could not validate that the Update request body schema matches the response schema of the Get method. The Get method does not have a schema.', + path: [ + 'paths', + '/resourceFour/{id}', + 'patch', + 'requestBody', + 'content', + 'application/vnd.atlas.2023-01-01+json', + ], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-update-method-request-body-is-get-method-response', + message: + 'The request body schema properties of the Update method must match the response body schema properties of the Get method.', + path: [ + 'paths', + '/resourceCircular/{id}', + 'patch', + 'requestBody', + 'content', + 'application/vnd.atlas.2023-01-01+json', + ], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-update-method-request-body-is-get-method-response', + message: + 'The request body schema properties of the Update method must match the response body schema properties of the Get method.', + path: [ + 'paths', + '/resourceCircular/{id}', + 'patch', + 'requestBody', + 'content', + 'application/vnd.atlas.2024-01-01+json', + ], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid oneOf case', + document: { + components: animals, + paths: { + '/animalResource/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/Dog', + }, + }, + }, + }, + }, + }, + patch: { + requestBody: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/Animal', + }, + }, + 'application/vnd.atlas.2024-01-01+json': { + schema: { + $ref: '#/components/schemas/Animal', + }, + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-107-update-method-request-body-is-get-method-response', + message: + 'The request body schema properties of the Update method must match the response body schema properties of the Get method.', + path: [ + 'paths', + '/animalResource/{id}', + 'patch', + 'requestBody', + 'content', + 'application/vnd.atlas.2023-01-01+json', + ], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-107-update-method-request-body-is-get-method-response', + message: + 'The request body schema properties of the Update method must match the response body schema properties of the Get method.', + path: [ + 'paths', + '/animalResource/{id}', + 'patch', + 'requestBody', + 'content', + 'application/vnd.atlas.2024-01-01+json', + ], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid method with exception', + document: { + components: componentSchemas, + paths: { + '/resource/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaThree', + }, + }, + }, + }, + }, + }, + patch: { + requestBody: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaOne', + }, + 'x-xgen-IPA-exception': { + 'xgen-IPA-107-update-method-request-body-is-get-method-response': 'reason', + }, + }, + }, + }, + }, + }, + '/resourceTwo/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaTwoResponse', + }, + }, + }, + }, + }, + }, + patch: { + requestBody: { + content: { + 'application/vnd.atlas.2023-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaTwoRequest', + }, + 'x-xgen-IPA-exception': { + 'xgen-IPA-107-update-method-request-body-is-get-method-response': 'reason', + }, + }, + 'application/vnd.atlas.2024-01-01+json': { + schema: { + $ref: '#/components/schemas/SchemaTwoRequest', + }, + 'x-xgen-IPA-exception': { + 'xgen-IPA-107-update-method-request-body-is-get-method-response': 'reason', + }, + }, + }, + }, + }, + }, + }, + }, + errors: [], + }, +]); diff --git a/tools/spectral/ipa/rulesets/IPA-107.yaml b/tools/spectral/ipa/rulesets/IPA-107.yaml index 1c0af00483..27b5af3090 100644 --- a/tools/spectral/ipa/rulesets/IPA-107.yaml +++ b/tools/spectral/ipa/rulesets/IPA-107.yaml @@ -6,6 +6,7 @@ functions: - IPA107UpdateResponseCodeShouldBe200OK - IPA107UpdateMethodResponseIsGetMethodResponse - IPA107UpdateMethodRequestHasNoReadonlyFields + - IPA107UpdateMethodRequestBodyIsGetResponse rules: xgen-IPA-107-put-must-not-have-query-params: @@ -103,3 +104,21 @@ rules: then: field: '@key' function: 'IPA107UpdateMethodRequestHasNoReadonlyFields' + xgen-IPA-107-update-method-request-body-is-get-method-response: + description: | + The request body must contain the resource being updated, i.e. the resource or parts of the resource returned by the Get method. + + ##### Implementation details + + Validation checks the PATCH/PUT methods for single resource paths. + - Validation ignores resources without a Get method. + - `readOnly:true` properties of Get method response will be ignored. + - `writeOnly:true` properties of Update method request will be ignored. + - Property comparison is based on `type` and `name` matching. + - `oneOf` and `discriminator` definitions must match exactly. + message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-107-update-method-request-body-is-get-method-response:' + severity: warn + given: '$.paths[*][put,patch].requestBody.content' + then: + field: '@key' + function: 'IPA107UpdateMethodRequestBodyIsGetResponse' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index 7a4c63b653..b176c3db7b 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -335,6 +335,20 @@ Rule checks for the following conditions: - Searches through the request schema to find any properties marked with readOnly attribute - Fails if any readOnly properties are found in the request schema +#### xgen-IPA-107-update-method-request-body-is-get-method-response + + ![warn](https://img.shields.io/badge/warning-yellow) +The request body must contain the resource being updated, i.e. the resource or parts of the resource returned by the Get method. + +##### Implementation details + +Validation checks the PATCH/PUT methods for single resource paths. + - Validation ignores resources without a Get method. + - `readOnly:true` properties of Get method response will be ignored. + - `writeOnly:true` properties of Update method request will be ignored. + - Property comparison is based on `type` and `name` matching. + - `oneOf` and `discriminator` definitions must match exactly. + ### IPA-108 diff --git a/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsGetResponse.js b/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsGetResponse.js index 830d73d5b3..7b4f6188de 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsGetResponse.js +++ b/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsGetResponse.js @@ -5,15 +5,22 @@ import { isSingletonResource, } from './utils/resourceEvaluation.js'; import { resolveObject } from './utils/componentUtils.js'; -import { isDeepEqual, removeRequestProperties, removeResponseProperties } from './utils/compareUtils.js'; import { hasException } from './utils/exceptions.js'; import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; -import { getResponseOfGetMethodByMediaType, getSchemaRef } from './utils/methodUtils.js'; +import { getResponseOfGetMethodByMediaType } from './utils/methodUtils.js'; +import { checkRequestResponseResourceEqualityAndReturnErrors } from './utils/validations.js'; const RULE_NAME = 'xgen-IPA-106-create-method-request-body-is-get-method-response'; const ERROR_MESSAGE = 'The request body schema properties must match the response body schema properties of the Get method.'; +/** + * Create method request body schema properties must match the response body schema properties of the Get method. + * + * @param {string} input - A create operation request 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.resolved; const unresolvedOas = documentInventory.unresolved; @@ -48,12 +55,15 @@ export default (input, _, { path, documentInventory }) => { unresolvedOas ); - const errors = checkViolationsAndReturnErrors( + const errors = checkRequestResponseResourceEqualityAndReturnErrors( path, postRequestContentPerMediaType, getResponseContentPerMediaType, postRequestContentPerMediaTypeUnresolved, - getResponseContentPerMediaTypeUnresolved + getResponseContentPerMediaTypeUnresolved, + 'Create', + 'Get', + ERROR_MESSAGE ); if (errors.length !== 0) { @@ -62,43 +72,3 @@ export default (input, _, { path, documentInventory }) => { collectAdoption(path, RULE_NAME); }; - -function checkViolationsAndReturnErrors( - path, - postRequestContentPerMediaType, - getResponseContentPerMediaType, - postRequestContentPerMediaTypeUnresolved, - getResponseContentPerMediaTypeUnresolved -) { - const errors = []; - - if (!getResponseContentPerMediaType.schema) { - return [ - { - path, - message: `Could not validate that the Create request body schema matches the response schema of the Get method. The Get method does not have a schema.`, - }, - ]; - } - - const postRequestSchemaRef = getSchemaRef(postRequestContentPerMediaTypeUnresolved.schema); - const getResponseSchemaRef = getSchemaRef(getResponseContentPerMediaTypeUnresolved.schema); - - if (postRequestSchemaRef && getResponseSchemaRef) { - if (postRequestSchemaRef === getResponseSchemaRef) { - return []; - } - } - - const filteredCreateRequestSchema = removeRequestProperties(postRequestContentPerMediaType.schema); - const filteredGetResponseSchema = removeResponseProperties(getResponseContentPerMediaType.schema); - - if (!isDeepEqual(filteredCreateRequestSchema, filteredGetResponseSchema)) { - errors.push({ - path, - message: ERROR_MESSAGE, - }); - } - - return errors; -} diff --git a/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestBodyIsGetResponse.js b/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestBodyIsGetResponse.js new file mode 100644 index 0000000000..29438459b4 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestBodyIsGetResponse.js @@ -0,0 +1,69 @@ +import { isSingleResourceIdentifier } from './utils/resourceEvaluation.js'; +import { resolveObject } from './utils/componentUtils.js'; +import { hasException } from './utils/exceptions.js'; +import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { getGETMethodResponseSchemaFromPathItem } from './utils/methodUtils.js'; +import { checkRequestResponseResourceEqualityAndReturnErrors } from './utils/validations.js'; + +const RULE_NAME = 'xgen-IPA-107-update-method-request-body-is-get-method-response'; +const ERROR_MESSAGE = + 'The request body schema properties of the Update method must match the response body schema properties of the Get method.'; + +/** + * Update method (PUT, PATCH) request body schema properties must match the response body schema properties of the Get method. + * + * @param {string} input - An update operation request 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.resolved; + const unresolvedOas = documentInventory.unresolved; + const resourcePath = path[1]; + let mediaType = input; + + if (!isSingleResourceIdentifier(resourcePath) || !mediaType.endsWith('json')) { + return; + } + + // Ignore if the Update method does not have a request body schema + const updateMethodRequest = resolveObject(oas, path); + + if (!updateMethodRequest || !updateMethodRequest.schema) { + return; + } + + if (hasException(updateMethodRequest, RULE_NAME)) { + collectException(updateMethodRequest, RULE_NAME, path); + return; + } + + // Ignore if there is no matching Get method + const getMethodResponse = getGETMethodResponseSchemaFromPathItem(oas.paths[resourcePath], mediaType); + if (!getMethodResponse) { + return; + } + + const updateMethodRequestUnresolved = resolveObject(unresolvedOas, path); + const getMethodResponseUnresolved = getGETMethodResponseSchemaFromPathItem( + unresolvedOas.paths[resourcePath], + mediaType + ); + + const errors = checkRequestResponseResourceEqualityAndReturnErrors( + path, + updateMethodRequest, + getMethodResponse, + updateMethodRequestUnresolved, + getMethodResponseUnresolved, + 'Update', + 'Get', + ERROR_MESSAGE + ); + + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); + } + + collectAdoption(path, RULE_NAME); +}; diff --git a/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestHasNoReadonlyFields.js b/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestHasNoReadonlyFields.js index 4a6cbe6177..71f399b2c2 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestHasNoReadonlyFields.js +++ b/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestHasNoReadonlyFields.js @@ -10,7 +10,7 @@ const ERROR_MESSAGE = 'The Update method request object must not include input f /** * Update method (PUT, PATCH) request objects must not include input fields (readOnly properties). * - * @param {object} input - An update operation request content version + * @param {string} input - An update operation request content version * @param {object} _ - Unused * @param {{ path: string[], documentInventory: object}} context - The context object containing the path and document */ diff --git a/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodResponseIsGetMethodResponse.js b/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodResponseIsGetMethodResponse.js index 5695aaae7a..8423124913 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodResponseIsGetMethodResponse.js +++ b/tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodResponseIsGetMethodResponse.js @@ -16,7 +16,7 @@ const ERROR_MESSAGE = /** * 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 {string} 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 */ diff --git a/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js b/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js index 29115e64a3..3b0021d6f9 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js @@ -4,8 +4,8 @@ * Deep schema structure equality check between two values * Compares property names and types, but not specific values * Does not handle circular references - * @param {*} object1 First schema to compare - * @param {*} object2 Second schema to compare + * @param {object} object1 First schema to compare + * @param {object} object2 Second schema to compare * @returns {boolean} Whether the schemas have identical structure */ export function isDeepEqual(object1, object2) { @@ -27,11 +27,15 @@ export function isDeepEqual(object1, object2) { if (!propKeys2.includes(key)) return false; // Check if the types match for each property - if (object1.properties[key].type !== object2.properties[key].type) return false; + if (object1.properties[key].type !== object2.properties[key].type) { + return false; + } // Recursively check nested objects if (typeof object1.properties[key] === 'object' && typeof object2.properties[key] === 'object') { - if (!isDeepEqual(object1.properties[key], object2.properties[key])) return false; + if (!isDeepEqual(object1.properties[key], object2.properties[key])) { + return false; + } } } } @@ -123,6 +127,7 @@ export function removePropertyKeys(schema, ...propertyNames) { return result; } + /** * Recursively removes properties for Response schemas * @param {object} schema The schema to process diff --git a/tools/spectral/ipa/rulesets/functions/utils/validations.js b/tools/spectral/ipa/rulesets/functions/utils/validations.js index 0e4062cfa9..a6b147ce36 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/validations.js +++ b/tools/spectral/ipa/rulesets/functions/utils/validations.js @@ -1,4 +1,6 @@ import { handleInternalError } from './collectionUtils.js'; +import { getSchemaRef } from './methodUtils.js'; +import { isDeepEqual, removeRequestProperties, removeResponseProperties } from './compareUtils.js'; /** * Common validation function for checking that responses have the expected status code. @@ -100,3 +102,62 @@ export function checkForbiddenPropertyAttributesAndReturnErrors( return errors; } + +/** + * Checks if a request body schema matches a response schema. + * writeOnly:true properties of the request will be ignored. + * readOnly:true properties of the response will be ignored. + * Returns errors if the schemas are not equal, ready to be used in a custom validation function. + * + * @param {string[]} path the path to the request object being evaluated + * @param {object} requestContent the resolved request content for a media type + * @param {object} responseContent the resolved response content for a media type + * @param {object} requestContentUnresolved the unresolved request content for a media type + * @param {object} responseContentUnresolved the unresolved response content for a media type + * @param {'Create' | 'Update'} requestMethod the method of the request, e.g. 'create', 'update' + * @param {'Get' | 'List'} responseMethod the method of the response, e.g. 'get', 'list' + * @param {string} errorMessage the error message + * @returns {[{path, message: string}]} the errors found, or an empty array in case of no errors + */ +export function checkRequestResponseResourceEqualityAndReturnErrors( + path, + requestContent, + responseContent, + requestContentUnresolved, + responseContentUnresolved, + requestMethod, + responseMethod, + errorMessage +) { + const errors = []; + + if (!responseContent.schema) { + return [ + { + path, + message: `Could not validate that the ${requestMethod} request body schema matches the response schema of the ${responseMethod} method. The ${responseMethod} method does not have a schema.`, + }, + ]; + } + + const requestSchemaRef = getSchemaRef(requestContentUnresolved.schema); + const responseSchemaRef = getSchemaRef(responseContentUnresolved.schema); + + if (requestSchemaRef && responseSchemaRef) { + if (requestSchemaRef === responseSchemaRef) { + return []; + } + } + + const filteredRequestSchema = removeRequestProperties(requestContent.schema); + const filteredResponseSchema = removeResponseProperties(responseContent.schema); + + if (!isDeepEqual(filteredRequestSchema, filteredResponseSchema)) { + errors.push({ + path, + message: errorMessage, + }); + } + + return errors; +}