Skip to content

Commit 170820f

Browse files
CLOUDP-304942: Adds xgen-IPA-107-update-method-request-body-is-get-method-response (#567)
1 parent 979483b commit 170820f

9 files changed

+854
-50
lines changed

tools/spectral/ipa/__tests__/IPA107UpdateMethodRequestBodyIsGetResponse.test.js

Lines changed: 666 additions & 0 deletions
Large diffs are not rendered by default.

tools/spectral/ipa/rulesets/IPA-107.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ functions:
66
- IPA107UpdateResponseCodeShouldBe200OK
77
- IPA107UpdateMethodResponseIsGetMethodResponse
88
- IPA107UpdateMethodRequestHasNoReadonlyFields
9+
- IPA107UpdateMethodRequestBodyIsGetResponse
910

1011
rules:
1112
xgen-IPA-107-put-must-not-have-query-params:
@@ -103,3 +104,21 @@ rules:
103104
then:
104105
field: '@key'
105106
function: 'IPA107UpdateMethodRequestHasNoReadonlyFields'
107+
xgen-IPA-107-update-method-request-body-is-get-method-response:
108+
description: |
109+
The request body must contain the resource being updated, i.e. the resource or parts of the resource returned by the Get method.
110+
111+
##### Implementation details
112+
113+
Validation checks the PATCH/PUT methods for single resource paths.
114+
- Validation ignores resources without a Get method.
115+
- `readOnly:true` properties of Get method response will be ignored.
116+
- `writeOnly:true` properties of Update method request will be ignored.
117+
- Property comparison is based on `type` and `name` matching.
118+
- `oneOf` and `discriminator` definitions must match exactly.
119+
message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-107-update-method-request-body-is-get-method-response:'
120+
severity: warn
121+
given: '$.paths[*][put,patch].requestBody.content'
122+
then:
123+
field: '@key'
124+
function: 'IPA107UpdateMethodRequestBodyIsGetResponse'

tools/spectral/ipa/rulesets/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,20 @@ Rule checks for the following conditions:
335335
- Searches through the request schema to find any properties marked with readOnly attribute
336336
- Fails if any readOnly properties are found in the request schema
337337

338+
#### xgen-IPA-107-update-method-request-body-is-get-method-response
339+
340+
![warn](https://img.shields.io/badge/warning-yellow)
341+
The request body must contain the resource being updated, i.e. the resource or parts of the resource returned by the Get method.
342+
343+
##### Implementation details
344+
345+
Validation checks the PATCH/PUT methods for single resource paths.
346+
- Validation ignores resources without a Get method.
347+
- `readOnly:true` properties of Get method response will be ignored.
348+
- `writeOnly:true` properties of Update method request will be ignored.
349+
- Property comparison is based on `type` and `name` matching.
350+
- `oneOf` and `discriminator` definitions must match exactly.
351+
338352

339353

340354
### IPA-108

tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsGetResponse.js

Lines changed: 14 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,22 @@ import {
55
isSingletonResource,
66
} from './utils/resourceEvaluation.js';
77
import { resolveObject } from './utils/componentUtils.js';
8-
import { isDeepEqual, removeRequestProperties, removeResponseProperties } from './utils/compareUtils.js';
98
import { hasException } from './utils/exceptions.js';
109
import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js';
11-
import { getResponseOfGetMethodByMediaType, getSchemaRef } from './utils/methodUtils.js';
10+
import { getResponseOfGetMethodByMediaType } from './utils/methodUtils.js';
11+
import { checkRequestResponseResourceEqualityAndReturnErrors } from './utils/validations.js';
1212

1313
const RULE_NAME = 'xgen-IPA-106-create-method-request-body-is-get-method-response';
1414
const ERROR_MESSAGE =
1515
'The request body schema properties must match the response body schema properties of the Get method.';
1616

17+
/**
18+
* Create method request body schema properties must match the response body schema properties of the Get method.
19+
*
20+
* @param {string} input - A create operation request content version
21+
* @param {object} _ - Unused
22+
* @param {{ path: string[], documentInventory: object}} context - The context object containing the path and document
23+
*/
1724
export default (input, _, { path, documentInventory }) => {
1825
const oas = documentInventory.resolved;
1926
const unresolvedOas = documentInventory.unresolved;
@@ -48,12 +55,15 @@ export default (input, _, { path, documentInventory }) => {
4855
unresolvedOas
4956
);
5057

51-
const errors = checkViolationsAndReturnErrors(
58+
const errors = checkRequestResponseResourceEqualityAndReturnErrors(
5259
path,
5360
postRequestContentPerMediaType,
5461
getResponseContentPerMediaType,
5562
postRequestContentPerMediaTypeUnresolved,
56-
getResponseContentPerMediaTypeUnresolved
63+
getResponseContentPerMediaTypeUnresolved,
64+
'Create',
65+
'Get',
66+
ERROR_MESSAGE
5767
);
5868

5969
if (errors.length !== 0) {
@@ -62,43 +72,3 @@ export default (input, _, { path, documentInventory }) => {
6272

6373
collectAdoption(path, RULE_NAME);
6474
};
65-
66-
function checkViolationsAndReturnErrors(
67-
path,
68-
postRequestContentPerMediaType,
69-
getResponseContentPerMediaType,
70-
postRequestContentPerMediaTypeUnresolved,
71-
getResponseContentPerMediaTypeUnresolved
72-
) {
73-
const errors = [];
74-
75-
if (!getResponseContentPerMediaType.schema) {
76-
return [
77-
{
78-
path,
79-
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.`,
80-
},
81-
];
82-
}
83-
84-
const postRequestSchemaRef = getSchemaRef(postRequestContentPerMediaTypeUnresolved.schema);
85-
const getResponseSchemaRef = getSchemaRef(getResponseContentPerMediaTypeUnresolved.schema);
86-
87-
if (postRequestSchemaRef && getResponseSchemaRef) {
88-
if (postRequestSchemaRef === getResponseSchemaRef) {
89-
return [];
90-
}
91-
}
92-
93-
const filteredCreateRequestSchema = removeRequestProperties(postRequestContentPerMediaType.schema);
94-
const filteredGetResponseSchema = removeResponseProperties(getResponseContentPerMediaType.schema);
95-
96-
if (!isDeepEqual(filteredCreateRequestSchema, filteredGetResponseSchema)) {
97-
errors.push({
98-
path,
99-
message: ERROR_MESSAGE,
100-
});
101-
}
102-
103-
return errors;
104-
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { isSingleResourceIdentifier } from './utils/resourceEvaluation.js';
2+
import { resolveObject } from './utils/componentUtils.js';
3+
import { hasException } from './utils/exceptions.js';
4+
import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js';
5+
import { getGETMethodResponseSchemaFromPathItem } from './utils/methodUtils.js';
6+
import { checkRequestResponseResourceEqualityAndReturnErrors } from './utils/validations.js';
7+
8+
const RULE_NAME = 'xgen-IPA-107-update-method-request-body-is-get-method-response';
9+
const ERROR_MESSAGE =
10+
'The request body schema properties of the Update method must match the response body schema properties of the Get method.';
11+
12+
/**
13+
* Update method (PUT, PATCH) request body schema properties must match the response body schema properties of the Get method.
14+
*
15+
* @param {string} input - An update operation request content version
16+
* @param {object} _ - Unused
17+
* @param {{ path: string[], documentInventory: object}} context - The context object containing the path and document
18+
*/
19+
export default (input, _, { path, documentInventory }) => {
20+
const oas = documentInventory.resolved;
21+
const unresolvedOas = documentInventory.unresolved;
22+
const resourcePath = path[1];
23+
let mediaType = input;
24+
25+
if (!isSingleResourceIdentifier(resourcePath) || !mediaType.endsWith('json')) {
26+
return;
27+
}
28+
29+
// Ignore if the Update method does not have a request body schema
30+
const updateMethodRequest = resolveObject(oas, path);
31+
32+
if (!updateMethodRequest || !updateMethodRequest.schema) {
33+
return;
34+
}
35+
36+
if (hasException(updateMethodRequest, RULE_NAME)) {
37+
collectException(updateMethodRequest, RULE_NAME, path);
38+
return;
39+
}
40+
41+
// Ignore if there is no matching Get method
42+
const getMethodResponse = getGETMethodResponseSchemaFromPathItem(oas.paths[resourcePath], mediaType);
43+
if (!getMethodResponse) {
44+
return;
45+
}
46+
47+
const updateMethodRequestUnresolved = resolveObject(unresolvedOas, path);
48+
const getMethodResponseUnresolved = getGETMethodResponseSchemaFromPathItem(
49+
unresolvedOas.paths[resourcePath],
50+
mediaType
51+
);
52+
53+
const errors = checkRequestResponseResourceEqualityAndReturnErrors(
54+
path,
55+
updateMethodRequest,
56+
getMethodResponse,
57+
updateMethodRequestUnresolved,
58+
getMethodResponseUnresolved,
59+
'Update',
60+
'Get',
61+
ERROR_MESSAGE
62+
);
63+
64+
if (errors.length !== 0) {
65+
return collectAndReturnViolation(path, RULE_NAME, errors);
66+
}
67+
68+
collectAdoption(path, RULE_NAME);
69+
};

tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodRequestHasNoReadonlyFields.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const ERROR_MESSAGE = 'The Update method request object must not include input f
1010
/**
1111
* Update method (PUT, PATCH) request objects must not include input fields (readOnly properties).
1212
*
13-
* @param {object} input - An update operation request content version
13+
* @param {string} input - An update operation request content version
1414
* @param {object} _ - Unused
1515
* @param {{ path: string[], documentInventory: object}} context - The context object containing the path and document
1616
*/

tools/spectral/ipa/rulesets/functions/IPA107UpdateMethodResponseIsGetMethodResponse.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const ERROR_MESSAGE =
1616
/**
1717
* Update method (PUT, PATCH) responses should reference the same schema as the Get method.
1818
*
19-
* @param {object} input - An update operation 200 response content version
19+
* @param {string} input - An update operation 200 response content version
2020
* @param {object} _ - Unused
2121
* @param {{ path: string[], documentInventory: object}} context - The context object containing the path and document
2222
*/

tools/spectral/ipa/rulesets/functions/utils/compareUtils.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
* Deep schema structure equality check between two values
55
* Compares property names and types, but not specific values
66
* Does not handle circular references
7-
* @param {*} object1 First schema to compare
8-
* @param {*} object2 Second schema to compare
7+
* @param {object} object1 First schema to compare
8+
* @param {object} object2 Second schema to compare
99
* @returns {boolean} Whether the schemas have identical structure
1010
*/
1111
export function isDeepEqual(object1, object2) {
@@ -27,11 +27,15 @@ export function isDeepEqual(object1, object2) {
2727
if (!propKeys2.includes(key)) return false;
2828

2929
// Check if the types match for each property
30-
if (object1.properties[key].type !== object2.properties[key].type) return false;
30+
if (object1.properties[key].type !== object2.properties[key].type) {
31+
return false;
32+
}
3133

3234
// Recursively check nested objects
3335
if (typeof object1.properties[key] === 'object' && typeof object2.properties[key] === 'object') {
34-
if (!isDeepEqual(object1.properties[key], object2.properties[key])) return false;
36+
if (!isDeepEqual(object1.properties[key], object2.properties[key])) {
37+
return false;
38+
}
3539
}
3640
}
3741
}
@@ -123,6 +127,7 @@ export function removePropertyKeys(schema, ...propertyNames) {
123127

124128
return result;
125129
}
130+
126131
/**
127132
* Recursively removes properties for Response schemas
128133
* @param {object} schema The schema to process

tools/spectral/ipa/rulesets/functions/utils/validations.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { handleInternalError } from './collectionUtils.js';
2+
import { getSchemaRef } from './methodUtils.js';
3+
import { isDeepEqual, removeRequestProperties, removeResponseProperties } from './compareUtils.js';
24

35
/**
46
* Common validation function for checking that responses have the expected status code.
@@ -100,3 +102,62 @@ export function checkForbiddenPropertyAttributesAndReturnErrors(
100102

101103
return errors;
102104
}
105+
106+
/**
107+
* Checks if a request body schema matches a response schema.
108+
* writeOnly:true properties of the request will be ignored.
109+
* readOnly:true properties of the response will be ignored.
110+
* Returns errors if the schemas are not equal, ready to be used in a custom validation function.
111+
*
112+
* @param {string[]} path the path to the request object being evaluated
113+
* @param {object} requestContent the resolved request content for a media type
114+
* @param {object} responseContent the resolved response content for a media type
115+
* @param {object} requestContentUnresolved the unresolved request content for a media type
116+
* @param {object} responseContentUnresolved the unresolved response content for a media type
117+
* @param {'Create' | 'Update'} requestMethod the method of the request, e.g. 'create', 'update'
118+
* @param {'Get' | 'List'} responseMethod the method of the response, e.g. 'get', 'list'
119+
* @param {string} errorMessage the error message
120+
* @returns {[{path, message: string}]} the errors found, or an empty array in case of no errors
121+
*/
122+
export function checkRequestResponseResourceEqualityAndReturnErrors(
123+
path,
124+
requestContent,
125+
responseContent,
126+
requestContentUnresolved,
127+
responseContentUnresolved,
128+
requestMethod,
129+
responseMethod,
130+
errorMessage
131+
) {
132+
const errors = [];
133+
134+
if (!responseContent.schema) {
135+
return [
136+
{
137+
path,
138+
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.`,
139+
},
140+
];
141+
}
142+
143+
const requestSchemaRef = getSchemaRef(requestContentUnresolved.schema);
144+
const responseSchemaRef = getSchemaRef(responseContentUnresolved.schema);
145+
146+
if (requestSchemaRef && responseSchemaRef) {
147+
if (requestSchemaRef === responseSchemaRef) {
148+
return [];
149+
}
150+
}
151+
152+
const filteredRequestSchema = removeRequestProperties(requestContent.schema);
153+
const filteredResponseSchema = removeResponseProperties(responseContent.schema);
154+
155+
if (!isDeepEqual(filteredRequestSchema, filteredResponseSchema)) {
156+
errors.push({
157+
path,
158+
message: errorMessage,
159+
});
160+
}
161+
162+
return errors;
163+
}

0 commit comments

Comments
 (0)