Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions tools/spectral/ipa/rulesets/IPA-107.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ functions:
- IPA107UpdateResponseCodeShouldBe200OK
- IPA107UpdateMethodResponseIsGetMethodResponse
- IPA107UpdateMethodRequestHasNoReadonlyFields
- IPA107UpdateMethodRequestBodyIsGetResponse

rules:
xgen-IPA-107-put-must-not-have-query-params:
Expand Down Expand Up @@ -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'
14 changes: 14 additions & 0 deletions tools/spectral/ipa/rulesets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -48,12 +55,15 @@ export default (input, _, { path, documentInventory }) => {
unresolvedOas
);

const errors = checkViolationsAndReturnErrors(
const errors = checkRequestResponseResourceEqualityAndReturnErrors(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created common function to be reused

path,
postRequestContentPerMediaType,
getResponseContentPerMediaType,
postRequestContentPerMediaTypeUnresolved,
getResponseContentPerMediaTypeUnresolved
getResponseContentPerMediaTypeUnresolved,
'Create',
'Get',
ERROR_MESSAGE
);

if (errors.length !== 0) {
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
@@ -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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Create method checks for equality across all fields except those marked as readOnly or writeOnly. Should we apply the same equality check for the Update method?

The description also refers to this as "the resource or parts of the resource returned by the Get method."

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline: IaC tools require Update request schemas to follow the same requirements as Create request schemas

path,
updateMethodRequest,
getMethodResponse,
updateMethodRequestUnresolved,
getMethodResponseUnresolved,
'Update',
'Get',
ERROR_MESSAGE
);

if (errors.length !== 0) {
return collectAndReturnViolation(path, RULE_NAME, errors);
}

collectAdoption(path, RULE_NAME);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
13 changes: 9 additions & 4 deletions tools/spectral/ipa/rulesets/functions/utils/compareUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
}
}
}
Expand Down Expand Up @@ -123,6 +127,7 @@ export function removePropertyKeys(schema, ...propertyNames) {

return result;
}

/**
* Recursively removes properties for Response schemas
* @param {object} schema The schema to process
Expand Down
61 changes: 61 additions & 0 deletions tools/spectral/ipa/rulesets/functions/utils/validations.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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 postRequestSchemaRef = getSchemaRef(requestContentUnresolved.schema);
const getResponseSchemaRef = getSchemaRef(responseContentUnresolved.schema);

if (postRequestSchemaRef && getResponseSchemaRef) {
if (postRequestSchemaRef === getResponseSchemaRef) {
return [];
}
}

const filteredRequestSchema = removeRequestProperties(requestContent.schema);
const filteredResponseSchema = removeResponseProperties(responseContent.schema);

if (!isDeepEqual(filteredRequestSchema, filteredResponseSchema)) {
errors.push({
path,
message: errorMessage,
});
}

return errors;
}
Loading