UNPKG

@postman/wsdl-to-postman

Version:

Convert a given WSDL specification (1.1) to Postman Collection

557 lines (534 loc) 23.4 kB
const { getElementPosition, addNodeInString, removeNodeFromString } = require('./../utils/mismatchUtils'), { XMLXSDValidator } = require('./../xsdValidation/XMLXSDValidator'), RESPONSE_BODY_PROPERTY = 'RESPONSE_BODY', BODY_PROPERTY = 'BODY', MISSING_IN_REQUEST_REASON_CODE = 'MISSING_IN_REQUEST', INVALID_TYPE_REASON_CODE = 'INVALID_TYPE', INVALID_BODY_REASON_CODE = 'INVALID_BODY', INVALID_RESPONSE_BODY_REASON_CODE = 'INVALID_RESPONSE_BODY', INVALID_BODY_DEFAULT_MESSAGE = 'The request body didn\'t match the specified schema', MISSING_CHILD_PATTERN = /Element \'(.*)\': Missing child element\(s\). Expected is \( (.*) \)/, NOT_EXPECTED_ONE_SIBLING_PATTERN = /Element \'(.*)\': This element is not expected\. Expected is \( (.*) \)/, NOT_EXPECTED_MULTIPLE_SIBLINGS_PATTERN = /Element \'(.*)\': This element is not expected. Expected is one of ( (.*) )/, INVALID_TYPE_PATTERN = /Element \'(.*)\': \'(.*)\' is not a valid value of the atomic type \'(.*)\'/, CONTENT_NOT_ALLOWED_PATTERN = /Element \'(.*)\': Character content is not allowed, because the content type is empty/, INVALID_XML_STRING_PATTERN = /Error validating message/, INVALID_ROOT_PATTERN = /Element \'(.*)\': No matching global declaration available for the validation root/, NOT_EXPECTED_IN_BODY_PATTERN = /Element \'(.*)\': This element is not expected/; class BodyMismatch { constructor(error, currentBody, expectedBody, operation, isResponse, options) { this.currentBody = currentBody; this.expectedBody = expectedBody; this.isResponse = isResponse; this.detailedBlobValidation = options.detailedBlobValidation; this.suggestAvailableFixes = options.suggestAvailableFixes; this.lib = new XMLXSDValidator().getValidatorLibrary(); this.schemaJsonPath = operation.xpathInfo.xpath; this.reason = this.getReason(error); this.elementData = this.getElementData(this.reason, currentBody, expectedBody); } /** * Return the mismatch formatted * @returns {object} Mismatch data formatted */ getMismatch() { let mismatch = { property: this.isResponse ? RESPONSE_BODY_PROPERTY : BODY_PROPERTY, transactionJsonPath: this.isResponse ? '$.response.body' : '$.request.body', schemaJsonPath: this.schemaJsonPath, reasonCode: this.getReasonCode(), reason: this.reason }; if (this.suggestAvailableFixes) { mismatch.suggestedFix = this.getSuggestedFix(); } return mismatch; } /** * Calculates and returns the corresponding message to the mismatch * @param {object} error The error generated from the validation library * @returns {string} the reason from the error or the default error depending on detailedBlobValidation option */ getReason(error) { return this.detailedBlobValidation ? error.message : INVALID_BODY_DEFAULT_MESSAGE; } /** * Calculates and return reason code depending on the detailedBlobValidation option * @returns {string} Reason code related to the mismatch */ getReasonCode() { return this.detailedBlobValidation ? this.elementData.reasonCode : this.isResponse ? INVALID_RESPONSE_BODY_REASON_CODE : INVALID_BODY_REASON_CODE; } /** * Formats and returns the suggestedFix value * @returns {object} the suggestedFix formatted */ getSuggestedFix() { let suggestedFix = { key: `/${this.elementData.keyPath}`, actualValue: this.elementData.currentValue, suggestedValue: this.elementData.suggestedValue }; if (this.detailedBlobValidation) { suggestedFix.key = `/${this.elementData.keyPath}`; suggestedFix.actualValue = this.elementData.currentValue; suggestedFix.suggestedValue = this.elementData.suggestedValue; } return suggestedFix; } /** * Handle the current and expected body strings depending on the error message and return the data necessary * by the mismatch creation * @param {string} reason The mismatch reason * @param {string} currentBody The body provided by the user * @param {string} expectedBody The expected body calculated from the schema * @returns {object} An object with the data related with the error message */ getElementData(reason, currentBody, expectedBody) { let data; if (reason.match(MISSING_CHILD_PATTERN)) { data = this.getMissingChildData(reason, currentBody, expectedBody); } else if ( reason.match(NOT_EXPECTED_ONE_SIBLING_PATTERN) || reason.match(NOT_EXPECTED_MULTIPLE_SIBLINGS_PATTERN) ) { data = this.getNotExpectedBySiblingsData(reason, currentBody, expectedBody); } else if (reason.match(NOT_EXPECTED_IN_BODY_PATTERN)) { data = this.getNotExpectedInBodyData(reason, currentBody, expectedBody); } else if (reason.match(INVALID_TYPE_PATTERN)) { data = this.getInvalidTypeData(reason, currentBody, expectedBody); } else if (reason.match(CONTENT_NOT_ALLOWED_PATTERN)) { data = this.getContentNotAllowedData(reason, currentBody, expectedBody); } else if ( reason.match(INVALID_XML_STRING_PATTERN) || reason.match(INVALID_ROOT_PATTERN) || reason.match(INVALID_BODY_DEFAULT_MESSAGE) ) { data = this.getGlobalErrorsData(); } return data; } /** * If the xpath contains an index component at the end it removes it and returns the clean xpath * @param {string} xpath A provided xpath * @returns {string} If the xpath contains an index component at the end it returns the xpath * with this component removed */ removeIndexFromXpath(xpath) { const reversedXpathAsArray = xpath.split('').reverse(); let cleanedXpath = xpath; if (reversedXpathAsArray[0] === ']') { cleanedXpath = reversedXpathAsArray.slice(3).reverse().join(''); } return cleanedXpath; } /** * Calculate the data when mismatch reason matches with a NOT_EXPECTED_IN_BODY_PATTERN message * @param {string} reason The reason related to the mismatch * @param {string} currentBody The body provided by the user * @param {string} expectedBody the expected body generated from the schema * @returns {object} an object with the data related to the mismatch */ getNotExpectedInBodyData(reason, currentBody, expectedBody) { const result = reason.match(NOT_EXPECTED_IN_BODY_PATTERN), elementName = result[1], elementXpath = `//${elementName}`, wrongNodesInCurrent = this.lib.findByXpathInXmlString(currentBody, elementXpath), wrongParentNode = wrongNodesInCurrent.map((node) => { return node.parent(); }).find((node) => { const nodeInExpected = this.lib.findByXpathInXmlString(expectedBody, `/${node.path()}`)[0], nodeInExpectedData = this.getNodeData(nodeInExpected); if (!nodeInExpectedData.childrenNames.includes(elementName)) { return true; } return false; }), wrongParentNodeData = this.getNodeData(wrongParentNode), wrongNode = wrongParentNodeData.children.find((child) => { return child.name() === elementName; }), currentValue = wrongParentNodeData.childrenAsString, suggestedValue = removeNodeFromString(wrongNode, wrongParentNodeData.nodeAsString), keyPath = wrongParentNodeData.path, reasonCode = this.isResponse ? INVALID_RESPONSE_BODY_REASON_CODE : INVALID_BODY_REASON_CODE; return { currentValue, suggestedValue, keyPath, reasonCode }; } /** * Calculate the data when mismatch reason matches with a MISSING_CHILD_PATTERN message * @param {string} reason The reason related to the mismatch * @param {string} currentBody The body provided by the user * @param {string} expectedBody the expected body generated from the schema * @returns {object} an object with the data related to the mismatch */ getMissingChildData(reason, currentBody, expectedBody) { const result = reason.match(MISSING_CHILD_PATTERN), name = result[2], parent = result[1], parentXpath = `//${parent}`, parentNodeDataWithoutChildren = this.lib.findByXpathInXmlString(currentBody, parentXpath).map((node) => { return this.getNodeData(node); }).filter((nodeData) => { return nodeData.children.length === 0; })[0], reasonCode = MISSING_IN_REQUEST_REASON_CODE, keyPath = parentNodeDataWithoutChildren.path, elementNodeInExpected = this.lib.findByXpathInXmlString( expectedBody, `${this.removeIndexFromXpath(keyPath)}/${name}` )[0], currentValue = '', suggestedValue = elementNodeInExpected.toString(); return { reasonCode, keyPath, currentValue, suggestedValue }; } /** * Gets the preview sibling xpath to find the next one and returns the missed node * @param {string} currentBody The provided body * @param {string} cleanBody the expected body generated from the schema * @param {string} siblingXpath the xpath related to the missed sibling from a NOT_EXPECTED_ONE_SIBLING_PATTERN reason * @returns {object} the node that corresponds with the missed element finding by the next sibling */ getElementFromMissingInRequestBySiblingsXpath(currentBody, cleanBody, siblingXpath) { const currentElements = this.lib.findByXpathInXmlString(currentBody, siblingXpath), expectedElements = this.lib.findByXpathInXmlString(cleanBody, siblingXpath), currentPrevElement = currentElements.map((element) => { return element.prevElement(); }), expectedPrevElement = expectedElements.map((element) => { return element.prevElement(); }), result = expectedPrevElement.filter((element) => { return !currentPrevElement.includes(element); }); return result; } /** * Gets a node an calculates specified data relatet to it * @param {object} node the node we want to get the data * @returns {object} an object with some data related to the provided node */ getNodeData(node) { const path = node.path(), children = this.lib.getChildrenElements(node), childrenNames = children.map((child) => { return child.name(); }), childrenAsString = this.lib.getChildrenAsString([node]), nodeAsString = node.toString(); return { node, path, children, childrenNames, childrenAsString, nodeAsString }; } /** * Handles the missing in request error when both elements in the reason message are expected in the schema * and returns the parent's data * @param {string} currentBody the body provided by the user * @param {string} expectedBody the body generated from the schema * @param {string} parentXpath the parent xpath of both elements * @param {string} missedElementName the name of the missed element in body * @param {string} wrongElementName the name of the element that has generated the mismatch * @returns {object} the parent element data(suggested and current values, reason code and keyPath) */ handleBothElementsAreExpected(currentBody, expectedBody, parentXpath, missedElementName, wrongElementName) { const expectedNode = this.lib.findByXpathInXmlString(expectedBody, parentXpath)[0], expectedNodeData = this.getNodeData(expectedNode), expectedMissedelementIndex = expectedNodeData.childrenNames.indexOf(missedElementName), providedNodes = this.lib.findByXpathInXmlString(currentBody, parentXpath), providedNodesData = providedNodes.map((node, index) => { return { ...this.getNodeData(node), index }; }), wrongNodeWithErrorData = providedNodesData.map((nodeData) => { const missedElementIndex = nodeData.childrenNames.indexOf(missedElementName), isMissedElement = missedElementIndex === -1, isWrongOrder = missedElementIndex > -1 && missedElementIndex !== expectedMissedelementIndex; return { ...nodeData, isMissedElement, isWrongOrder }; }).find((nodeData) => { const { isMissedElement, isWrongOrder } = nodeData; return isMissedElement || isWrongOrder; }); let currentValue, suggestedValue, reasonCode; if (wrongNodeWithErrorData.isMissedElement) { const missedElementXpath = `//${missedElementName}`, expectedElement = this.lib.findByXpathInXmlString(expectedNodeData.nodeAsString, missedElementXpath)[0]; currentValue = wrongNodeWithErrorData.childrenAsString; suggestedValue = addNodeInString(expectedElement, wrongNodeWithErrorData.nodeAsString); reasonCode = MISSING_IN_REQUEST_REASON_CODE; } else if (wrongNodeWithErrorData.isWrongOrder) { const missedNodeXpath = `//${missedElementName}`, wrongNodeXpath = `//${wrongElementName}`, missedNode = this.lib.findByXpathInXmlString(wrongNodeWithErrorData.nodeAsString, missedNodeXpath)[0], wrongNode = this.lib.findByXpathInXmlString(wrongNodeWithErrorData.nodeAsString, wrongNodeXpath)[0], missedNodePosition = getElementPosition(missedNode).index, wrongNodePosition = getElementPosition(wrongNode).index, childrenAsStringArray = wrongNodeWithErrorData.children.map((child) => { return child.toString(); }), swapElementsInArray = (elementsAsStringArray, index1, index2) => { const newOrderedArray = elementsAsStringArray.slice(), temporalElement = newOrderedArray[index1]; newOrderedArray[index1] = newOrderedArray[index2]; newOrderedArray[index2] = temporalElement; return newOrderedArray; }, newOrderedChildrenArray = swapElementsInArray(childrenAsStringArray, missedNodePosition, wrongNodePosition); currentValue = wrongNodeWithErrorData.childrenAsString; suggestedValue = newOrderedChildrenArray.join('\n'); reasonCode = this.isResponse ? INVALID_RESPONSE_BODY_REASON_CODE : INVALID_BODY_REASON_CODE; } return { keyPath: wrongNodeWithErrorData.path, currentValue, suggestedValue, reasonCode }; } /** * Switch the element in the provided position with the value provided and returns a new array * @param {array} myArray The array where we want to switch an element * @param {number} index The position of the element we want to change * @param {any} newValue The new value we want to put in the provided position * @returns {array} A new array with the specifyed element switched with the provided value */ switchValueInArray(myArray, index, newValue) { const newArray = myArray.slice(); newArray[index] = newValue; return newArray; } /** * Handles the missing in request error when the wrong element specified in the reason * is not expected by the schema and return the parent's data * @param {string} currentBody the body provided by the user * @param {string} expectedBody the body generated from the schema * @param {string} wrongElementName the not expected element in the schema from the reason * @param {string} missedElementName the eexpected element name in the schema * @returns {object} the data from the parent node of the specified elements */ handleNotExpectedElement(currentBody, expectedBody, wrongElementName, missedElementName) { const wrongNodeXpath = `//${wrongElementName}`, missedElementXpath = `//${missedElementName}`, expectedNodes = this.lib.findByXpathInXmlString(expectedBody, missedElementXpath), wrongNodesInCurrent = this.lib.findByXpathInXmlString(currentBody, wrongNodeXpath), intersection = this.getNodesIntersectionByParentName(expectedNodes, wrongNodesInCurrent), wrongParentNode = intersection[0], wrongParentNodeData = this.getNodeData(wrongParentNode), parentNodeXpathInExpected = this.removeIndexFromXpath(wrongParentNodeData.path), missedElementInExpected = this.lib.findByXpathInXmlString( expectedBody, `/${parentNodeXpathInExpected}/${missedElementName}` )[0], wrongElementInWrongParent = this.lib.findByXpathInXmlString( currentBody, `/${wrongParentNodeData.path}/${wrongElementName}` )[0], missedElementAsString = missedElementInExpected.toString(), wrongElementPositionInCurrent = wrongParentNodeData.childrenNames.indexOf(wrongElementName), currentValue = wrongParentNodeData.childrenAsString, wrongParentNodeChildrenAsArrayOfStrings = wrongParentNodeData.children.map((child) => { return child.toString(); }), wrongParentContainsMissedElement = wrongParentNodeData.childrenNames.includes(missedElementName), suggestedValue = wrongParentContainsMissedElement ? removeNodeFromString(wrongElementInWrongParent, wrongParentNodeData.nodeAsString) : this.switchValueInArray( wrongParentNodeChildrenAsArrayOfStrings, wrongElementPositionInCurrent, missedElementAsString ).join('\n'), reasonCode = MISSING_IN_REQUEST_REASON_CODE; return { keyPath: wrongParentNodeData.path, currentValue, suggestedValue, reasonCode }; } /** * Proces the reason and bodies to return the data related with the mismatched elements'parent * @param {string} reason the mismatch reason message * @param {string} currentBody the provided body by the user * @param {string} expectedBody the body generated from the schema * @returns {object} the data related with the parent of the wrong element */ getNotExpectedBySiblingsData(reason, currentBody, expectedBody) { const result = reason.match(NOT_EXPECTED_ONE_SIBLING_PATTERN) ? reason.match(NOT_EXPECTED_ONE_SIBLING_PATTERN) : reason.match(NOT_EXPECTED_MULTIPLE_SIBLINGS_PATTERN), missedElementName = result[2], wrongElementName = result[1], missedElementXpath = `//${missedElementName}`, wrongElementXpath = `//${wrongElementName}`, missedElementNodesInExpected = this.lib.findByXpathInXmlString(expectedBody, missedElementXpath), wrongElementNodesInExpected = this.lib.findByXpathInXmlString(expectedBody, wrongElementXpath), bothElementsInExpectedIntersection = this.getNodesIntersectionByParentPath( missedElementNodesInExpected, wrongElementNodesInExpected ), bothElementsAreExpected = bothElementsInExpectedIntersection.length === 1; let nodeData; if (bothElementsAreExpected) { const parentXpath = bothElementsInExpectedIntersection[0]; nodeData = this.handleBothElementsAreExpected( currentBody, expectedBody, parentXpath, missedElementName, wrongElementName ); } else { nodeData = this.handleNotExpectedElement(currentBody, expectedBody, wrongElementName, missedElementName); } return nodeData; } /** * Calculates the intersection between two arrays using the array element's parents * @param {array} nodesArray1 The array of nodes from the left * @param {array} nodesArray2 The array of nodes from the right * @returns {array} the intersection between the provided arrays using the parent's path */ getNodesIntersectionByParentPath(nodesArray1, nodesArray2) { const array1ParentsPath = nodesArray1.map((node) => { return node.parent().path(); }), array2ParentsPath = nodesArray2.map((node) => { return node.parent().path(); }), intersection = array1ParentsPath.filter((path) => { return array2ParentsPath.includes(path); }); return intersection; } /** * Calculates the intersection between the provided arrays using the array element's parent name * @param {aray} missedNodesInExpected The array of nodes from the left * @param {array} wrongNodesInCurrent The array of nodes from the right * @returns {array} the intersection between the provided arrays using the element's parent names */ getNodesIntersectionByParentName(missedNodesInExpected, wrongNodesInCurrent) { const wrongNodesParents = wrongNodesInCurrent.map((node) => { return node.parent(); }), missedNodesNames = missedNodesInExpected.map((node) => { return node.parent().name(); }), intersection = wrongNodesParents.filter((node) => { return missedNodesNames.includes(node.name()); }); return intersection; } /** * Calculates the data when the reason message matches with the INVALID_TYPE_PATTERN message * @param {string} reason the reason of the mismatch * @param {string} currentBody the body provided by the user * @param {string} expectedBody the body generated from the schema * @returns {object} the data related to the invalid type element */ getInvalidTypeData(reason, currentBody, expectedBody) { const result = reason.match(INVALID_TYPE_PATTERN), name = result[1], value = result[2], elementXpath = `//${name}[text()="${value}"]`, elementInCurrent = this.lib.findByXpathInXmlString(currentBody, elementXpath)[0], reasonCode = INVALID_TYPE_REASON_CODE, keyPath = elementInCurrent.path(), elementInExpected = this.lib.findByXpathInXmlString(expectedBody, keyPath)[0], currentValue = elementInCurrent.text(), suggestedValue = elementInExpected.text(); return { currentValue, suggestedValue, reasonCode, keyPath }; } /** * Calculates the data when the reason message matches with the NOT_ALLOWED_CONTENT_PATTERN message * @param {string} reason the reason of the mismatch * @param {string} currentBody the body provided by the user * @param {string} expectedBody the body generated from the schema * @returns {object} the data related to the mismatched element */ getContentNotAllowedData(reason, currentBody, expectedBody) { const result = reason.match(CONTENT_NOT_ALLOWED_PATTERN), name = result[1], elementXpath = `//${name}[count(*)=0]`, elementInExpected = this.lib.findByXpathInXmlString(expectedBody, elementXpath)[0], keyPath = elementInExpected.path(), reasonCode = this.isResponse ? INVALID_RESPONSE_BODY_REASON_CODE : INVALID_BODY_REASON_CODE, elementInCurrent = this.lib.findByXpathInXmlString(currentBody, keyPath)[0], currentValue = elementInCurrent.text(), suggestedValue = ''; return { currentValue, suggestedValue, reasonCode, keyPath }; } /** * Calculates the data when the reason message matches with the INVALID_XML_STRING and INVALID_ELEMENT_ROOT patterns * @param {string} reason the reason of the mismatch * @param {string} currentBody the body provided by the user * @param {string} expectedBody the body generated from the schema * @returns {object} the data related to the invalid type element */ getGlobalErrorsData() { const currentValue = this.currentBody, suggestedValue = this.expectedBody, keyPath = '/soap:Body', reasonCode = this.isResponse ? INVALID_RESPONSE_BODY_REASON_CODE : INVALID_BODY_REASON_CODE; return { currentValue, suggestedValue, keyPath, reasonCode }; } } module.exports = { BodyMismatch };