lib/armValidator/roundTripValidator.ts (192 lines of code) (raw):

import { getJsonPatchDiff } from "../apiScenario/diffUtils"; import { RequestResponsePair, LiveValidationIssue } from "../liveValidation/liveValidator"; import { OperationContext } from "../liveValidation/operationValidator"; import { roundTripValidationErrors } from "../util/errorDefinitions"; import * as utils from "../util/utils"; import { Parameter, Operation } from "../swagger/swaggerTypes"; import { JsonLoader } from "../swagger/jsonLoader"; import { SchemaSearcher } from "../apiScenario/schemaSearcher"; const allowed = true; const notAllowed = false; function checkReplacedSchemaInParameter( jsonPath: string, parameter: Parameter, jsonLoader: JsonLoader ) { const subSet = (currentValue: string) => ["create", "read"].includes(currentValue); if (parameter.in === "body") { const schema = jsonLoader.resolveRefObj(parameter.schema!); const foundSchema = SchemaSearcher.findSchemaByJsonPointer(jsonPath, schema, jsonLoader); if ( foundSchema.readOnly || foundSchema.default || (foundSchema["x-ms-mutability"] && foundSchema["x-ms-mutability"].every(subSet)) ) { return allowed; } return notAllowed; } return notAllowed; } function checkRemovedSchemaInParameter( jsonPath: string, parameter: Parameter, jsonLoader: JsonLoader ) { const subSet = (currentValue: string) => ["create", "update"].includes(currentValue); if (parameter.in === "body") { const schema = jsonLoader.resolveRefObj(parameter.schema!); const foundSchema = SchemaSearcher.findSchemaByJsonPointer(jsonPath, schema, jsonLoader); if ( foundSchema["x-ms-secret"] || (foundSchema["x-ms-mutability"] && foundSchema["x-ms-mutability"].every(subSet)) ) { return allowed; } return notAllowed; } return notAllowed; } function checkSchemaInResponse( jsonPath: string, op: Operation, jsonLoader: JsonLoader, responseStatusCode: string ) { let statusCode; if (isNaN(+responseStatusCode)) { statusCode = utils.statusCodeStringToStatusCode[responseStatusCode.toLowerCase()]; if (statusCode === undefined) { statusCode = "default"; } } else { statusCode = responseStatusCode; } let responseSchema: any = op.responses[statusCode]; if (responseSchema === undefined) { responseSchema = op.responses["default"]; } if (responseSchema.schema) { responseSchema = responseSchema.schema; } const schema = jsonLoader.resolveRefObj(responseSchema); const foundSchema = SchemaSearcher.findSchemaByJsonPointer(jsonPath, schema, jsonLoader); if (foundSchema.readOnly || foundSchema.default) { return allowed; } return notAllowed; } export function diffRequestResponse( payload: RequestResponsePair, info: OperationContext, jsonLoader: JsonLoader ) { const diffs = getJsonPatchDiff(payload.liveRequest.body ?? {}, payload.liveResponse.body ?? {}, { includeOldValue: true, minimizeDiff: false, }); const rest = diffs .map((it: any) => { const jsonPath: string = it.remove || it.add || it.replace; if (it.replace !== undefined) { let isAllowed = false; for (let parameter of info.operationMatch?.operation.parameters ?? []) { if (isAllowed) { break; } isAllowed = checkReplacedSchemaInParameter(it.replace, parameter, jsonLoader); } for (let parameter of info.operationMatch?.operation._path.parameters ?? []) { if (isAllowed) { break; } isAllowed = checkReplacedSchemaInParameter(it.replace, parameter, jsonLoader); } if (!isAllowed) { return buildLiveValidationIssue("ROUNDTRIP_INCONSISTENT_PROPERTY", jsonPath, it); } } else if (it.add !== undefined && it.value !== null) { // IF a property is not in request but returned in response as null, ignore. let isAllowed = checkSchemaInResponse( it.add, info.operationMatch?.operation!, jsonLoader, payload.liveResponse.statusCode ); if (!isAllowed) { return buildLiveValidationIssue("ROUNDTRIP_ADDITIONAL_PROPERTY", jsonPath, it); } } else if (it.remove !== undefined) { let isAllowed = false; for (let parameter of info.operationMatch?.operation.parameters ?? []) { if (isAllowed) { break; } isAllowed = checkRemovedSchemaInParameter(it.remove, parameter, jsonLoader); } for (let parameter of info.operationMatch?.operation._path.parameters ?? []) { if (isAllowed) { break; } isAllowed = checkRemovedSchemaInParameter(it.remove, parameter, jsonLoader); } if (!isAllowed) { return buildLiveValidationIssue("ROUNDTRIP_MISSING_PROPERTY", jsonPath, it); } } return undefined; }) .filter((a) => a !== undefined); return rest; } export function buildLiveValidationIssue( errorCode: string, path: string, it: any ): LiveValidationIssue { let severity, message; const properties = path.split("/"); let property = properties.pop(); if (!isNaN(Number(property)) && properties.length > 0) { property = `${properties.pop()}/${property}`; } switch (errorCode) { case "ROUNDTRIP_INCONSISTENT_PROPERTY": { severity = roundTripValidationErrors.ROUNDTRIP_INCONSISTENT_PROPERTY.severity; message = roundTripValidationErrors.ROUNDTRIP_INCONSISTENT_PROPERTY.message({ getValue: it.value, putValue: it.oldValue, }); break; } case "ROUNDTRIP_ADDITIONAL_PROPERTY": { severity = roundTripValidationErrors.ROUNDTRIP_ADDITIONAL_PROPERTY.severity; message = roundTripValidationErrors.ROUNDTRIP_ADDITIONAL_PROPERTY.message({ property: property, }); break; } case "ROUNDTRIP_MISSING_PROPERTY": { severity = roundTripValidationErrors.ROUNDTRIP_MISSING_PROPERTY.severity; message = roundTripValidationErrors.ROUNDTRIP_MISSING_PROPERTY.message({ property: property, }); break; } } const ret = { code: errorCode, pathsInPayload: [path], severity: severity, message: message, jsonPathsInPayload: [], schemaPath: "", source: { url: "", position: { column: 0, line: 0, }, }, }; return ret as LiveValidationIssue; }