diff --git a/CHANGELOG.md b/CHANGELOG.md index 857ca9c32..fac6e3f66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ ## [Unreleased] +## [v4.18.0] - 2023-09-28 + +- [#425](https://github.com/postmanlabs/openapi-to-postman/issues/425) [8413](https://github.com/postmanlabs/postman-app-support/issues/8413) Added support for multiple request and response examples. + +## [v4.17.0] - 2023-09-12 + +## [v4.16.0] - 2023-08-18 + +### Added + +- Added support for remote $ref resolution in bundle() API. + ## [v4.15.0] - 2023-06-27 ### Added @@ -41,7 +53,7 @@ - Fixed an issue where definition validation was not considering multiple white space characters. - Fixed issue [#708](https://github.com/postmanlabs/openapi-to-postman/issues/708) where if string is defined for required field, conversion was failing. - Fixed issue where for certain path segments, collection generation failed. -- Fixed TypeError occurring while checking typeof bodyContent in getXmlVersionContent. +- Fixed TypeError occurring while checking typeof bodyContent in getXmlVersionContent. ## [v4.12.0] - 2023-05-04 @@ -295,7 +307,7 @@ Newer releases follow the [Keep a Changelog](https://keepachangelog.com/en/1.0.0 - Added support for internal $ref resolution in validation flows. - Fixed issue where parameter resolution was "schema" when "example" was specified. - Add supported formats for schema resolution (deref). -- Fix for [#7643](https://github.com/postmanlabs/postman-app-support/issues/7643), [#7914](https://github.com/postmanlabs/postman-app-support/issues/7914), [#9004](https://github.com/postmanlabs/postman-app-support/issues/9004) - Added support for Auth params in response/example. +- Fix for [#7643](https://github.com/postmanlabs/postman-app-support/issues/7643), [#7914](https://github.com/postmanlabs/postman-app-support/issues/7914), [#9004](https://github.com/postmanlabs/postman-app-support/issues/9004) - Added support for Auth params in response/example. - Bumped up multiple dependecies and dev-dependencies versions to keep them up-to-date. - Updated code coverage tool from deprecated istanbul to nyc. @@ -382,7 +394,7 @@ Newer releases follow the [Keep a Changelog](https://keepachangelog.com/en/1.0.0 #### v1.1.13 (April 21, 2020) - Added support for detailed validation body mismatches with option detailedBlobValidation. -- Fix for [#8098](https://github.com/postmanlabs/postman-app-support/issues/8098) - Unable to validate schema with type array. +- Fix for [#8098](https://github.com/postmanlabs/postman-app-support/issues/8098) - Unable to validate schema with type array. - Fixed URIError for invalid URI in transaction. - Fix for [#152](https://github.com/postmanlabs/openapi-to-postman/issues/152) - Path references not resolved due to improver handling of special characters. - Fix for [#160](https://github.com/postmanlabs/openapi-to-postman/issues/160) - Added handling for variables in local servers not a part of a URL segment. All path servers to be added as collection variables. @@ -588,7 +600,13 @@ Newer releases follow the [Keep a Changelog](https://keepachangelog.com/en/1.0.0 - Base release -[Unreleased]: https://github.com/postmanlabs/openapi-to-postman/compare/v4.15.0...HEAD +[Unreleased]: https://github.com/postmanlabs/openapi-to-postman/compare/v4.18.0...HEAD + +[v4.18.0]: https://github.com/postmanlabs/openapi-to-postman/compare/v4.17.0...v4.18.0 + +[v4.17.0]: https://github.com/postmanlabs/openapi-to-postman/compare/v4.16.0...v4.17.0 + +[v4.16.0]: https://github.com/postmanlabs/openapi-to-postman/compare/v4.15.0...v4.16.0 [v4.15.0]: https://github.com/postmanlabs/openapi-to-postman/compare/v4.14.0...v4.15.0 diff --git a/lib/bundle.js b/lib/bundle.js index c4a2da3fe..1cd82d8fc 100644 --- a/lib/bundle.js +++ b/lib/bundle.js @@ -1,10 +1,14 @@ const _ = require('lodash'), { isExtRef, + isExtURLRef, + stringIsAValidUrl, + isExtRemoteRef, getKeyInComponents, getJsonPointerRelationToRoot, removeLocalReferenceFromPath, localPointer, + httpSeparator, jsonPointerLevelSeparator, isLocalRef, jsonPointerDecodeAndReplace, @@ -83,14 +87,21 @@ function calculatePath(parentFileName, referencePath) { * @returns {object} - Detect root files result object */ function findNodeFromPath(referencePath, allData) { - const partialComponents = referencePath.split(localPointer); - let isPartial = partialComponents.length > 1, - node = allData.find((node) => { - if (isPartial) { - referencePath = partialComponents[0]; - } - return comparePaths(node.fileName, referencePath); - }); + const isReferenceRemoteURL = stringIsAValidUrl(referencePath), + partialComponents = referencePath.split(localPointer), + isPartial = partialComponents.length > 1; + + let node = allData.find((node) => { + if (isPartial && !isReferenceRemoteURL) { + referencePath = partialComponents[0]; + } + + if (isReferenceRemoteURL) { + return _.startsWith(node.path, referencePath); + } + + return comparePaths(node.fileName, referencePath); + }); return node; } @@ -290,13 +301,86 @@ function handleLocalCollisions(trace, initialMainKeys) { * @param {string} commonPathFromData - The common path in the file's paths * @param {Array} allData - array of { path, content} objects * @param {object} globalReferences - The accumulated global references from all nodes + * @param {function} remoteRefResolver - The function that would be called to fetch remote ref contents * @returns {object} - The references in current node and the new content from the node */ -function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename, version, rootMainKeys, - commonPathFromData, allData, globalReferences) { +async function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename, version, rootMainKeys, + commonPathFromData, allData, globalReferences, remoteRefResolver) { let referencesInNode = [], nodeReferenceDirectory = {}, - mainKeys = {}; + mainKeys = {}, + remoteRefContentMap = new Map(), + remoteRefSet = new Set(), + remoteRefResolutionPromises = []; + + remoteRefResolver && traverseUtility(currentNode).forEach(function (property) { + if (property) { + let hasReferenceTypeKey; + + hasReferenceTypeKey = Object.keys(property) + .find( + (key) => { + const isExternal = isExtURLRef(property, key), + isReferenciable = isExternal; + + return isReferenciable; + } + ); + + if (hasReferenceTypeKey) { + const tempRef = calculatePath(parentFilename, property.$ref), + isRefEncountered = remoteRefSet.has(tempRef); + + if (isRefEncountered) { + return; + } + + remoteRefResolutionPromises.push( + new Promise(async (resolveInner) => { + + /** + * Converts contents received from remoteRefResolver into stringified JSON + * @param {string | object} content - contents from remoteRefResolver + * @returns {string} Stringified JSON contents + */ + function convertToJSONString (content) { + if (typeof content === 'object') { + return JSON.stringify(content); + } + + const parsedFile = parseFile(content); + + return JSON.stringify(parsedFile.oasObject); + } + + try { + let contentFromRemote = await remoteRefResolver(property.$ref), + nodeTemp = { + fileName: tempRef, + path: tempRef, + content: convertToJSONString(contentFromRemote), + href: property.$ref + }; + + remoteRefContentMap.set(tempRef, contentFromRemote); + + allData.push(nodeTemp); + } + catch (err) { + // swallow the err + } + finally { + resolveInner(); + } + }) + ); + + remoteRefSet.add(tempRef); + } + } + }); + + await Promise.all(remoteRefResolutionPromises); traverseUtility(currentNode).forEach(function (property) { if (property) { @@ -371,6 +455,94 @@ function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename, ve referencesInNode.push({ path: pathSolver(property), keyInComponents: nodeTrace, newValue: this.node }); } } + + const hasRemoteReferenceTypeKey = Object.keys(property) + .find( + (key) => { + const isExternal = isExtURLRef(property, key), + + // Only process URL refs if remoteRefResolver is provided and a valid function + isReferenciable = isExternal && _.isFunction(remoteRefResolver); + + return isReferenciable; + } + ), + handleRemoteURLReference = () => { + const tempRef = calculatePath(parentFilename, property.$ref); + + if (remoteRefContentMap.get(tempRef) === undefined) { + return; + } + + let nodeTrace = handleLocalCollisions( + getTraceFromParentKeyInComponents(this, tempRef, mainKeys, version, commonPathFromData), + rootMainKeys + ), + componentKey = nodeTrace[nodeTrace.length - 1], + referenceInDocument = getJsonPointerRelationToRoot( + tempRef, + nodeTrace, + version + ), + traceToParent = [...this.parents.map((item) => { + return item.key; + }).filter((item) => { + return item !== undefined; + }), this.key], + newValue = Object.assign({}, this.node), + [, local] = tempRef.split(localPointer), + nodeFromData, + refHasContent = false, + parseResult, + newRefInDoc, + inline, + contentFromRemote = remoteRefContentMap.get(tempRef), + nodeTemp = { + fileName: tempRef, + path: tempRef, + content: contentFromRemote + }; + + nodeFromData = nodeTemp; + + if (nodeFromData && nodeFromData.content) { + parseResult = parseFile(JSON.stringify(nodeFromData.content)); + if (parseResult.result) { + newValue.$ref = referenceInDocument; + refHasContent = true; + nodeFromData.parsed = parseResult; + } + } + this.update({ $ref: tempRef }); + + if (nodeTrace.length === 0) { + inline = true; + } + + if (_.isNil(globalReferences[tempRef])) { + nodeReferenceDirectory[tempRef] = { + local, + keyInComponents: nodeTrace, + node: newValue, + reference: inline ? newRefInDoc : referenceInDocument, + traceToParent, + parentNodeKey: parentFilename, + mainKeyInTrace: nodeTrace[nodeTrace.length - 1], + refHasContent, + inline + }; + } + + mainKeys[componentKey] = tempRef; + + if (!added(property.$ref, referencesInNode)) { + referencesInNode.push({ path: pathSolver(property), keyInComponents: nodeTrace, newValue: this.node }); + } + }; + + if (hasRemoteReferenceTypeKey) { + handleRemoteURLReference(); + } } }); @@ -386,10 +558,11 @@ function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename, ve * @param {object} rootMainKeys - A dictionary with the component keys in local components object and its mainKeys * @param {string} commonPathFromData - The common path in the file's paths * @param {object} globalReferences - The accumulated global refernces from all nodes + * @param {function} remoteRefResolver - The function that would be called to fetch remote ref contents * @returns {object} - Detect root files result object */ -function getNodeContentAndReferences (currentNode, allData, specRoot, version, rootMainKeys, - commonPathFromData, globalReferences) { +async function getNodeContentAndReferences (currentNode, allData, specRoot, version, rootMainKeys, + commonPathFromData, globalReferences, remoteRefResolver) { let graphAdj = [], missingNodes = [], nodeContent, @@ -406,7 +579,7 @@ function getNodeContentAndReferences (currentNode, allData, specRoot, version, r nodeContent = parseResult.oasObject; } - const { referencesInNode, nodeReferenceDirectory } = getReferences( + const { referencesInNode, nodeReferenceDirectory } = await getReferences( nodeContent, currentNode.fileName !== specRoot.fileName, removeLocalReferenceFromPath, @@ -415,7 +588,8 @@ function getNodeContentAndReferences (currentNode, allData, specRoot, version, r rootMainKeys, commonPathFromData, allData, - globalReferences + globalReferences, + remoteRefResolver ); referencesInNode.forEach((reference) => { @@ -516,9 +690,11 @@ function handleCircularReference(traverseContext, documentContext) { * @param {function} refTypeResolver - The resolver function to test if node has a reference * @param {object} components - The global components object * @param {string} version - The current version + * @param {function} remoteRefResolver - The function that would be called to fetch remote ref contents * @returns {object} The components object related to the file */ -function generateComponentsObject (documentContext, rootContent, refTypeResolver, components, version) { +function generateComponentsObject(documentContext, rootContent, + refTypeResolver, components, version, remoteRefResolver) { let notInLine = Object.entries(documentContext.globalReferences).filter(([, value]) => { return value.keyInComponents.length !== 0; }), @@ -555,6 +731,7 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver isMissingNode = documentContext.missing.find((missingNode) => { return missingNode.path === nodeRef; }); + if (isMissingNode) { refData.nodeContent = refData.node; refData.local = false; @@ -562,6 +739,29 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver else if (!refData) { return; } + else if (!isExtRef(property, '$ref') && isExtURLRef(property, '$ref')) { + let splitPathByHttp = property.$ref.split(httpSeparator), + prefix = splitPathByHttp + .slice(0, splitPathByHttp.length - 1).join(httpSeparator) + + httpSeparator + splitPathByHttp[splitPathByHttp.length - 1] + .split(localPointer)[0], + separatedPaths = [prefix, splitPathByHttp[splitPathByHttp.length - 1].split(localPointer)[1]]; + + nodeRef = separatedPaths[0]; + local = separatedPaths[1]; + + refData.nodeContent = documentContext.nodeContents[nodeRef]; + + const isReferenceRemoteURL = stringIsAValidUrl(nodeRef); + + if (isReferenceRemoteURL && _.isFunction(remoteRefResolver)) { + Object.keys(documentContext.nodeContents).forEach((key) => { + if (_.startsWith(key, nodeRef) && !key.split(nodeRef)[1].includes(httpSeparator)) { + refData.nodeContent = documentContext.nodeContents[key]; + } + }); + } + } else { refData.nodeContent = documentContext.nodeContents[nodeRef]; } @@ -697,9 +897,10 @@ module.exports = { * @param {Array} allData - array of { path, content} objects * @param {Array} origin - process origin (BROWSER or node) * @param {string} version - The version we are using + * @param {function} remoteRefResolver - The function that would be called to fetch remote ref contents * @returns {object} - Detect root files result object */ - getBundleContentAndComponents: function (specRoot, allData, origin, version) { + getBundleContentAndComponents: async function (specRoot, allData, origin, version, remoteRefResolver) { if (origin === BROWSER) { path = pathBrowserify; } @@ -716,7 +917,7 @@ module.exports = { commonPathFromData = Utils.findCommonSubpath(allData.map((fileData) => { return fileData.fileName; })); - rootContextData = algorithm.traverseAndBundle(specRoot, (currentNode, globalReferences) => { + rootContextData = await algorithm.traverseAndBundle(specRoot, (currentNode, globalReferences) => { return getNodeContentAndReferences( currentNode, allData, @@ -724,7 +925,8 @@ module.exports = { version, initialMainKeys, commonPathFromData, - globalReferences + globalReferences, + remoteRefResolver ); }); components = generateComponentsWrapper( @@ -735,10 +937,12 @@ module.exports = { finalElements = generateComponentsObject( rootContextData, rootContextData.nodeContents[specRoot.fileName], - isExtRef, + isExtRemoteRef, components, - version + version, + remoteRefResolver ); + return { fileContent: finalElements.resRoot, components: finalElements.newComponents, diff --git a/lib/dfs.js b/lib/dfs.js index f0bd82f19..7b6a8dd72 100644 --- a/lib/dfs.js +++ b/lib/dfs.js @@ -29,26 +29,37 @@ class DFS { return { traverseOrder, missing }; } - traverseAndBundle(node, getAdjacentAndBundle) { + async traverseAndBundle(node, getAdjacentAndBundle) { let traverseOrder = [], stack = [], missing = [], visited = new Set(), nodeContents = {}, - globalReferences = {}; + globalReferences = {}, + hrefsVisited = new Set(); + stack.push(node); while (stack.length > 0) { node = stack.pop(); - if (!visited.has(node)) { + if (!visited.has(node) && + + /** + * For nodes that are fetched for remote URLs we ensure they + * aren't visited more than once + */ + (!node.href || (!hrefsVisited.has(node.href))) + ) { traverseOrder.push(node); visited.add(node); + node.href && hrefsVisited.add(node.href); + let { graphAdj, missingNodes, nodeContent, nodeReferenceDirectory, nodeName - } = getAdjacentAndBundle(node, globalReferences); + } = await getAdjacentAndBundle(node, globalReferences); nodeContents[nodeName] = nodeContent; Object.entries(nodeReferenceDirectory).forEach(([key, data]) => { globalReferences[key] = data; diff --git a/lib/jsonPointer.js b/lib/jsonPointer.js index e28961689..3c69ec0a0 100644 --- a/lib/jsonPointer.js +++ b/lib/jsonPointer.js @@ -3,6 +3,7 @@ const slashes = /\//g, escapedSlash = /~1/g, escapedSlashString = '~1', localPointer = '#', + httpSeparator = 'http', escapedTilde = /~0/g, jsonPointerLevelSeparator = '/', escapedTildeString = '~0', @@ -169,6 +170,35 @@ function isExtRef(obj, key) { !stringIsAValidUrl(obj[key]); } +/** + * Determines if a value of a given key property of an object + * is an external reference with key $ref and value that does not start with # + * @param {object} obj - parent object of the $ref property + * @param {string} key - property key to check + * @returns {boolean} - true if the property key is $ref and the value does not start with # + * otherwise false + */ +function isExtURLRef(obj, key) { + return key === '$ref' && + typeof obj[key] === 'string' && + obj[key] !== undefined && + !obj[key].startsWith(localPointer) && + stringIsAValidUrl(obj[key]); +} + + +/** + * Determines if a value of a given key property of an object + * is an external reference with key $ref and value that does not start with # + * @param {object} obj - parent object of the $ref property + * @param {string} key - property key to check + * @returns {boolean} - true if the property key is $ref and the value does not start with # + * otherwise false + */ +function isExtRemoteRef(obj, key) { + return isExtRef(obj, key) || isExtURLRef(obj, key); +} + /** * Removes the local pointer inside a path * aab.yaml#component returns aab.yaml @@ -233,11 +263,15 @@ module.exports = { concatJsonPointer, getKeyInComponents, isExtRef, + isExtURLRef, + isExtRemoteRef, removeLocalReferenceFromPath, isLocalRef, getEntityName, isRemoteRef, localPointer, + httpSeparator, jsonPointerLevelSeparator, - generateObjectName + generateObjectName, + stringIsAValidUrl }; diff --git a/lib/schemaUtils.js b/lib/schemaUtils.js index 64bb13076..5eb5e7049 100644 --- a/lib/schemaUtils.js +++ b/lib/schemaUtils.js @@ -634,7 +634,7 @@ module.exports = { Object.assign(specComponentsAndUtils, concreteUtils.getRequiredData(spec)); for (path in paths) { - if (paths.hasOwnProperty(path)) { + if (paths.hasOwnProperty(path) && typeof paths[path] === 'object' && paths[path]) { currentPathObject = paths[path]; // discard the leading slash, if it exists @@ -5242,11 +5242,14 @@ module.exports = { * * @returns {object} process result { rootFile, bundledContent } */ - getBundledFileData(parsedRootFiles, inputData, origin, format, version, options = {}) { - const data = parsedRootFiles.map((root) => { - let bundleData = getBundleContentAndComponents(root, inputData, origin, version); - return bundleData; - }); + async getBundledFileData(parsedRootFiles, inputData, origin, format, version, options = {}, remoteRefResolver) { + const data = []; + + for (const file of parsedRootFiles) { + let bundleDataFile = await getBundleContentAndComponents(file, inputData, origin, version, remoteRefResolver); + + data.push(bundleDataFile); + } let bundleData = data.map(this.mapBundleOutput(format, parsedRootFiles, version, options)); @@ -5264,10 +5267,13 @@ module.exports = { * @param {string} format - the format required by the user * @param {boolean} toBundle - if it will be used in bundle * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. + * @param {function} remoteRefResolver - The function that would be called to fetch remote ref contents * * @returns {object} root files information and data input */ - mapProcessRelatedFiles(rootFiles, inputData, origin, version, format, toBundle = false, options = {}) { + async mapProcessRelatedFiles( + rootFiles, inputData, origin, version, format, toBundle = false, options = {}, remoteRefResolver + ) { let bundleFormat = format, parsedRootFiles = rootFiles.map((rootFile) => { let parsedContent = parseFileOrThrow(rootFile.content); @@ -5279,7 +5285,8 @@ module.exports = { return compareVersion(version, fileVersion); }), data = toBundle ? - this.getBundledFileData(parsedRootFiles, inputData, origin, bundleFormat, version, options) : + await this.getBundledFileData(parsedRootFiles, + inputData, origin, bundleFormat, version, options, remoteRefResolver) : this.getRelatedFilesData(parsedRootFiles, inputData, origin); return data; @@ -5295,7 +5302,7 @@ module.exports = { * * @returns {object} root files information and data input */ - processRelatedFiles(inputRelatedFiles, toBundle = false, options = {}) { + async processRelatedFiles(inputRelatedFiles, toBundle = false, options = {}) { let version = inputRelatedFiles.specificationVersion ? inputRelatedFiles.specificationVersion : '3.0', res = { result: true, @@ -5311,8 +5318,9 @@ module.exports = { }; if (inputRelatedFiles.rootFiles && inputRelatedFiles.rootFiles.length > 0) { try { - res.output.data = this.mapProcessRelatedFiles(inputRelatedFiles.rootFiles, inputRelatedFiles.data, - inputRelatedFiles.origin, version, inputRelatedFiles.bundleFormat, toBundle, options); + res.output.data = await this.mapProcessRelatedFiles(inputRelatedFiles.rootFiles, inputRelatedFiles.data, + inputRelatedFiles.origin, version, + inputRelatedFiles.bundleFormat, toBundle, options, inputRelatedFiles.remoteRefResolver); if (res.output.data === undefined || res.output.data.result === false || res.output.data.length === 0) { res.result = false; diff --git a/lib/xmlSchemaFaker.js b/lib/xmlSchemaFaker.js index 8bc04d92c..82962b637 100644 --- a/lib/xmlSchemaFaker.js +++ b/lib/xmlSchemaFaker.js @@ -14,6 +14,10 @@ function convertSchemaToXML(name, schema, attribute, indentChar, indent, resolve cIndent = _.times(indent, _.constant(indentChar)).join(''), retVal = ''; + if (schema === null || typeof schema === 'undefined') { + return retVal; + } + const schemaExample = typeof schema === 'object' && (schema.example); name = _.get(schema, 'xml.name', name || 'element'); diff --git a/libV2/schemaUtils.js b/libV2/schemaUtils.js index 78fe122d5..64a688879 100644 --- a/libV2/schemaUtils.js +++ b/libV2/schemaUtils.js @@ -1054,16 +1054,165 @@ let QUERYPARAM = 'query', return HEADER_TYPE.INVALID; }, - resolveRequestBodyData = (context, requestBodySchema, bodyType) => { + /** + * Gets XML Example data in correct format based on schema + * + * @param {Object} context - Global context object + * @param {Object} exampleData - Example data to be used + * @param {Object} requestBodySchema - Schema of the request body + * @returns {String} XML Example data + */ + getXMLExampleData = (context, exampleData, requestBodySchema) => { + const { parametersResolution, indentCharacter } = context.computedOptions; + + let reqBodySchemaWithExample = requestBodySchema; + + // Assign example at schema level to be faked by xmlSchemaFaker + if (typeof requestBodySchema === 'object') { + reqBodySchemaWithExample = Object.assign({}, requestBodySchema, { example: exampleData }); + } + + return xmlFaker(null, reqBodySchemaWithExample, indentCharacter, parametersResolution); + }, + + /** + * Generates postman equivalent examples which contains request and response mappings of + * each example based on examples mentioned ind definition + * + * @param {Object} context - Global context object + * @param {Object} responseExamples - Examples defined in the response + * @param {Object} requestBodyExamples - Examples defined in the request body + * @param {Object} responseBodySchema - Schema of the response body + * @param {Boolean} isXMLExample - Whether the example is XML example + * @returns {Array} Examples for corresponding operation + */ + generateExamples = (context, responseExamples, requestBodyExamples, responseBodySchema, isXMLExample) => { + const pmExamples = []; + + _.forEach(responseExamples, (responseExample, index) => { + + if (!_.isObject(responseExample)) { + return; + } + + let responseExampleData = getExampleData(context, { [responseExample.key]: responseExample.value }), + requestExample; + + if (isXMLExample) { + responseExampleData = getXMLExampleData(context, responseExampleData, responseBodySchema); + } + + if (_.isEmpty(requestBodyExamples)) { + pmExamples.push({ + response: responseExampleData, + name: _.get(responseExample, 'value.summary') || responseExample.key + }); + return; + } + + requestExample = _.find(requestBodyExamples, (example, index) => { + if ( + example.contentType === responseExample.contentType && + _.toLower(example.key) === _.toLower(responseExample.key) + ) { + requestBodyExamples[index].isUsed = true; + return true; + } + return false; + }); + + // If exact content type is not matching, pick first content type with same example key + if (!requestExample) { + requestExample = _.find(requestBodyExamples, (example, index) => { + if (_.toLower(example.key) === _.toLower(responseExample.key)) { + requestBodyExamples[index].isUsed = true; + return true; + } + return false; + }); + } + + if (!requestExample) { + if (requestBodyExamples[index] && !requestBodyExamples[index].isUsed) { + requestExample = requestBodyExamples[index]; + requestBodyExamples[index].isUsed = true; + } + else { + for (let i = 0; i < requestBodyExamples.length; i++) { + if (!requestBodyExamples[i].isUsed) { + requestExample = requestBodyExamples[i]; + requestBodyExamples[i].isUsed = true; + break; + } + } + + if (!requestExample) { + requestExample = requestBodyExamples[0]; + } + } + } + + pmExamples.push({ + request: getExampleData(context, { [requestExample.key]: requestExample.value }), + response: responseExampleData, + name: _.get(responseExample, 'value.summary') || (responseExample.key !== '_default' && responseExample.key) || + _.get(requestExample, 'value.summary') || requestExample.key || 'Example' + }); + }); + + let responseExample, + responseExampleData; + + for (let i = 0; i < requestBodyExamples.length; i++) { + + if (!requestBodyExamples[i].isUsed || pmExamples.length === 0) { + if (!responseExample) { + responseExample = _.head(responseExamples); + + if (responseExample) { + responseExampleData = getExampleData(context, { [responseExample.key]: responseExample.value }); + } + + if (isXMLExample) { + responseExampleData = getXMLExampleData(context, responseExampleData, responseBodySchema); + } + } + pmExamples.push({ + request: getExampleData(context, { [requestBodyExamples[i].key]: requestBodyExamples[i].value }), + response: responseExampleData, + name: _.get(requestBodyExamples[i], 'value.summary') || + (requestBodyExamples[i].key !== '_default' && requestBodyExamples[i].key) || + _.get(responseExample, 'value.summary') || 'Example' + }); + } + } + + return pmExamples; + }, + + /** + * Resolves the request/response body data + * + * @param {Object} context - Global context object + * @param {Object} requestBodySchema - Schema of the request / response body + * @param {String} bodyType - Content type of the body + * @param {Boolean} isExampleBody - Whether the body is example body + * @param {Object} requestBodyExamples - Examples defined in the request body + * @returns {Array} Request / Response body data + */ + resolveBodyData = (context, requestBodySchema, bodyType, isExampleBody = false, requestBodyExamples) => { let { parametersResolution, indentCharacter } = context.computedOptions, headerFamily = getHeaderFamily(bodyType), bodyData = '', shouldGenerateFromExample = parametersResolution === 'example', + isBodyTypeXML = bodyType === APP_XML || bodyType === TEXT_XML || headerFamily === HEADER_TYPE.XML, + bodyKey = isExampleBody ? 'response' : 'request', + responseExamples, example, examples; if (_.isEmpty(requestBodySchema)) { - return bodyData; + return [{ [bodyKey]: bodyData }]; } if (requestBodySchema.$ref) { @@ -1130,15 +1279,8 @@ let QUERYPARAM = 'query', */ const exampleData = example || getExampleData(context, examples); - if (bodyType === APP_XML || bodyType === TEXT_XML || headerFamily === HEADER_TYPE.XML) { - let reqBodySchemaWithExample = requestBodySchema; - - // Assign example at schema level to be faked by xmlSchemaFaker - if (typeof requestBodySchema === 'object') { - reqBodySchemaWithExample = Object.assign({}, requestBodySchema, { example: exampleData }); - } - - return xmlFaker(null, reqBodySchemaWithExample, indentCharacter, parametersResolution); + if (isBodyTypeXML) { + bodyData = getXMLExampleData(context, exampleData, requestBodySchema); } else { bodyData = exampleData; @@ -1151,47 +1293,71 @@ let QUERYPARAM = 'query', requestBodySchema = resolveSchema(context, requestBodySchema); } - if (bodyType === APP_XML || bodyType === TEXT_XML || headerFamily === HEADER_TYPE.XML) { - return xmlFaker(null, requestBodySchema, indentCharacter, parametersResolution); + if (isBodyTypeXML) { + bodyData = xmlFaker(null, requestBodySchema, indentCharacter, parametersResolution); } + else { + if (requestBodySchema.properties) { + // If any property exists with format:binary or byte schemaFaker crashes + // we just delete based on that format + + // TODO: This could have properties inside properties which needs to be handled + // That's why for some properties we are not deleting the format + _.forOwn(requestBodySchema.properties, (schema, prop) => { + if (!_.isObject(requestBodySchema.properties[prop])) { + return; + } + if ( + requestBodySchema.properties[prop].format === 'binary' || + requestBodySchema.properties[prop].format === 'byte' || + requestBodySchema.properties[prop].format === 'decimal' + ) { + delete requestBodySchema.properties[prop].format; + } + }); + } - if (requestBodySchema.properties) { - // If any property exists with format:binary or byte schemaFaker crashes - // we just delete based on that format - - // TODO: This could have properties inside properties which needs to be handled - // That's why for some properties we are not deleting the format - _.forOwn(requestBodySchema.properties, (schema, prop) => { - if (!_.isObject(requestBodySchema.properties[prop])) { - return; - } + // This is to handle cases when the jsf throws errors on finding unsupported types/formats + try { + bodyData = fakeSchema(context, requestBodySchema, shouldGenerateFromExample); + } + catch (e) { + console.warn( + 'Error faking a schema. Not faking this schema. Schema:', requestBodySchema, + 'Error', e.message + ); - if ( - requestBodySchema.properties[prop].format === 'binary' || - requestBodySchema.properties[prop].format === 'byte' || - requestBodySchema.properties[prop].format === 'decimal' - ) { - delete requestBodySchema.properties[prop].format; - } - }); + bodyData = ''; + } } + } - // This is to handle cases when the jsf throws errors on finding unsupported types/formats - try { - bodyData = fakeSchema(context, requestBodySchema, shouldGenerateFromExample); - } - catch (e) { - console.warn( - 'Error faking a schema. Not faking this schema. Schema:', requestBodySchema, - 'Error', e.message - ); + // Generate multiple examples when either request or response contains more than one example + if ( + isExampleBody && + shouldGenerateFromExample && + (_.size(examples) > 1 || _.size(requestBodyExamples) > 1) + ) { + responseExamples = [{ + key: '_default', + value: bodyData, + contentType: bodyType + }]; - return ''; + if (!_.isEmpty(examples)) { + responseExamples = _.map(examples, (example, key) => { + return { + key, + value: example, + contentType: bodyType + }; + }); } + return generateExamples(context, responseExamples, requestBodyExamples, requestBodySchema, isBodyTypeXML); } - return bodyData; + return [{ [bodyKey]: bodyData }]; }, resolveUrlEncodedRequestBodyForPostmanRequest = (context, requestBodyContent) => { @@ -1200,7 +1366,8 @@ let QUERYPARAM = 'query', requestBodyData = { mode: 'urlencoded', urlencoded: urlEncodedParams - }; + }, + resolvedBody; if (_.isEmpty(requestBodyContent)) { return requestBodyData; @@ -1210,7 +1377,8 @@ let QUERYPARAM = 'query', requestBodyContent.schema = resolveSchema(context, requestBodyContent.schema); } - bodyData = resolveRequestBodyData(context, requestBodyContent.schema); + resolvedBody = resolveBodyData(context, requestBodyContent.schema)[0]; + resolvedBody && (bodyData = resolvedBody.request); const encoding = requestBodyContent.encoding || {}; @@ -1255,13 +1423,16 @@ let QUERYPARAM = 'query', requestBodyData = { mode: 'formdata', formdata: formDataParams - }; + }, + resolvedBody; if (_.isEmpty(requestBodyContent)) { return requestBodyData; } - bodyData = resolveRequestBodyData(context, requestBodyContent.schema); + resolvedBody = resolveBodyData(context, requestBodyContent.schema)[0]; + resolvedBody && (bodyData = resolvedBody.request); + encoding = _.get(requestBodyContent, 'encoding', {}); _.forOwn(bodyData, (value, key) => { @@ -1332,12 +1503,23 @@ let QUERYPARAM = 'query', else if (content.hasOwnProperty(APP_XML)) { bodyType = APP_XML; } else if (content.hasOwnProperty(TEXT_XML)) { bodyType = TEXT_XML; } else { - // take the first property it has - // types like image/png etc - for (const cType in content) { - if (content.hasOwnProperty(cType)) { - bodyType = cType; - break; + // prefer JSON type of body if available + _.forOwn(content, (value, key) => { + if (content.hasOwnProperty(key) && getHeaderFamily(key) === HEADER_TYPE.JSON) { + bodyType = key; + return false; + } + }); + + // use first available type of body if no JSON or XML body is available + if (!bodyType) { + // take the first property it has + // types like image/png etc + for (const cType in content) { + if (content.hasOwnProperty(cType)) { + bodyType = cType; + break; + } } } } @@ -1350,7 +1532,8 @@ let QUERYPARAM = 'query', bodyData, headerFamily, dataToBeReturned = {}, - { concreteUtils } = context; + { concreteUtils } = context, + resolvedBody; headerFamily = getHeaderFamily(bodyType); @@ -1361,7 +1544,8 @@ let QUERYPARAM = 'query', } // Handling for Raw mode data else { - bodyData = resolveRequestBodyData(context, requestContent[bodyType], bodyType); + resolvedBody = resolveBodyData(context, requestContent[bodyType], bodyType)[0]; + resolvedBody && (bodyData = resolvedBody.request); if ((bodyType === TEXT_XML || bodyType === APP_XML || headerFamily === HEADER_TYPE.XML)) { bodyData = getXmlVersionContent(bodyData); @@ -1616,11 +1800,27 @@ let QUERYPARAM = 'query', return pmParams; }, - resolveResponseBody = (context, responseBody = {}) => { - let responseContent, bodyType, bodyData, headerFamily, acceptHeader; + /** + * Resolve the responses from definition which will be converted to request examples. + * This includes both request and response body of corresponding example. + * + * @param {Object} context - Global context object + * @param {Object} responseBody - Response body schema + * @param {Object} requestBodyExamples - Examples defined in the request body of corresponding operation + * @returns {Array} - Postman examples + */ + resolveResponseBody = (context, responseBody = {}, requestBodyExamples) => { + let responseContent, + bodyType, + allBodyData, + headerFamily, + acceptHeader, + emptyResponse = [{ + body: undefined + }]; if (_.isEmpty(responseBody)) { - return responseBody; + return emptyResponse; } if (responseBody.$ref) { @@ -1630,40 +1830,54 @@ let QUERYPARAM = 'query', responseContent = responseBody.content; if (_.isEmpty(responseContent)) { - return responseContent; + return emptyResponse; } bodyType = getRawBodyType(responseContent); headerFamily = getHeaderFamily(bodyType); - bodyData = resolveRequestBodyData(context, responseContent[bodyType], bodyType); + allBodyData = resolveBodyData(context, responseContent[bodyType], bodyType, true, requestBodyExamples); - if ((bodyType === TEXT_XML || bodyType === APP_XML || headerFamily === HEADER_TYPE.XML)) { - bodyData = getXmlVersionContent(bodyData); - } + return _.map(allBodyData, (bodyData) => { + let requestBodyData = bodyData.request, + responseBodyData = bodyData.response, + exampleName = bodyData.name; - const { indentCharacter } = context.computedOptions, - rawModeData = !_.isObject(bodyData) && _.isFunction(_.get(bodyData, 'toString')) ? - bodyData.toString() : - JSON.stringify(bodyData, null, indentCharacter), - responseMediaTypes = _.keys(responseContent); - - if (responseMediaTypes.length > 0) { - acceptHeader = [{ - key: 'Accept', - value: responseMediaTypes[0] - }]; - } + if ((bodyType === TEXT_XML || bodyType === APP_XML || headerFamily === HEADER_TYPE.XML)) { + responseBodyData && (responseBodyData = getXmlVersionContent(responseBodyData)); + } - return { - body: rawModeData, - contentHeader: [{ - key: 'Content-Type', - value: bodyType - }], - bodyType, - acceptHeader - }; + const { indentCharacter } = context.computedOptions, + getRawModeData = (bodyData) => { + return !_.isObject(bodyData) && _.isFunction(_.get(bodyData, 'toString')) ? + bodyData.toString() : + JSON.stringify(bodyData, null, indentCharacter); + }, + requestRawModeData = getRawModeData(requestBodyData), + responseRawModeData = getRawModeData(responseBodyData), + responseMediaTypes = _.keys(responseContent); + + if (responseMediaTypes.length > 0) { + acceptHeader = [{ + key: 'Accept', + value: responseMediaTypes[0] + }]; + } + + return { + request: { + body: requestRawModeData + }, + body: responseRawModeData, + contentHeader: [{ + key: 'Content-Type', + value: bodyType + }], + name: exampleName, + bodyType, + acceptHeader + }; + }); }, resolveResponseHeaders = (context, responseHeaders) => { @@ -1784,59 +1998,119 @@ let QUERYPARAM = 'query', resolveResponseForPostmanRequest = (context, operationItem, request) => { let responses = [], - requestAcceptHeader; + requestBodyExamples = [], + requestAcceptHeader, + requestBody = operationItem.requestBody, + requestContent, + rawBodyType, + headerFamily, + isBodyTypeXML; + + // store all request examples which will be used for creation of examples with correct request and response matching + if (typeof requestBody === 'object') { + if (requestBody.$ref) { + requestBody = resolveSchema(context, requestBody); + } + + requestContent = requestBody.content; + + if (typeof requestContent === 'object') { + rawBodyType = getRawBodyType(requestContent); + headerFamily = getHeaderFamily(rawBodyType); + isBodyTypeXML = rawBodyType === APP_XML || rawBodyType === TEXT_XML || headerFamily === HEADER_TYPE.XML; + + _.forEach(requestContent, (content, contentType) => { + if (_.has(content, 'examples')) { + _.forEach(content.examples, (example, name) => { + const exampleObj = example; + + if (isBodyTypeXML && exampleObj.value) { + const exampleData = getExampleData(context, { [name]: exampleObj }); + + if (isBodyTypeXML) { + let bodyData = getXMLExampleData(context, exampleData, resolveSchema(context, content.schema)); + + exampleObj.value = getXmlVersionContent(bodyData); + } + } + + requestBodyExamples.push({ + contentType, + key: name, + value: example + }); + }); + } + }); + } + } _.forOwn(operationItem.responses, (responseObj, code) => { - let response, - responseSchema = _.has(responseObj, '$ref') ? resolveSchema(context, responseObj) : responseObj, + let responseSchema = _.has(responseObj, '$ref') ? resolveSchema(context, responseObj) : responseObj, { includeAuthInfoInExample } = context.computedOptions, - responseAuthHelper, auth = request.auth, - { body, contentHeader = [], bodyType, acceptHeader } = resolveResponseBody(context, responseSchema) || {}, - headers = resolveResponseHeaders(context, responseSchema.headers), - originalRequest = request, - reqHeaders = _.clone(request.headers) || [], - reqQueryParams = _.clone(_.get(request, 'params.queryParams', [])); - - // add Accept header in example's original request headers - _.isArray(acceptHeader) && (reqHeaders.push(...acceptHeader)); - - if (includeAuthInfoInExample) { - if (!auth) { - auth = generateAuthForCollectionFromOpenAPI(context.openapi, context.openapi.security); + resolvedExamples = resolveResponseBody(context, responseSchema, requestBodyExamples) || {}, + headers = resolveResponseHeaders(context, responseSchema.headers); + + _.forOwn(resolvedExamples, (resolvedExample = {}) => { + let { body, contentHeader = [], bodyType, acceptHeader, name } = resolvedExample, + resolvedRequestBody = _.get(resolvedExample, 'request.body'), + originalRequest, + response, + responseAuthHelper, + requestBodyObj = {}, + reqHeaders = _.clone(request.headers) || [], + reqQueryParams = _.clone(_.get(request, 'params.queryParams', [])); + + // add Accept header in example's original request headers + _.isArray(acceptHeader) && (reqHeaders.push(...acceptHeader)); + + if (_.get(request, 'body.mode') === 'raw' && !_.isNil(resolvedRequestBody)) { + requestBodyObj = { + body: Object.assign({}, request.body, { raw: resolvedRequestBody }) + }; } - responseAuthHelper = getResponseAuthHelper(auth); + if (includeAuthInfoInExample) { + if (!auth) { + auth = generateAuthForCollectionFromOpenAPI(context.openapi, context.openapi.security); + } - reqHeaders.push(...responseAuthHelper.header); - reqQueryParams.push(...responseAuthHelper.query); + responseAuthHelper = getResponseAuthHelper(auth); - originalRequest = _.assign({}, request, { - headers: reqHeaders, - params: _.assign({}, request.params, { queryParams: reqQueryParams }) - }); - } - else { - originalRequest = _.assign({}, request, { - headers: reqHeaders - }); - } + reqHeaders.push(...responseAuthHelper.header); + reqQueryParams.push(...responseAuthHelper.query); - // set accept header value as first found response content's media type - if (_.isEmpty(requestAcceptHeader)) { - requestAcceptHeader = acceptHeader; - } + originalRequest = _.assign({}, request, { + headers: reqHeaders, + params: _.assign({}, request.params, { queryParams: reqQueryParams }) + }, requestBodyObj); + } + else { + originalRequest = _.assign({}, request, { headers: reqHeaders }, requestBodyObj); + } - response = { - name: _.get(responseSchema, 'description'), - body, - headers: _.concat(contentHeader, headers), - code, - originalRequest, - _postman_previewlanguage: getPreviewLangugaForResponseBody(bodyType) - }; + // When example key is not available, key name will be `_default` naming should be done based on description + if (_.get(resolvedExample, 'name') === '_default' || !(typeof name === 'string' && name.length)) { + name = _.get(responseSchema, 'description', `${code} response`); + } - responses.push(response); + // set accept header value as first found response content's media type + if (_.isEmpty(requestAcceptHeader)) { + requestAcceptHeader = acceptHeader; + } + + response = { + name, + body, + headers: _.concat(contentHeader, headers), + code, + originalRequest, + _postman_previewlanguage: getPreviewLangugaForResponseBody(bodyType) + }; + + responses.push(response); + }); }); return { responses, acceptHeader: requestAcceptHeader }; diff --git a/libV2/xmlSchemaFaker.js b/libV2/xmlSchemaFaker.js index 8bc04d92c..82962b637 100644 --- a/libV2/xmlSchemaFaker.js +++ b/libV2/xmlSchemaFaker.js @@ -14,6 +14,10 @@ function convertSchemaToXML(name, schema, attribute, indentChar, indent, resolve cIndent = _.times(indent, _.constant(indentChar)).join(''), retVal = ''; + if (schema === null || typeof schema === 'undefined') { + return retVal; + } + const schemaExample = typeof schema === 'object' && (schema.example); name = _.get(schema, 'xml.name', name || 'element'); diff --git a/package-lock.json b/package-lock.json index 491d4ef82..b7af3034e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "openapi-to-postmanv2", - "version": "4.15.0", + "version": "4.18.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "openapi-to-postmanv2", - "version": "4.15.0", + "version": "4.18.0", "license": "Apache-2.0", "dependencies": { - "ajv": "8.5.0", + "ajv": "8.11.0", "ajv-draft-04": "1.0.0", "ajv-formats": "2.1.1", "async": "3.2.4", @@ -21,7 +21,7 @@ "oas-resolver-browser": "2.5.6", "object-hash": "3.0.0", "path-browserify": "1.0.1", - "postman-collection": "4.1.5", + "postman-collection": "4.2.1", "swagger2openapi": "7.0.8", "traverse": "0.6.6", "yaml": "1.10.2" @@ -560,9 +560,9 @@ } }, "node_modules/ajv": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.5.0.tgz", - "integrity": "sha512-Y2l399Tt1AguU3BPRP9Fn4eN+Or+StUGWCUpbnFyXSo8NZ9S4uj+AG2pjs5apK+ZMOwYOz1+a+VKvKH7CudXgQ==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -4253,9 +4253,9 @@ } }, "node_modules/postman-collection": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.1.5.tgz", - "integrity": "sha512-BY3NfP7EYExZG5ER9P82r0ZRc17z88WZAzn121EpWC8FM3HYtFwWJpXOsZk+2MKFn3agCq4JPRhnWw3G6XBXgw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.2.1.tgz", + "integrity": "sha512-DFLt3/yu8+ldtOTIzmBUctoupKJBOVK4NZO0t68K2lIir9smQg7OdQTBjOXYy+PDh7u0pSDvD66tm93eBHEPHA==", "dependencies": { "@faker-js/faker": "5.5.3", "file-type": "3.9.0", @@ -4266,7 +4266,7 @@ "mime-format": "2.0.1", "mime-types": "2.1.35", "postman-url-encoder": "3.0.5", - "semver": "7.3.7", + "semver": "7.5.4", "uuid": "8.3.2" }, "engines": { @@ -4296,9 +4296,9 @@ } }, "node_modules/postman-collection/node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -6018,9 +6018,9 @@ } }, "ajv": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.5.0.tgz", - "integrity": "sha512-Y2l399Tt1AguU3BPRP9Fn4eN+Or+StUGWCUpbnFyXSo8NZ9S4uj+AG2pjs5apK+ZMOwYOz1+a+VKvKH7CudXgQ==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -8776,9 +8776,9 @@ } }, "postman-collection": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.1.5.tgz", - "integrity": "sha512-BY3NfP7EYExZG5ER9P82r0ZRc17z88WZAzn121EpWC8FM3HYtFwWJpXOsZk+2MKFn3agCq4JPRhnWw3G6XBXgw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.2.1.tgz", + "integrity": "sha512-DFLt3/yu8+ldtOTIzmBUctoupKJBOVK4NZO0t68K2lIir9smQg7OdQTBjOXYy+PDh7u0pSDvD66tm93eBHEPHA==", "requires": { "@faker-js/faker": "5.5.3", "file-type": "3.9.0", @@ -8789,7 +8789,7 @@ "mime-format": "2.0.1", "mime-types": "2.1.35", "postman-url-encoder": "3.0.5", - "semver": "7.3.7", + "semver": "7.5.4", "uuid": "8.3.2" }, "dependencies": { @@ -8810,9 +8810,9 @@ } }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } diff --git a/package.json b/package.json index fc511bf5e..50b75f754 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openapi-to-postmanv2", - "version": "4.15.0", + "version": "4.18.0", "description": "Convert a given OpenAPI specification to Postman Collection v2.0", "homepage": "https://github.com/postmanlabs/openapi-to-postman", "bugs": "https://github.com/postmanlabs/openapi-to-postman/issues", @@ -116,7 +116,7 @@ "openapi2postmanv2": "./bin/openapi2postmanv2.js" }, "dependencies": { - "ajv": "8.5.0", + "ajv": "8.11.0", "ajv-draft-04": "1.0.0", "ajv-formats": "2.1.1", "async": "3.2.4", @@ -128,7 +128,7 @@ "object-hash": "3.0.0", "graphlib": "2.1.8", "path-browserify": "1.0.1", - "postman-collection": "4.1.5", + "postman-collection": "4.2.1", "swagger2openapi": "7.0.8", "traverse": "0.6.6", "yaml": "1.10.2" diff --git a/test/data/toBundleExamples/remote_url_refs/circular/expected.json b/test/data/toBundleExamples/remote_url_refs/circular/expected.json new file mode 100644 index 000000000..877a52803 --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/circular/expected.json @@ -0,0 +1,85 @@ +{ + "openapi": "3.0.2", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "Swagger API Team", + "email": "apiteam@swagger.io", + "url": "http://swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "paths": { + "/pets": { + "get": { + "description": "Returns all pets alesuada ac...", + "operationId": "findPets", + "responses": { + "200": { + "description": "An paged array of pets", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/https_localhost8080_schema.json-_components_schemas_ErrorDetail" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "details": { + "description": "The detailed identifier of a spacecraft", + "type": "string" + }, + "https_localhost8080_schema.json-_components_schemas_ErrorDetail": { + "type": "object", + "description": "The error detail.", + "properties": { + "code": { + "readOnly": true, + "type": "string", + "description": "The error code." + }, + "message": { + "readOnly": true, + "type": "string", + "description": "The error message." + }, + "target": { + "readOnly": true, + "type": "string", + "description": "The error target." + }, + "details": { + "readOnly": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/details" + }, + "description": "The error details." + } + } + } + }, + "securitySchemes": { + "ApiKey": { + "type": "apiKey", + "in": "header", + "name": "X-Api-Key" + } + } + } +} \ No newline at end of file diff --git a/test/data/toBundleExamples/remote_url_refs/circular/root.json b/test/data/toBundleExamples/remote_url_refs/circular/root.json new file mode 100644 index 000000000..9fc75a021 --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/circular/root.json @@ -0,0 +1,56 @@ +{ + "openapi": "3.0.2", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "Swagger API Team", + "email": "apiteam@swagger.io", + "url": "http://swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "paths": { + "/pets": { + "get": { + "description": "Returns all pets alesuada ac...", + "operationId": "findPets", + "responses": { + "200": { + "description": "An paged array of pets", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "https://localhost:8080/schema.json#/components/schemas/ErrorDetail" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "details": { + "description": "The detailed identifier of a spacecraft", + "type": "string" + } + }, + "securitySchemes": { + "ApiKey": { + "type": "apiKey", + "in": "header", + "name": "X-Api-Key" + } + } + } +} diff --git a/test/data/toBundleExamples/remote_url_refs/circular/schema.json b/test/data/toBundleExamples/remote_url_refs/circular/schema.json new file mode 100644 index 000000000..b74f2bbab --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/circular/schema.json @@ -0,0 +1,35 @@ +{ + "components": { + "schemas": { + "ErrorDetail": { + "type": "object", + "description": "The error detail.", + "properties": { + "code": { + "readOnly": true, + "type": "string", + "description": "The error code." + }, + "message": { + "readOnly": true, + "type": "string", + "description": "The error message." + }, + "target": { + "readOnly": true, + "type": "string", + "description": "The error target." + }, + "details": { + "readOnly": true, + "type": "array", + "items": { + "$ref": "https://localhost:8080/root.json#/components/schemas/details" + }, + "description": "The error details." + } + } + } + } + } +} diff --git a/test/data/toBundleExamples/remote_url_refs/expected.json b/test/data/toBundleExamples/remote_url_refs/expected.json new file mode 100644 index 000000000..cf2271309 --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/expected.json @@ -0,0 +1,122 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Sample API", + "description": "Buy or rent spacecrafts" + }, + "paths": { + "/spacecrafts/{spacecraftId}": { + "parameters": [ + { + "name": "spacecraftId", + "description": "The unique identifier of the spacecraft", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/SpacecraftId" + } + } + ], + "get": { + "summary": "Read a spacecraft", + "responses": { + "200": { + "description": "The spacecraft corresponding to the provided `spacecraftId`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/https_localhost8080_Spacecraft.json-components_schemas_Spacecraft" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "SpacecraftId": { + "description": "The unique identifier of a spacecraft", + "type": "string" + }, + "Error": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "description": "A human readable error message", + "type": "string" + } + } + }, + "https_localhost8080_Spacecraft.json-components_schemas_Spacecraft": { + "type": "object", + "required": [ + "variant", + "fuelFlowRate", + "type" + ], + "properties": { + "id": { + "description": "THE ID", + "type": "string" + }, + "variant": { + "description": "The identifier of a spacecraft", + "type": "string" + }, + "peakThrust": { + "type": "string", + "description": "PEAKTHRUST" + }, + "fuelFlowRate": { + "type": "string", + "description": "Fuel injection capacity rate" + }, + "maxImpulse": { + "description": "maxImpulse maxImpulse", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "capsule", + "probe", + "satellite", + "spaceplane", + "station" + ] + }, + "peakThrustSecond": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "description": "A human readable error message from nested deep ref", + "type": "string" + } + } + } + } + } + }, + "securitySchemes": { + "ApiKey": { + "type": "apiKey", + "in": "header", + "name": "X-Api-Key" + } + } + }, + "security": [ + { + "ApiKey": [] + } + ] +} \ No newline at end of file diff --git a/test/data/toBundleExamples/remote_url_refs/expected_2.json b/test/data/toBundleExamples/remote_url_refs/expected_2.json new file mode 100644 index 000000000..4de9b4599 --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/expected_2.json @@ -0,0 +1,80 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Sample API", + "description": "Buy or rent spacecrafts" + }, + "paths": { + "/spacecrafts/{spacecraftId}": { + "parameters": [ + { + "name": "spacecraftId", + "description": "The unique identifier of the spacecraft", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/https_localhost8080_SpacecraftId.json-_SpacecraftId" + } + } + ], + "get": { + "summary": "Read a spacecraft", + "responses": { + "200": { + "description": "The spacecraft corresponding to the provided `spacecraftId`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Spacecraft" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "SpacecraftId": { + "description": "The unique identifier of a spacecraft", + "type": "string" + }, + "Spacecraft": { + "type": "object", + "required": [ + "variant", + "fuelFlowRate", + "type" + ], + "properties": { + "id": { + "description": "THE ID", + "type": "string" + }, + "variant": { + "description": "The identifier of a spacecraft", + "type": "string" + } + } + }, + "https_localhost8080_SpacecraftId.json-_SpacecraftId": { + "description": "The unique identifier of a spacecraft", + "type": "string" + } + }, + "securitySchemes": { + "ApiKey": { + "type": "apiKey", + "in": "header", + "name": "X-Api-Key" + } + } + }, + "security": [ + { + "ApiKey": [] + } + ] +} \ No newline at end of file diff --git a/test/data/toBundleExamples/remote_url_refs/expected_3.json b/test/data/toBundleExamples/remote_url_refs/expected_3.json new file mode 100644 index 000000000..ec5a9ec46 --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/expected_3.json @@ -0,0 +1,76 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Sample API", + "description": "Buy or rent spacecrafts" + }, + "paths": { + "/spacecrafts/{spacecraftId}": { + "parameters": [ + { + "name": "spacecraftId", + "description": "The unique identifier of the spacecraft", + "in": "path", + "required": true, + "schema": { + "$ref": "https://localhost:8080/SpacecraftId.json#/SpacecraftId" + } + } + ], + "get": { + "summary": "Read a spacecraft", + "responses": { + "200": { + "description": "The spacecraft corresponding to the provided `spacecraftId`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Spacecraft" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "SpacecraftId": { + "description": "The unique identifier of a spacecraft", + "type": "string" + }, + "Spacecraft": { + "type": "object", + "required": [ + "variant", + "fuelFlowRate", + "type" + ], + "properties": { + "id": { + "description": "THE ID", + "type": "string" + }, + "variant": { + "description": "The identifier of a spacecraft", + "type": "string" + } + } + } + }, + "securitySchemes": { + "ApiKey": { + "type": "apiKey", + "in": "header", + "name": "X-Api-Key" + } + } + }, + "security": [ + { + "ApiKey": [] + } + ] +} \ No newline at end of file diff --git a/test/data/toBundleExamples/remote_url_refs/root.json b/test/data/toBundleExamples/remote_url_refs/root.json new file mode 100644 index 000000000..518da732d --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/root.json @@ -0,0 +1,70 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Sample API", + "description": "Buy or rent spacecrafts" + }, + "paths": { + "/spacecrafts/{spacecraftId}": { + "parameters": [ + { + "name": "spacecraftId", + "description": "The unique identifier of the spacecraft", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/SpacecraftId" + } + } + ], + "get": { + "summary": "Read a spacecraft", + "responses": { + "200": { + "description": "The spacecraft corresponding to the provided `spacecraftId`", + "content": { + "application/json": { + "schema": { + "$ref": "https://localhost:8080/Spacecraft.json#components/schemas/Spacecraft" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "SpacecraftId": { + "description": "The unique identifier of a spacecraft", + "type": "string" + }, + "Error": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "description": "A human readable error message", + "type": "string" + } + } + } + }, + "securitySchemes": { + "ApiKey": { + "type": "apiKey", + "in": "header", + "name": "X-Api-Key" + } + } + }, + "security": [ + { + "ApiKey": [] + } + ] +} \ No newline at end of file diff --git a/test/data/toBundleExamples/remote_url_refs/root_2.json b/test/data/toBundleExamples/remote_url_refs/root_2.json new file mode 100644 index 000000000..24d8245a4 --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/root_2.json @@ -0,0 +1,76 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Sample API", + "description": "Buy or rent spacecrafts" + }, + "paths": { + "/spacecrafts/{spacecraftId}": { + "parameters": [ + { + "name": "spacecraftId", + "description": "The unique identifier of the spacecraft", + "in": "path", + "required": true, + "schema": { + "$ref": "https://localhost:8080/SpacecraftId.json#/SpacecraftId" + } + } + ], + "get": { + "summary": "Read a spacecraft", + "responses": { + "200": { + "description": "The spacecraft corresponding to the provided `spacecraftId`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Spacecraft" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "SpacecraftId": { + "description": "The unique identifier of a spacecraft", + "type": "string" + }, + "Spacecraft": { + "type": "object", + "required": [ + "variant", + "fuelFlowRate", + "type" + ], + "properties": { + "id": { + "description": "THE ID", + "type": "string" + }, + "variant": { + "description": "The identifier of a spacecraft", + "type": "string" + } + } + } + }, + "securitySchemes": { + "ApiKey": { + "type": "apiKey", + "in": "header", + "name": "X-Api-Key" + } + } + }, + "security": [ + { + "ApiKey": [] + } + ] +} diff --git a/test/data/toBundleExamples/remote_url_refs/root_3.json b/test/data/toBundleExamples/remote_url_refs/root_3.json new file mode 100644 index 000000000..24d8245a4 --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/root_3.json @@ -0,0 +1,76 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Sample API", + "description": "Buy or rent spacecrafts" + }, + "paths": { + "/spacecrafts/{spacecraftId}": { + "parameters": [ + { + "name": "spacecraftId", + "description": "The unique identifier of the spacecraft", + "in": "path", + "required": true, + "schema": { + "$ref": "https://localhost:8080/SpacecraftId.json#/SpacecraftId" + } + } + ], + "get": { + "summary": "Read a spacecraft", + "responses": { + "200": { + "description": "The spacecraft corresponding to the provided `spacecraftId`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Spacecraft" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "SpacecraftId": { + "description": "The unique identifier of a spacecraft", + "type": "string" + }, + "Spacecraft": { + "type": "object", + "required": [ + "variant", + "fuelFlowRate", + "type" + ], + "properties": { + "id": { + "description": "THE ID", + "type": "string" + }, + "variant": { + "description": "The identifier of a spacecraft", + "type": "string" + } + } + } + }, + "securitySchemes": { + "ApiKey": { + "type": "apiKey", + "in": "header", + "name": "X-Api-Key" + } + } + }, + "security": [ + { + "ApiKey": [] + } + ] +} diff --git a/test/data/toBundleExamples/remote_url_refs/schemas/Error.json b/test/data/toBundleExamples/remote_url_refs/schemas/Error.json new file mode 100644 index 000000000..28743fea1 --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/schemas/Error.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "description": "A human readable error message", + "type": "string" + } + } +} \ No newline at end of file diff --git a/test/data/toBundleExamples/remote_url_refs/schemas/Spacecraft.json b/test/data/toBundleExamples/remote_url_refs/schemas/Spacecraft.json new file mode 100644 index 000000000..cd6bb58c2 --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/schemas/Spacecraft.json @@ -0,0 +1,49 @@ +{ + "components": { + "schemas": { + "Spacecraft": { + "type": "object", + "required": [ + "variant", + "fuelFlowRate", + "type" + ], + "properties": { + "id": { + "description": "THE ID", + "type": "string" + }, + "variant": { + "description": "The identifier of a spacecraft", + "type": "string" + }, + "peakThrust": { + "type": "string", + "description": "PEAKTHRUST" + }, + "fuelFlowRate": { + "type": "string", + "description": "Fuel injection capacity rate" + }, + "maxImpulse": { + "description": "maxImpulse maxImpulse", + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "capsule", + "probe", + "satellite", + "spaceplane", + "station" + ] + }, + "peakThrustSecond": { + "$ref": "http://localhost:8080/peakThrustSecond.json#components/schemas/peakThrustSecond" + } + } + } + } + } +} diff --git a/test/data/toBundleExamples/remote_url_refs/schemas/SpacecraftId.json b/test/data/toBundleExamples/remote_url_refs/schemas/SpacecraftId.json new file mode 100644 index 000000000..9d6406631 --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/schemas/SpacecraftId.json @@ -0,0 +1,6 @@ +{ + "SpacecraftId": { + "description": "The unique identifier of a spacecraft", + "type": "string" + } +} diff --git a/test/data/toBundleExamples/remote_url_refs/schemas/peakThrustSecond.json b/test/data/toBundleExamples/remote_url_refs/schemas/peakThrustSecond.json new file mode 100644 index 000000000..7c4f5be03 --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/schemas/peakThrustSecond.json @@ -0,0 +1,9 @@ +{ + "components": { + "schemas": { + "peakThrustSecond": { + "$ref": "http://localhost:8080/peakThrustSecondProperty.json#components/schemas/peakThrustSecondProperty" + } + } + } +} diff --git a/test/data/toBundleExamples/remote_url_refs/schemas/peakThrustSecondProperty.json b/test/data/toBundleExamples/remote_url_refs/schemas/peakThrustSecondProperty.json new file mode 100644 index 000000000..0f99a0d3c --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/schemas/peakThrustSecondProperty.json @@ -0,0 +1,18 @@ +{ + "components": { + "schemas": { + "peakThrustSecondProperty": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "description": "A human readable error message from nested deep ref", + "type": "string" + } + } + } + } + } +} diff --git a/test/data/toBundleExamples/remote_url_refs/yaml/SpacecraftId.yaml b/test/data/toBundleExamples/remote_url_refs/yaml/SpacecraftId.yaml new file mode 100644 index 000000000..1c534cb5e --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/yaml/SpacecraftId.yaml @@ -0,0 +1,3 @@ +SpacecraftId: + description: The unique identifier of a spacecraft + type: string diff --git a/test/data/toBundleExamples/remote_url_refs/yaml/expected.json b/test/data/toBundleExamples/remote_url_refs/yaml/expected.json new file mode 100644 index 000000000..77a8d2938 --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/yaml/expected.json @@ -0,0 +1,80 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Sample API", + "description": "Buy or rent spacecrafts" + }, + "paths": { + "/spacecrafts/{spacecraftId}": { + "parameters": [ + { + "name": "spacecraftId", + "description": "The unique identifier of the spacecraft", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/https_localhost8080_SpacecraftId.yaml-_SpacecraftId" + } + } + ], + "get": { + "summary": "Read a spacecraft", + "responses": { + "200": { + "description": "The spacecraft corresponding to the provided `spacecraftId`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Spacecraft" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "SpacecraftId": { + "description": "The unique identifier of a spacecraft", + "type": "string" + }, + "Spacecraft": { + "type": "object", + "required": [ + "variant", + "fuelFlowRate", + "type" + ], + "properties": { + "id": { + "description": "THE ID", + "type": "string" + }, + "variant": { + "description": "The identifier of a spacecraft", + "type": "string" + } + } + }, + "https_localhost8080_SpacecraftId.yaml-_SpacecraftId": { + "description": "The unique identifier of a spacecraft", + "type": "string" + } + }, + "securitySchemes": { + "ApiKey": { + "type": "apiKey", + "in": "header", + "name": "X-Api-Key" + } + } + }, + "security": [ + { + "ApiKey": [] + } + ] +} \ No newline at end of file diff --git a/test/data/toBundleExamples/remote_url_refs/yaml/expected.yaml b/test/data/toBundleExamples/remote_url_refs/yaml/expected.yaml new file mode 100644 index 000000000..0b579011a --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/yaml/expected.yaml @@ -0,0 +1,52 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Sample API + description: Buy or rent spacecrafts +paths: + '/spacecrafts/{spacecraftId}': + parameters: + - name: spacecraftId + description: The unique identifier of the spacecraft + in: path + required: true + schema: + $ref: >- + #/components/schemas/https_localhost8080_SpacecraftId.yaml-_SpacecraftId + get: + summary: Read a spacecraft + responses: + '200': + description: The spacecraft corresponding to the provided `spacecraftId` + content: + application/json: + schema: + $ref: '#/components/schemas/Spacecraft' +components: + schemas: + SpacecraftId: + description: The unique identifier of a spacecraft + type: string + Spacecraft: + type: object + required: + - variant + - fuelFlowRate + - type + properties: + id: + description: THE ID + type: string + variant: + description: The identifier of a spacecraft + type: string + https_localhost8080_SpacecraftId.yaml-_SpacecraftId: + description: The unique identifier of a spacecraft + type: string + securitySchemes: + ApiKey: + type: apiKey + in: header + name: X-Api-Key +security: + - ApiKey: [] diff --git a/test/data/toBundleExamples/remote_url_refs/yaml/root.json b/test/data/toBundleExamples/remote_url_refs/yaml/root.json new file mode 100644 index 000000000..b1ebd01dd --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/yaml/root.json @@ -0,0 +1,76 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Sample API", + "description": "Buy or rent spacecrafts" + }, + "paths": { + "/spacecrafts/{spacecraftId}": { + "parameters": [ + { + "name": "spacecraftId", + "description": "The unique identifier of the spacecraft", + "in": "path", + "required": true, + "schema": { + "$ref": "https://localhost:8080/SpacecraftId.yaml#/SpacecraftId" + } + } + ], + "get": { + "summary": "Read a spacecraft", + "responses": { + "200": { + "description": "The spacecraft corresponding to the provided `spacecraftId`", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Spacecraft" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "SpacecraftId": { + "description": "The unique identifier of a spacecraft", + "type": "string" + }, + "Spacecraft": { + "type": "object", + "required": [ + "variant", + "fuelFlowRate", + "type" + ], + "properties": { + "id": { + "description": "THE ID", + "type": "string" + }, + "variant": { + "description": "The identifier of a spacecraft", + "type": "string" + } + } + } + }, + "securitySchemes": { + "ApiKey": { + "type": "apiKey", + "in": "header", + "name": "X-Api-Key" + } + } + }, + "security": [ + { + "ApiKey": [] + } + ] +} diff --git a/test/data/toBundleExamples/remote_url_refs/yaml/root.yaml b/test/data/toBundleExamples/remote_url_refs/yaml/root.yaml new file mode 100644 index 000000000..897f5f901 --- /dev/null +++ b/test/data/toBundleExamples/remote_url_refs/yaml/root.yaml @@ -0,0 +1,49 @@ +--- +openapi: 3.0.0 +info: + version: 1.0.0 + title: Sample API + description: Buy or rent spacecrafts +paths: + "/spacecrafts/{spacecraftId}": + parameters: + - name: spacecraftId + description: The unique identifier of the spacecraft + in: path + required: true + schema: + "$ref": https://localhost:8080/SpacecraftId.yaml#/SpacecraftId + get: + summary: Read a spacecraft + responses: + '200': + description: The spacecraft corresponding to the provided `spacecraftId` + content: + application/json: + schema: + "$ref": "#/components/schemas/Spacecraft" +components: + schemas: + SpacecraftId: + description: The unique identifier of a spacecraft + type: string + Spacecraft: + type: object + required: + - variant + - fuelFlowRate + - type + properties: + id: + description: THE ID + type: string + variant: + description: The identifier of a spacecraft + type: string + securitySchemes: + ApiKey: + type: apiKey + in: header + name: X-Api-Key +security: +- ApiKey: [] diff --git a/test/data/valid_openapi/custom_headers.json b/test/data/valid_openapi/custom_headers.json index 4d3e93495..66acb0735 100644 --- a/test/data/valid_openapi/custom_headers.json +++ b/test/data/valid_openapi/custom_headers.json @@ -16,12 +16,6 @@ "type": "integer", "format": "int32" } - }, - "application/vnd.retailer.v3+json": { - "schema": { - "type": "integer", - "format": "int32" - } } } } @@ -34,4 +28,4 @@ "url": "https://api.com" } ] -} \ No newline at end of file +} diff --git a/test/data/valid_openapi/description-test.yaml b/test/data/valid_openapi/description-test.yaml new file mode 100644 index 000000000..434393fd9 --- /dev/null +++ b/test/data/valid_openapi/description-test.yaml @@ -0,0 +1,57 @@ +openapi: "3.0.0" +info: + version: "1.0.0" + title: "Sample API" + description: Buy or rent spacecrafts + +paths: + /space/{spacecraftId}: + get: + parameters: + - name: spacecraftId + description: "Required spacecraftId path param" + in: path + required: true + schema: + type: string + - name: pathParamOptional + description: "Path param optional description" + in: path + required: false + schema: + type: string + - name: limit + in: query + description: "QUERY PARAM DESCRIPTION" + required: true + schema: + type: integer + format: int32 + - name: optionalQueryParam + in: query + description: "QUERY PARAM Optional" + required: false + schema: + type: integer + format: int32 + - name: page + in: header + description: "HEADER PARAM DESCRIPTION" + required: true + schema: + type: string + - name: offset + in: header + description: "HEADER PARAM Optional" + required: false + schema: + type: string + + summary: Read a spacecraft + responses: + "201": + description: The spacecraft corresponding to the provided `spacecraftId` + content: + application/json: + schema: + type: string diff --git a/test/data/valid_openapi/multiExampleMatchingRequestResponse.yaml b/test/data/valid_openapi/multiExampleMatchingRequestResponse.yaml new file mode 100644 index 000000000..050a93473 --- /dev/null +++ b/test/data/valid_openapi/multiExampleMatchingRequestResponse.yaml @@ -0,0 +1,69 @@ +openapi: 3.0.0 +info: + title: None + version: 1.0.0 + description: None +paths: + /v1: + post: + requestBody: + content: + 'application/json': + schema: + $ref: "#/components/schemas/World" + examples: + valid-request: + value: + includedFields: + - user + - height + - weight + missing-required-parameter: + value: + includedFields: + - user + responses: + 200: + description: None + content: + 'application/json': + schema: + $ref: "#/components/schemas/Request" + examples: + valid-request: + summary: Complete request + value: + { + "user": 1, + "height": 168, + "weight": 44 + } + missing-required-parameter: + summary: Request with only required params + value: + { + "user": 1 + } +components: + schemas: + World: + type: object + properties: + includedFields: + type: array + Request: + type: object + required: + - user + - height + - weight + properties: + user: + type: integer + description: None + height: + type: integer + description: None + weight: + type: integer + description: None diff --git a/test/data/valid_openapi/multiExampleRequest.yaml b/test/data/valid_openapi/multiExampleRequest.yaml new file mode 100644 index 000000000..5909e5487 --- /dev/null +++ b/test/data/valid_openapi/multiExampleRequest.yaml @@ -0,0 +1,50 @@ +openapi: 3.0.0 +info: + title: None + version: 1.0.0 + description: None +paths: + /v1/: + post: + requestBody: + content: + 'application/json': + schema: + $ref: "#/components/schemas/Request" + examples: + valid-request: + value: + { + "user": 1, + "height": 168, + "weight": 44 + } + missing-required-parameter: + value: + { + "user": 1 + } + responses: + 200: + description: None + content: + 'application/json': + example: { hello: 'world' } +components: + schemas: + Request: + type: object + required: + - user + - height + - weight + properties: + user: + type: integer + description: None + height: + type: integer + description: None + weight: + type: integer + description: None diff --git a/test/data/valid_openapi/multiExampleRequestResponse.yaml b/test/data/valid_openapi/multiExampleRequestResponse.yaml new file mode 100644 index 000000000..3b4c31d0e --- /dev/null +++ b/test/data/valid_openapi/multiExampleRequestResponse.yaml @@ -0,0 +1,70 @@ +openapi: 3.0.0 +info: + title: None + version: 1.0.0 + description: None +paths: + /v1: + post: + requestBody: + content: + 'application/json': + schema: + $ref: "#/components/schemas/World" + examples: + valid-request: + value: + includedFields: + - user + - height + - weight + missing-required-parameter: + value: + includedFields: + - user + responses: + 200: + description: None + content: + 'application/json': + schema: + $ref: "#/components/schemas/Request" + examples: + not-matching-key: + summary: Request with only required params + value: + { + "user": 1 + } + not-matching-key-2: + summary: Complete request + value: + { + "user": 1, + "height": 168, + "weight": 44 + } + +components: + schemas: + World: + type: object + properties: + includedFields: + type: array + Request: + type: object + required: + - user + - height + - weight + properties: + user: + type: integer + description: None + height: + type: integer + description: None + weight: + type: integer + description: None diff --git a/test/data/valid_openapi/multiExampleResponse.yaml b/test/data/valid_openapi/multiExampleResponse.yaml new file mode 100644 index 000000000..9386ae380 --- /dev/null +++ b/test/data/valid_openapi/multiExampleResponse.yaml @@ -0,0 +1,60 @@ +openapi: 3.0.0 +info: + title: None + version: 1.0.0 + description: None +paths: + /v1: + post: + requestBody: + content: + 'application/json': + schema: + $ref: "#/components/schemas/World" + examples: + hello-world: + value: + hello: world + responses: + 200: + description: None + content: + 'application/json': + schema: + $ref: "#/components/schemas/Request" + examples: + valid-request: + value: + { + "user": 1, + "height": 168, + "weight": 44 + } + missing-required-parameter: + value: + { + "user": 1 + } +components: + schemas: + World: + type: object + properties: + hello: + type: string + Request: + type: object + required: + - user + - height + - weight + properties: + user: + type: integer + description: None + height: + type: integer + description: None + weight: + type: integer + description: None diff --git a/test/unit/base.test.js b/test/unit/base.test.js index 410c45b3c..1e7e90905 100644 --- a/test/unit/base.test.js +++ b/test/unit/base.test.js @@ -371,7 +371,7 @@ describe('CONVERT FUNCTION TESTS ', function() { Converter.convert({ type: 'string', data: openapi }, { schemaFaker: true }, (err, conversionResult) => { expect(err).to.be.null; expect(conversionResult.output[0].data.item[0].response[0].header[0].value) - .to.equal('application/vnd.retailer.v3+json'); + .to.equal('application/vnd.retailer.v3+xml'); done(); }); }); diff --git a/test/unit/bundle.test.js b/test/unit/bundle.test.js index 7a796b853..330adf06c 100644 --- a/test/unit/bundle.test.js +++ b/test/unit/bundle.test.js @@ -11,6 +11,7 @@ let expect = require('chai').expect, nestedRefsFromRootComponents = path.join(__dirname, BUNDLES_FOLDER + '/nested_references_from_root_components'), localRefFolder = path.join(__dirname, BUNDLES_FOLDER + '/local_references'), schemaFromResponse = path.join(__dirname, BUNDLES_FOLDER + '/schema_from_response'), + remoteURLRefExamples = path.join(__dirname, BUNDLES_FOLDER + '/remote_url_refs'), petstoreFolder = path.join(__dirname, PETSTORE_FOLDER), withParamsFolder = path.join(__dirname, BUNDLES_FOLDER + '/with_parameters'), withRefInItems = path.join(__dirname, BUNDLES_FOLDER + '/with_ref_in_items'), @@ -2712,10 +2713,281 @@ describe('bundle files method - 3.0', function () { expect(res.output.specification.version).to.equal('3.0'); expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected); }); + + it('Should return bundled file as json - remote_url_refs', async function () { + let contentRootFile = fs.readFileSync(remoteURLRefExamples + '/root_2.json', 'utf8'), + spacecraftId = fs.readFileSync(remoteURLRefExamples + '/schemas/SpacecraftId.json', 'utf8'), + + remoteRefResolver = async (refURL) => { + if (refURL.includes('SpacecraftId')) { + return JSON.parse(spacecraftId); + } + }, + expected = fs.readFileSync(remoteURLRefExamples + '/expected_2.json', 'utf8'), + input = { + type: 'multiFile', + specificationVersion: '3.0', + rootFiles: [ + { + path: 'root.json' + } + ], + data: [ + { + path: 'root.json', + content: contentRootFile + } + ], + options: {}, + bundleFormat: 'JSON', + remoteRefResolver + }; + + const res = await Converter.bundle(input); + + expect(res).to.not.be.empty; + expect(res.result).to.be.true; + expect(res.output.specification.version).to.equal('3.0'); + expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected); + }); + + it('Should return bundled file as json with deep url refs - remote_url_refs', async function () { + let contentRootFile = fs.readFileSync(remoteURLRefExamples + '/root.json', 'utf8'), + spacecraft = fs.readFileSync(remoteURLRefExamples + '/schemas/Spacecraft.json', 'utf8'), + peakThrustSecond = fs.readFileSync(remoteURLRefExamples + '/schemas/peakThrustSecond.json', 'utf8'), + peakThrustSecondProperty = + fs.readFileSync(remoteURLRefExamples + '/schemas/peakThrustSecondProperty.json', 'utf8'), + remoteRefResolver = async (refURL) => { + if (refURL.includes('peakThrustSecondProperty')) { + return JSON.parse(peakThrustSecondProperty); + } + + if (refURL.includes('peakThrustSecond')) { + return JSON.parse(peakThrustSecond); + } + + if (refURL.includes('Spacecraft')) { + return JSON.parse(spacecraft); + } + }, + expected = fs.readFileSync(remoteURLRefExamples + '/expected.json', 'utf8'), + input = { + type: 'multiFile', + specificationVersion: '3.0', + rootFiles: [ + { + path: 'root.json' + } + ], + data: [ + { + path: 'root.json', + content: contentRootFile + } + ], + options: {}, + bundleFormat: 'JSON', + remoteRefResolver + }; + + const res = await Converter.bundle(input); + + expect(res).to.not.be.empty; + expect(res.result).to.be.true; + expect(res.output.specification.version).to.equal('3.0'); + expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected); + }); + + it('Should return bundled file as json with yaml content remote refs - remote_url_refs', async function () { + let contentRootFile = fs.readFileSync(remoteURLRefExamples + '/yaml/root.json', 'utf8'), + spacecraftId = fs.readFileSync(remoteURLRefExamples + '/yaml/SpacecraftId.yaml', 'utf8'), + + remoteRefResolver = async (refURL) => { + if (refURL.includes('SpacecraftId')) { + return spacecraftId; + } + }, + expected = fs.readFileSync(remoteURLRefExamples + '/yaml/expected.json', 'utf8'), + input = { + type: 'multiFile', + specificationVersion: '3.0', + rootFiles: [ + { + path: 'root.json' + } + ], + data: [ + { + path: 'root.json', + content: contentRootFile + } + ], + options: {}, + bundleFormat: 'JSON', + remoteRefResolver + }; + + const res = await Converter.bundle(input); + + expect(res).to.not.be.empty; + expect(res.result).to.be.true; + expect(res.output.specification.version).to.equal('3.0'); + expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected); + }); + + it('Should return bundled file as yaml with yaml content remote refs - remote_url_refs', async function () { + let contentRootFile = fs.readFileSync(remoteURLRefExamples + '/yaml/root.yaml', 'utf8'), + spacecraftId = fs.readFileSync(remoteURLRefExamples + '/yaml/SpacecraftId.yaml', 'utf8'), + + remoteRefResolver = async (refURL) => { + if (refURL.includes('SpacecraftId')) { + return spacecraftId; + } + }, + expected = fs.readFileSync(remoteURLRefExamples + '/yaml/expected.yaml', 'utf8'), + input = { + type: 'multiFile', + specificationVersion: '3.0', + rootFiles: [ + { + path: 'root.json' + } + ], + data: [ + { + path: 'root.json', + content: contentRootFile + } + ], + options: {}, + bundleFormat: 'YAML', + remoteRefResolver + }; + + const res = await Converter.bundle(input); + + expect(res).to.not.be.empty; + expect(res.result).to.be.true; + expect(res.output.specification.version).to.equal('3.0'); + expect(res.output.data[0].bundledContent).to.be.equal(expected); + }); + + it('Should return bundled file as json with yaml content refs and root file - remote_url_refs', async function () { + let contentRootFile = fs.readFileSync(remoteURLRefExamples + '/yaml/root.yaml', 'utf8'), + spacecraftId = fs.readFileSync(remoteURLRefExamples + '/yaml/SpacecraftId.yaml', 'utf8'), + + remoteRefResolver = async (refURL) => { + if (refURL.includes('SpacecraftId')) { + return spacecraftId; + } + }, + expected = fs.readFileSync(remoteURLRefExamples + '/yaml/expected.json', 'utf8'), + input = { + type: 'multiFile', + specificationVersion: '3.0', + rootFiles: [ + { + path: 'root.json' + } + ], + data: [ + { + path: 'root.json', + content: contentRootFile + } + ], + options: {}, + bundleFormat: 'JSON', + remoteRefResolver + }; + + const res = await Converter.bundle(input); + + expect(res).to.not.be.empty; + expect(res.result).to.be.true; + expect(res.output.specification.version).to.equal('3.0'); + expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected); + }); + + it('Should return bundled file as json without resolving the reference if resolver threw err - remote_url_refs', + async function () { + let contentRootFile = fs.readFileSync(remoteURLRefExamples + '/root_3.json', 'utf8'), + remoteRefResolver = async () => { + // eslint-disable-next-line no-throw-literal + throw { message: 'Something went wrong' }; + }, + expected = fs.readFileSync(remoteURLRefExamples + '/expected_3.json', 'utf8'), + input = { + type: 'multiFile', + specificationVersion: '3.0', + rootFiles: [ + { + path: 'root.json' + } + ], + data: [ + { + path: 'root.json', + content: contentRootFile + } + ], + options: {}, + bundleFormat: 'JSON', + remoteRefResolver + }; + + const res = await Converter.bundle(input); + + expect(res).to.not.be.empty; + expect(res.result).to.be.true; + expect(res.output.specification.version).to.equal('3.0'); + expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected); + }); + + it('Should return bundled file as json with circular url refs - remote_url_refs', async function () { + let contentRootFile = fs.readFileSync(remoteURLRefExamples + '/circular/root.json', 'utf8'), + schema = fs.readFileSync(remoteURLRefExamples + '/circular/schema.json', 'utf8'), + + remoteRefResolver = async (refURL) => { + if (refURL.includes('schema')) { + return JSON.parse(schema); + } + + if (refURL.includes('root')) { + return JSON.parse(contentRootFile); + } + }, + expected = fs.readFileSync(remoteURLRefExamples + '/circular/expected.json', 'utf8'), + input = { + type: 'multiFile', + specificationVersion: '3.0', + rootFiles: [ + { + path: 'root.json' + } + ], + data: [ + { + path: 'root.json', + content: contentRootFile + } + ], + options: {}, + bundleFormat: 'JSON', + remoteRefResolver + }; + + const res = await Converter.bundle(input); + + expect(res).to.not.be.empty; + expect(res.result).to.be.true; + expect(res.output.specification.version).to.equal('3.0'); + expect(JSON.stringify(JSON.parse(res.output.data[0].bundledContent), null, 2)).to.be.equal(expected); + }); }); describe('getReferences method when node does not have any reference', function() { - it('Should return reference data empty if there are not any reference', function() { + it('Should return reference data empty if there are not any reference', async function() { const userData = 'type: object\n' + 'properties:\n' + ' id:\n' + @@ -2724,7 +2996,7 @@ describe('getReferences method when node does not have any reference', function( ' type: string', userNode = parse.getOasObject(userData), nodeIsRoot = false, - result = getReferences( + result = await getReferences( userNode.oasObject, nodeIsRoot, removeLocalReferenceFromPath, @@ -2738,7 +3010,7 @@ describe('getReferences method when node does not have any reference', function( expect(Object.keys(result.nodeReferenceDirectory).length).to.equal(0); }); - it('Should return the reference data - schema_from_response', function() { + it('Should return the reference data - schema_from_response', async function() { const userData = 'User:\n' + ' $ref: \"./user.yaml\"\n' + '\n' + @@ -2758,7 +3030,7 @@ describe('getReferences method when node does not have any reference', function( ' type: string', userNode = parse.getOasObject(userData), nodeIsRoot = false, - result = getReferences( + result = await getReferences( userNode.oasObject, nodeIsRoot, removeLocalReferenceFromPath, diff --git a/test/unit/convertV2.test.js b/test/unit/convertV2.test.js index a6ed95316..19e77cfd5 100644 --- a/test/unit/convertV2.test.js +++ b/test/unit/convertV2.test.js @@ -53,6 +53,7 @@ const expect = require('chai').expect, path.join(__dirname, VALID_OPENAPI_PATH, '/query_param_with_enum_resolve_as_example.json'), formDataParamDescription = path.join(__dirname, VALID_OPENAPI_PATH, '/form_data_param_description.yaml'), allHTTPMethodsSpec = path.join(__dirname, VALID_OPENAPI_PATH, '/all-http-methods.yaml'), + descriptionTestSpec = path.join(__dirname, VALID_OPENAPI_PATH, '/description-test.yaml'), invalidNullInfo = path.join(__dirname, INVALID_OPENAPI_PATH, '/invalid-null-info.json'), invalidNullInfoTitle = path.join(__dirname, INVALID_OPENAPI_PATH, '/invalid-info-null-title.json'), invalidNullInfoVersion = path.join(__dirname, INVALID_OPENAPI_PATH, '/invalid-info-null-version.json'), @@ -93,7 +94,15 @@ const expect = require('chai').expect, recursiveRefComponents = path.join(__dirname, VALID_OPENAPI_PATH, '/recursiveRefComponents.yaml'), securityAuthUnresolvedInPathItem = - path.join(__dirname, VALID_OPENAPI_PATH, '/securityAuthUnresolvedInPathItem.yaml'); + path.join(__dirname, VALID_OPENAPI_PATH, '/securityAuthUnresolvedInPathItem.yaml'), + multiExampleRequest = + path.join(__dirname, VALID_OPENAPI_PATH, '/multiExampleRequest.yaml'), + multiExampleResponse = + path.join(__dirname, VALID_OPENAPI_PATH, '/multiExampleResponse.yaml'), + multiExampleRequestResponse = + path.join(__dirname, VALID_OPENAPI_PATH, '/multiExampleRequestResponse.yaml'), + multiExampleMatchingRequestResponse = + path.join(__dirname, VALID_OPENAPI_PATH, '/multiExampleMatchingRequestResponse.yaml'); describe('The convert v2 Function', function() { @@ -1159,6 +1168,38 @@ describe('The convert v2 Function', function() { }); }); + it('should generate a collection with description for Query Params, Path variables and Headers', function(done) { + var openapi = fs.readFileSync(descriptionTestSpec, 'utf8'); + + Converter.convertV2({ type: 'string', data: openapi }, + {}, (err, conversionResult) => { + expect( + conversionResult.output[0].data.item[0].item[0].item[0].request.url.query[0].description.content + ).to.equal('(Required) QUERY PARAM DESCRIPTION'); + + expect( + conversionResult.output[0].data.item[0].item[0].item[0].request.url.query[1].description.content + ).to.equal('QUERY PARAM Optional'); + + expect( + conversionResult.output[0].data.item[0].item[0].item[0].request.url.variable[0].description.content + ).to.equal('(Required) Required spacecraftId path param'); + + expect( + conversionResult.output[0].data.item[0].item[0].item[0].request.url.variable[1].description.content + ).to.equal('Path param optional description'); + + expect( + conversionResult.output[0].data.item[0].item[0].item[0].request.header[0].description.content + ).to.equal('(Required) HEADER PARAM DESCRIPTION'); + + expect( + conversionResult.output[0].data.item[0].item[0].item[0].request.header[1].description.content + ).to.equal('HEADER PARAM Optional'); + done(); + }); + }); + it('Should have disableBodyPruning option for protocolProfileBehavior set to true for all types of request' + allHTTPMethodsSpec, function (done) { var openapi = fs.readFileSync(allHTTPMethodsSpec, 'utf8'); @@ -2385,4 +2426,159 @@ describe('The convert v2 Function', function() { done(); }); }); + + describe('Should generate multiple examples when', function() { + it('request body contains multiple examples but request body has single example', function(done) { + var openapi = fs.readFileSync(multiExampleRequest, 'utf8'); + Converter.convertV2({ type: 'string', data: openapi }, { parametersResolution: 'Example' }, + (err, conversionResult) => { + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output.length).to.equal(1); + expect(conversionResult.output[0].type).to.equal('collection'); + expect(conversionResult.output[0].data).to.have.property('info'); + expect(conversionResult.output[0].data).to.have.property('item'); + expect(conversionResult.output[0].data.item[0].item.length).to.equal(1); + + const item = conversionResult.output[0].data.item[0].item[0]; + + expect(JSON.parse(item.request.body.raw)).to.eql({ + user: 1, + height: 168, + weight: 44 + }); + expect(item.response).to.have.lengthOf(2); + expect(item.response[0].name).to.eql('valid-request'); + expect(item.response[0]._postman_previewlanguage).to.eql('json'); + expect(JSON.parse(item.response[0].body)).to.eql({ hello: 'world' }); + expect(JSON.parse(item.response[0].originalRequest.body.raw)).to.eql({ + user: 1, + height: 168, + weight: 44 + }); + + expect(item.response[1].name).to.eql('missing-required-parameter'); + expect(item.response[1]._postman_previewlanguage).to.eql('json'); + expect(JSON.parse(item.response[1].body)).to.eql({ hello: 'world' }); + expect(JSON.parse(item.response[1].originalRequest.body.raw)).to.eql({ + user: 1 + }); + done(); + }); + }); + + it('response body contains multiple examples but response body has single example', function(done) { + var openapi = fs.readFileSync(multiExampleResponse, 'utf8'); + Converter.convertV2({ type: 'string', data: openapi }, { parametersResolution: 'Example' }, + (err, conversionResult) => { + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output.length).to.equal(1); + expect(conversionResult.output[0].type).to.equal('collection'); + expect(conversionResult.output[0].data).to.have.property('info'); + expect(conversionResult.output[0].data).to.have.property('item'); + expect(conversionResult.output[0].data.item[0].item.length).to.equal(1); + + const item = conversionResult.output[0].data.item[0].item[0]; + + expect(JSON.parse(item.request.body.raw)).to.eql({ hello: 'world' }); + expect(item.response).to.have.lengthOf(2); + expect(item.response[0].name).to.eql('valid-request'); + expect(item.response[0]._postman_previewlanguage).to.eql('json'); + expect(JSON.parse(item.response[0].body)).to.eql({ + user: 1, + height: 168, + weight: 44 + }); + expect(JSON.parse(item.response[0].originalRequest.body.raw)).to.eql({ hello: 'world' }); + + expect(item.response[1].name).to.eql('missing-required-parameter'); + expect(item.response[1]._postman_previewlanguage).to.eql('json'); + expect(JSON.parse(item.response[1].body)).to.eql({ user: 1 }); + expect(JSON.parse(item.response[1].originalRequest.body.raw)).to.eql({ hello: 'world' }); + done(); + }); + }); + + it('both request and response body contains multiple examples with matching keys', function(done) { + var openapi = fs.readFileSync(multiExampleMatchingRequestResponse, 'utf8'); + Converter.convertV2({ type: 'string', data: openapi }, { parametersResolution: 'Example' }, + (err, conversionResult) => { + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output.length).to.equal(1); + expect(conversionResult.output[0].type).to.equal('collection'); + expect(conversionResult.output[0].data).to.have.property('info'); + expect(conversionResult.output[0].data).to.have.property('item'); + expect(conversionResult.output[0].data.item[0].item.length).to.equal(1); + + const item = conversionResult.output[0].data.item[0].item[0]; + + expect(JSON.parse(item.request.body.raw)).to.eql({ + includedFields: ['user', 'height', 'weight'] + }); + expect(item.response).to.have.lengthOf(2); + expect(item.response[0].name).to.eql('Complete request'); + expect(item.response[0]._postman_previewlanguage).to.eql('json'); + expect(JSON.parse(item.response[0].body)).to.eql({ + user: 1, + height: 168, + weight: 44 + }); + expect(JSON.parse(item.response[0].originalRequest.body.raw)).to.eql({ + includedFields: ['user', 'height', 'weight'] + }); + + expect(item.response[1].name).to.eql('Request with only required params'); + expect(item.response[1]._postman_previewlanguage).to.eql('json'); + expect(JSON.parse(item.response[1].body)).to.eql({ user: 1 }); + expect(JSON.parse(item.response[1].originalRequest.body.raw)).to.eql({ + includedFields: ['user'] + }); + done(); + }); + }); + + it('both request and response body contains multiple examples in mentioned order when no matching keys', + function(done) { + var openapi = fs.readFileSync(multiExampleRequestResponse, 'utf8'); + Converter.convertV2({ type: 'string', data: openapi }, { parametersResolution: 'Example' }, + (err, conversionResult) => { + expect(err).to.be.null; + expect(conversionResult.result).to.equal(true); + expect(conversionResult.output.length).to.equal(1); + expect(conversionResult.output[0].type).to.equal('collection'); + expect(conversionResult.output[0].data).to.have.property('info'); + expect(conversionResult.output[0].data).to.have.property('item'); + expect(conversionResult.output[0].data.item[0].item.length).to.equal(1); + + const item = conversionResult.output[0].data.item[0].item[0]; + + expect(JSON.parse(item.request.body.raw)).to.eql({ + includedFields: ['user', 'height', 'weight'] + }); + expect(item.response).to.have.lengthOf(2); + expect(item.response[0].name).to.eql('Request with only required params'); + expect(item.response[0]._postman_previewlanguage).to.eql('json'); + expect(JSON.parse(item.response[0].body)).to.eql({ + user: 1 + }); + expect(JSON.parse(item.response[0].originalRequest.body.raw)).to.eql({ + includedFields: ['user', 'height', 'weight'] + }); + + expect(item.response[1].name).to.eql('Complete request'); + expect(item.response[1]._postman_previewlanguage).to.eql('json'); + expect(JSON.parse(item.response[1].body)).to.eql({ + user: 1, + height: 168, + weight: 44 + }); + expect(JSON.parse(item.response[1].originalRequest.body.raw)).to.eql({ + includedFields: ['user'] + }); + done(); + }); + }); + }); });