diff --git a/CHANGELOG.md b/CHANGELOG.md index e0c43e8bd..ce6d8ec92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # OpenAPI-Postman Changelog +#### v1.1.12 (Mar 26, 2020) +* Fix for https://github.com/postmanlabs/openapi-to-postman/issues/133 and https://github.com/postmanlabs/openapi-to-postman/issues/101 +* Ignore resolving circular references. +* Upgrade commander from 2.3.0 to 2.20.3 +* Upgrade postman-collection from 3.5.1 to 3.5.5 + #### v1.1.11 (Mar 14, 2020) * Safely handling invalid reference schemas/properties diff --git a/lib/deref.js b/lib/deref.js index eef9ce612..5500cdfaa 100644 --- a/lib/deref.js +++ b/lib/deref.js @@ -55,21 +55,22 @@ module.exports = { * @param {*} components components in openapi spec. * @param {object} schemaResolutionCache stores already resolved references * @param {*} stack counter which keeps a tab on nested schemas + * @param {*} seenRef References that are repeated. Used to identify circular references. * @returns {*} schema - schema that adheres to all individual schemas in schemaArr */ - resolveAllOf: function (schemaArr, parameterSourceOption, components, schemaResolutionCache, stack = 0) { + resolveAllOf: function (schemaArr, parameterSourceOption, components, schemaResolutionCache, stack = 0, seenRef) { if (!(schemaArr instanceof Array)) { return null; } if (schemaArr.length === 1) { // for just one entry in allOf, don't need to enforce type: object restriction - return this.resolveRefs(schemaArr[0], parameterSourceOption, components, schemaResolutionCache, stack); + return this.resolveRefs(schemaArr[0], parameterSourceOption, components, schemaResolutionCache, stack, seenRef); } // generate one object for each schema let indivObjects = schemaArr.map((schema) => { - return this.resolveRefs(schema, parameterSourceOption, components, schemaResolutionCache, stack); + return this.resolveRefs(schema, parameterSourceOption, components, schemaResolutionCache, stack, seenRef); }).filter((schema) => { return schema.type === 'object'; }), @@ -107,9 +108,10 @@ module.exports = { * @param {*} components components in openapi spec. * @param {object} schemaResolutionCache stores already resolved references * @param {*} stack counter which keeps a tab on nested schemas + * @param {*} seenRef - References that are repeated. Used to identify circular references. * @returns {*} schema satisfying JSON-schema-faker. */ - resolveRefs: function (schema, parameterSourceOption, components, schemaResolutionCache, stack = 0) { + resolveRefs: function (schema, parameterSourceOption, components, schemaResolutionCache, stack = 0, seenRef = {}) { var resolvedSchema, prop, splitRef; stack++; schemaResolutionCache = schemaResolutionCache || {}; @@ -122,16 +124,27 @@ module.exports = { } if (schema.anyOf) { - return this.resolveRefs(schema.anyOf[0], parameterSourceOption, components, schemaResolutionCache, stack); + return this.resolveRefs(schema.anyOf[0], parameterSourceOption, components, schemaResolutionCache, stack, + _.cloneDeep(seenRef)); } if (schema.oneOf) { - return this.resolveRefs(schema.oneOf[0], parameterSourceOption, components, schemaResolutionCache, stack); + return this.resolveRefs(schema.oneOf[0], parameterSourceOption, components, schemaResolutionCache, stack, + _.cloneDeep(seenRef)); } if (schema.allOf) { - return this.resolveAllOf(schema.allOf, parameterSourceOption, components, schemaResolutionCache, stack); + return this.resolveAllOf(schema.allOf, parameterSourceOption, components, schemaResolutionCache, stack, + _.cloneDeep(seenRef)); } if (schema.$ref && _.isFunction(schema.$ref.split)) { let refKey = schema.$ref; + + // if this reference is seen before, ignore and move on. + if (seenRef[refKey]) { + return { value: '' }; + } + // add to seen array if not encountered before. + seenRef[refKey] = stack; + // points to an existing location // .split will return [#, components, schemas, schemaName] splitRef = refKey.split('/'); @@ -148,9 +161,10 @@ module.exports = { // splitRef.slice(1) will return ['components', 'schemas', 'PaginationEnvelope', 'properties', 'page'] // not using _.get here because that fails if there's a . in the property name (Pagination.Envelope, for example) resolvedSchema = this._getEscaped(components, splitRef.slice(1)); + if (resolvedSchema) { let refResolvedSchema = this.resolveRefs(resolvedSchema, parameterSourceOption, - components, schemaResolutionCache, stack); + components, schemaResolutionCache, stack, _.cloneDeep(seenRef)); schemaResolutionCache[refKey] = refResolvedSchema; return refResolvedSchema; } @@ -182,7 +196,7 @@ module.exports = { } /* eslint-enable */ tempSchema.properties[prop] = this.resolveRefs(property, - parameterSourceOption, components, schemaResolutionCache, stack); + parameterSourceOption, components, schemaResolutionCache, stack, _.cloneDeep(seenRef)); } } return tempSchema; @@ -201,7 +215,7 @@ module.exports = { // without this, schemas with circular references aren't faked correctly let tempSchema = _.omit(schema, 'items'); tempSchema.items = this.resolveRefs(schema.items, parameterSourceOption, - components, schemaResolutionCache, stack); + components, schemaResolutionCache, stack, _.cloneDeep(seenRef)); return tempSchema; } else if (!schema.hasOwnProperty('default')) { diff --git a/lib/schemaUtils.js b/lib/schemaUtils.js index b9aa25c2c..d303a3fe2 100644 --- a/lib/schemaUtils.js +++ b/lib/schemaUtils.js @@ -589,10 +589,11 @@ module.exports = { * resolve references while generating params. * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. * @param {object} schemaCache - object storing schemaFaker and schmeResolution caches + * @param {object} variableStore - array for storing collection variables * @returns {*} Postman itemGroup or request * @no-unit-test */ - convertChildToItemGroup: function (openapi, child, components, options, schemaCache) { + convertChildToItemGroup: function (openapi, child, components, options, schemaCache, variableStore) { options = _.merge({}, defaultOptions, options); var resource = child, @@ -620,14 +621,14 @@ module.exports = { resourceSubChild = resource.children[subChild]; resourceSubChild.name = resource.name + '/' + resourceSubChild.name; - return this.convertChildToItemGroup(openapi, resourceSubChild, components, options, schemaCache); + return this.convertChildToItemGroup(openapi, resourceSubChild, components, options, schemaCache, variableStore); } /* eslint-enable */ // recurse over child leaf nodes // and add as children to this folder for (i = 0, requestCount = resource.requests.length; i < requestCount; i++) { itemGroup.items.add( - this.convertRequestToItem(openapi, resource.requests[i], components, options, schemaCache) + this.convertRequestToItem(openapi, resource.requests[i], components, options, schemaCache, variableStore) ); } @@ -637,7 +638,8 @@ module.exports = { for (subChild in resource.children) { if (resource.children.hasOwnProperty(subChild) && resource.children[subChild].requestCount > 0) { itemGroup.items.add( - this.convertChildToItemGroup(openapi, resource.children[subChild], components, options, schemaCache) + this.convertChildToItemGroup(openapi, resource.children[subChild], components, options, schemaCache, + variableStore) ); } } @@ -648,14 +650,15 @@ module.exports = { // 2. it has only 1 direct request of its own if (resource.requests.length === 1) { - return this.convertRequestToItem(openapi, resource.requests[0], components, options, schemaCache); + return this.convertRequestToItem(openapi, resource.requests[0], components, options, schemaCache, variableStore); } // 3. it's a folder that has no child request // but one request somewhere in its child folders for (subChild in resource.children) { if (resource.children.hasOwnProperty(subChild) && resource.children[subChild].requestCount === 1) { - return this.convertChildToItemGroup(openapi, resource.children[subChild], components, options, schemaCache); + return this.convertChildToItemGroup(openapi, resource.children[subChild], components, options, schemaCache, + variableStore); } } }, @@ -1497,6 +1500,67 @@ module.exports = { return refObj; }, + /** Finds all the possible path variables in a given path string + * @param {string} path Path string : /pets/{petId} + * @returns {array} Array of path variables. + */ + findPathVariablesFromPath: function (path) { + + // /{{path}}/{{file}}.{{format}}/{{hello}} return [ '{{path}}', '{{hello}}' ] + // https://regex101.com/r/XGL4Gh/1 + return path.match(/(\/\{\{[^\/\{\}]+\}\})(?=\/|$)/g); + }, + + /** Finds all the possible collection variables in a given path string + * @param {string} path Path string : /pets/{petId} + * @returns {array} Array of collection variables. + */ + findCollectionVariablesFromPath: function (path) { + + // /:path/{{file}}.{{format}}/:hello => only {{file}} and {{format}} will match + // https://regex101.com/r/XGL4Gh/2 + return path.match(/(\{\{[^\/\{\}]+\}\})/g); + }, + + /** Separates outs collection and path variables from the reqUrl + * + * @param {string} reqUrl Request Url + * @param {Array} pathVars Path variables + * + * @returns {Object} reqUrl, updated path Variables array and collection Variables. + */ + sanitizeUrlPathParams: function (reqUrl, pathVars) { + var matches, + collectionVars = []; + + // converts all the of the following: + // /{{path}}/{{file}}.{{format}}/{{hello}} => /:path/{{file}}.{{format}}/:hello + matches = this.findPathVariablesFromPath(reqUrl); + if (matches) { + matches.forEach((match) => { + const replaceWith = match.replace(/{{/g, ':').replace(/}}/g, ''); + reqUrl = reqUrl.replace(match, replaceWith); + }); + } + + // Separates pathVars array and collectionVars. + matches = this.findCollectionVariablesFromPath(reqUrl); + if (matches) { + matches.forEach((match) => { + const collVar = match.replace(/{{/g, '').replace(/}}/g, ''); + + pathVars = pathVars.filter((item) => { + if (item.name === collVar) { + collectionVars.push(item); + } + return !(item.name === collVar); + }); + }); + } + + return { reqUrl, pathVars, collectionVars }; + }, + /** * function to convert an openapi path item to postman item * @param {*} openapi openapi object with root properties @@ -1505,10 +1569,11 @@ module.exports = { * resolve references while generating params. * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. * @param {object} schemaCache - object storing schemaFaker and schmeResolution caches + * @param {array} variableStore - array * @returns {Object} postman request Item * @no-unit-test */ - convertRequestToItem: function(openapi, operationItem, components, options, schemaCache) { + convertRequestToItem: function(openapi, operationItem, components, options, schemaCache, variableStore) { options = _.merge({}, defaultOptions, options); var reqName, pathVariables = openapi.baseUrlVariables, @@ -1528,11 +1593,34 @@ module.exports = { swagResponse, localServers = _.get(operationItem, 'properties.servers'), exampleRequestBody, + sanitizeResult, globalServers = _.get(operationItem, 'servers'); // handling path templating in request url if any - reqUrl = reqUrl.replace(/{/g, ':').replace(/}/g, ''); - + // convert all {anything} to {{anything}} + reqUrl = this.fixPathVariablesInUrl(reqUrl); + + // convert all /{{one}}/{{two}} to /:one/:two + // Doesn't touch /{{file}}.{{format}} + sanitizeResult = this.sanitizeUrlPathParams(reqUrl, reqParams.path); + + // Updated reqUrl + reqUrl = sanitizeResult.reqUrl; + + // Updated reqParams.path + reqParams.path = sanitizeResult.pathVars; + + // Add collection variables to the variableStore. + sanitizeResult.collectionVars.forEach((element) => { + if (!variableStore[element.name]) { + variableStore[element.name] = { + id: element.name, + value: element.default || '', + description: element.description, + type: 'collection' + }; + } + }); // accounting for the overriding of the root level and path level servers object if present at the operation level if (Array.isArray(localServers) && localServers.length) { diff --git a/lib/schemapack.js b/lib/schemapack.js index 5ada66160..9163ef7c2 100644 --- a/lib/schemapack.js +++ b/lib/schemapack.js @@ -37,7 +37,9 @@ const COLLECTION_NAME = 'Converted from OpenAPI', generateCollection = function (specWrapper, generatedStore, components, options, schemaCache) { var folderTree = specWrapper.tree, // this is the trie we generate (as a scaffold to the collection) openapi = specWrapper.spec, // this is the JSON-version of the openAPI spec - child; + child, + key, + variableStore = {}; for (child in folderTree.root.children) { // A Postman request or folder is added if atleast one request is present in that sub-child's tree @@ -45,10 +47,18 @@ const COLLECTION_NAME = 'Converted from OpenAPI', if (folderTree.root.children.hasOwnProperty(child) && folderTree.root.children[child].requestCount > 0) { generatedStore.collection.items.add( schemaUtils.convertChildToItemGroup(openapi, folderTree.root.children[child], - components, options, schemaCache) + components, options, schemaCache, variableStore) ); } } + for (key in variableStore) { + // variableStore contains all the kinds of variable created. + // Add only the variables with type 'collection' to generatedStore.collection.variables + if (variableStore[key].type === 'collection') { + const collectionVar = new sdk.Variable(variableStore[key]); + generatedStore.collection.variables.add(collectionVar); + } + } }; diff --git a/package-lock.json b/package-lock.json index 29ed3bd61..7434b98eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "openapi-to-postmanv2", - "version": "1.1.11", + "version": "1.1.12", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -188,7 +188,7 @@ "charset": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/charset/-/charset-1.0.1.tgz", - "integrity": "sha1-jVlUbDVb5hBJqPqRZHR3kzGYUr0=" + "integrity": "sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==" }, "check-error": { "version": "1.0.2", @@ -230,9 +230,9 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "commander": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz", - "integrity": "sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM=" + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "comment-parser": { "version": "0.4.2", @@ -345,9 +345,9 @@ } }, "dom-serializer": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.1.tgz", - "integrity": "sha512-sK3ujri04WyjwQXVoK4PU3y8ula1stq10GJZpqHIUgoGZdsGzAGu65BnU3d08aTVSvO7mGPZUc0wTEDL+qGE0Q==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", "requires": { "domelementtype": "^2.0.1", "entities": "^2.0.0" @@ -356,24 +356,24 @@ "domelementtype": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", - "integrity": "sha1-H4vf6R9aeAYydOgDtL3O326U+U0=" + "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==" }, "entities": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", - "integrity": "sha1-aNYITKsbB5dnVA2A5Wo5tCPkq/Q=" + "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==" } } }, "domelementtype": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha1-0EjESzew0Qp/Kj1f7j9DM9eQSB8=" + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" }, "domhandler": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha1-iAUJfpM9ZehVRvcm1g9euItE+AM=", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", "requires": { "domelementtype": "1" } @@ -381,7 +381,7 @@ "domutils": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha1-Vuo0HoNOBuZ0ivehyyXaZ+qfjCo=", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", "requires": { "dom-serializer": "0", "domelementtype": "1" @@ -430,7 +430,7 @@ "entities": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha1-vfpzUplmTfr9NFKe1PhSKidf6lY=" + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" }, "escape-html": { "version": "1.0.3", @@ -814,7 +814,7 @@ "htmlparser2": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha1-vWedw/WYl7ajS7EHSchVu1OpOS8=", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", "requires": { "domelementtype": "^1.3.1", "domhandler": "^2.3.0", @@ -1109,7 +1109,7 @@ "lodash.mergewith": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha1-YXEh+JrFX1kEfHrsHM1mVMZZD1U=" + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" }, "lru-cache": { "version": "4.1.5", @@ -1132,7 +1132,7 @@ "marked": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", - "integrity": "sha1-tkIB8FHScbHtwQoE0a6bdLuOXA4=" + "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==" }, "mem": { "version": "4.3.0", @@ -1145,9 +1145,9 @@ } }, "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha1-plBX6ZjbCQ9zKmj2wnbTh9QSbDI=" + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz", + "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==" }, "mime-format": { "version": "2.0.0", @@ -1158,11 +1158,11 @@ } }, "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha1-tvjQs+lR77d97eyhlM/20W9nb4E=", + "version": "2.1.25", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.25.tgz", + "integrity": "sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg==", "requires": { - "mime-db": "1.40.0" + "mime-db": "1.42.0" } }, "mimic-fn": { @@ -1288,11 +1288,6 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, - "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" - }, "node-fetch-h2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", @@ -1550,9 +1545,9 @@ "dev": true }, "postcss": { - "version": "7.0.17", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", - "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.27.tgz", + "integrity": "sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==", "requires": { "chalk": "^2.4.2", "source-map": "^0.6.1", @@ -1562,7 +1557,7 @@ "supports-color": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha1-B2Srxpxj1ayELdSGfo0CXogN+PM=", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", "requires": { "has-flag": "^3.0.0" } @@ -1570,9 +1565,9 @@ } }, "postman-collection": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-3.5.1.tgz", - "integrity": "sha1-Kfp59mYSqJ6+EUwFU0rk/Kh3VeQ=", + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-3.5.5.tgz", + "integrity": "sha512-W0w0wqLlMSvFSY0LYsoNKpaFcjeg+MeNOR1XK4VyX8XFDt3uAhwCe88dS23Ee/ZG7K8T83fJU8lqVk7fjOuAUA==", "requires": { "escape-html": "1.0.3", "faker": "4.1.0", @@ -1583,17 +1578,17 @@ "lodash": "4.17.15", "marked": "0.7.0", "mime-format": "2.0.0", - "mime-types": "2.1.24", - "postman-url-encoder": "1.0.2", + "mime-types": "2.1.25", + "postman-url-encoder": "1.0.3", "sanitize-html": "1.20.1", "semver": "6.3.0", - "uuid": "3.3.2" + "uuid": "3.3.3" }, "dependencies": { "iconv-lite": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.0.tgz", - "integrity": "sha1-Wc3eCiopfMKusMZEWhle6J8SdVA=", + "integrity": "sha512-NnEhI9hIEKHOzJ4f697DMz9IQEXr/MMJ5w64vN2/4Ai+wRnvV7SBrL0KLoRlwaKVghOc7LQ5YkPLuX146b6Ydw==", "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -1601,14 +1596,14 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha1-tEf2ZwoEVbv+7dETku/zMOoJdUg=" + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" } } }, "postman-url-encoder": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/postman-url-encoder/-/postman-url-encoder-1.0.2.tgz", - "integrity": "sha1-hy3zy1+To1kJJ5IwhhYfJDboPl4=" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/postman-url-encoder/-/postman-url-encoder-1.0.3.tgz", + "integrity": "sha512-bkLjnntRHuPBQVOyGXrlrV1AWGNoZjkAI9C1pbATGzw5nLy4pOSDu5KVUsK20u6hhriFFXKUIblp0WqS3iMygw==" }, "prelude-ls": { "version": "1.1.2", @@ -1655,9 +1650,9 @@ "dev": true }, "readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha1-pRwmdUZY4KPCHb9ZFjvUW6b0R/w=", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -1748,7 +1743,8 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, "safe-json-stringify": { "version": "1.2.0", @@ -1772,7 +1768,7 @@ "sanitize-html": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.20.1.tgz", - "integrity": "sha1-9u/99V3TmIBxcSFaYr/CGBG6z4U=", + "integrity": "sha512-txnH8TQjaQvg2Q0HY06G6CDJLVYCpbnxrdO0WN8gjCKaU5J0KbyGYhZxx5QJg3WLZ1lB7XU9kDkfrCXUozqptA==", "requires": { "chalk": "^2.4.1", "htmlparser2": "^3.10.0", @@ -1789,7 +1785,7 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha1-7gpkyK9ejO6mdoexM3YeG+y9HT0=" + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" }, "set-blocking": { "version": "2.0.0", @@ -1881,11 +1877,18 @@ } }, "string_decoder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", - "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "requires": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + } } }, "strip-ansi": { @@ -2056,9 +2059,9 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE=" + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" }, "which": { "version": "1.3.1", @@ -2138,7 +2141,7 @@ "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha1-u3J3n1+kZRhrH0OPZ0+jR/2121Q=" + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { "version": "4.0.0", diff --git a/package.json b/package.json index 983cddf3b..f90639806 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openapi-to-postmanv2", - "version": "1.1.11", + "version": "1.1.12", "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", @@ -118,11 +118,11 @@ "dependencies": { "ajv": "6.10.2", "async": "3.1.0", - "commander": "2.3.0", + "commander": "2.20.3", "js-yaml": "3.13.1", "lodash": "4.17.13", "oas-resolver": "2.2.5", - "postman-collection": "3.5.1" + "postman-collection": "3.5.5" }, "author": "Postman Labs ", "license": "Apache-2.0", diff --git a/test/data/valid_openapi/issue#133.json b/test/data/valid_openapi/issue#133.json new file mode 100644 index 000000000..c8a403d4d --- /dev/null +++ b/test/data/valid_openapi/issue#133.json @@ -0,0 +1,215 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Path Variable issues", + "license": { + "name": "MIT" + } + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/send-sms.{format}": { + "get": { + "summary": "List all pets", + "operationId": "listPets", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "format", + "in": "path", + "description": "description", + "required": true, + "schema": { + "type": "string", + "pattern": "json|xml", + "example": "json" + } + } + ] + } + }, + "/some/{path}": { + "get": { + "summary": "List all pets", + "operationId": "listPets", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "path", + "in": "path", + "description": "description", + "required": true, + "schema": { + "type": "string", + "pattern": "json|xml", + "example": "json" + } + } + ] + } + }, + "/new/{path}.{new-path-variable}": { + "get": { + "summary": "List all pets", + "operationId": "listPets", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "path", + "in": "path", + "description": "description", + "required": true, + "schema": { + "type": "string", + "pattern": "json|xml", + "example": "json" + } + }, + { + "name": "new-path-variable", + "in": "path", + "description": "description", + "required": true, + "schema": { + "type": "string", + "pattern": "json|xml", + "example": "json" + } + } + ] + } + }, + "/next/{path}/{new-path-variable}": { + "get": { + "summary": "List all pets", + "operationId": "listPets", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "path", + "in": "path", + "description": "description", + "required": true, + "schema": { + "type": "string", + "pattern": "json|xml", + "example": "json" + } + }, + { + "name": "new-path-variable", + "in": "path", + "description": "description", + "required": true, + "schema": { + "type": "string", + "pattern": "json|xml", + "example": "json" + } + } + ] + } + }, + "/anotherpath/{path}/{new-path-variable}.{onemore}": { + "get": { + "summary": "List all pets", + "operationId": "listPets", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "path", + "in": "path", + "description": "description", + "required": true, + "schema": { + "type": "string", + "pattern": "json|xml", + "example": "json" + } + }, + { + "name": "new-path-variable", + "in": "path", + "description": "description", + "required": true, + "schema": { + "type": "string", + "pattern": "json|xml", + "example": "json" + } + }, + { + "name": "onemore", + "in": "path", + "description": "description", + "required": true, + "schema": { + "type": "string", + "pattern": "json|xml", + "example": "json" + } + } + ] + } + }, + "/{path}({new-path-variable}={onemore})": { + "get": { + "summary": "List all pets", + "operationId": "listPets", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "path", + "in": "path", + "description": "description", + "required": true, + "schema": { + "type": "string", + "pattern": "json|xml", + "example": "json" + } + }, + { + "name": "new-path-variable", + "in": "path", + "description": "description", + "required": true, + "schema": { + "type": "string", + "pattern": "json|xml", + "example": "json" + } + }, + { + "name": "onemore", + "in": "path", + "description": "description", + "required": true, + "schema": { + "type": "string", + "pattern": "json|xml", + "example": "json" + } + } + ] + } + } + } +} diff --git a/test/data/valid_openapi/unique_items_schema.json b/test/data/valid_openapi/unique_items_schema.json new file mode 100644 index 000000000..0e2360ee3 --- /dev/null +++ b/test/data/valid_openapi/unique_items_schema.json @@ -0,0 +1,209 @@ +{ + "openapi": "3.0.0", + "info": { + "description": "Sample API", + "version": "v1", + "title": "Sample API", + "termsOfService": "http://www.example.com/termsOfService", + "contact": { + "name": "Sample", + "url": "http://sample.com/", + "email": "sample@sample.com" + } + }, + "tags": [ + { + "name": "Folder tree" + } + ], + "paths": { + "/folder-tree": { + "get": { + "tags": [ + "Folder tree" + ], + "summary": "folder tree", + "description": "Returns a list of folders in a nested, tree structure.", + "operationId": "listFolderTree", + "parameters": [ + { + "name": "myParam", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "Auth token.", + "required": true, + "schema": { + "type": "string", + "default": "token='{{token}}'" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FolderTree" + } + } + } + } + } + } + }, + "/new-path-with-many-circular-refs": { + "get": { + "tags": [ + "circular reference" + ], + "summary": "circular reference", + "description": "circular reference", + "operationId": "circularReference", + "parameters": [], + "responses": { + "200": { + "description": "Circular Reference", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/circularReferences" + } + } + } + } + } + } + }, + "/new-passth-with-many-circular-refs": { + "get": { + "tags": [ + "circular reference" + ], + "summary": "circular reference 2", + "description": "circular reference", + "operationId": "circularReference", + "parameters": [], + "responses": { + "200": { + "description": "Circular Reference", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/b" + } + } + } + } + } + } + } + }, + "servers": [ + { + "url": "https://{host}/sample/api", + "variables": { + "host": { + "default": "unknown" + } + } + } + ], + "components": { + "schemas": { + "circularReferences": { + "type": "object", + "properties": { + "nextReference": { + "type": "array", + "items": { + "$ref": "#/components/schemas/circular" + } + } + }, + "xml": { + "name": "circularReferences" + } + }, + "circular": { + "type": "object", + "properties": { + "firstCircular": { + "$ref": "#/components/schemas/circular" + }, + "nextCircular": { + "$ref": "#/components/schemas/circularReferences" + }, + "anotherCircular": { + "$ref": "#/components/schemas/circularReferences" + } + }, + "xml": { + "name": "circular" + } + }, + "FolderTree": { + "type": "object", + "properties": { + "foobar": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "children": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/components/schemas/FolderTree" + } + } + } + }, + "b": { + "type": "object", + "properties": { + "b": { + "properties": { + "c": { + "$ref": "#/components/schemas/a" + } + } + }, + "a": { + "$ref": "#/components/schemas/a" + } + }, + "xml": { + "name": "circular" + } + }, + "a": { + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/test/unit/base.test.js b/test/unit/base.test.js index 445eb3e47..6efc36a6d 100644 --- a/test/unit/base.test.js +++ b/test/unit/base.test.js @@ -11,6 +11,8 @@ describe('CONVERT FUNCTION TESTS ', function() { var testSpec = path.join(__dirname, VALID_OPENAPI_PATH + '/test.json'), testSpec1 = path.join(__dirname, VALID_OPENAPI_PATH + '/test1.json'), + issue133 = path.join(__dirname, VALID_OPENAPI_PATH + '/issue#133.json'), + unique_items_schema = path.join(__dirname, VALID_OPENAPI_PATH + '/unique_items_schema.json'), serverOverRidingSpec = path.join(__dirname, VALID_OPENAPI_PATH + '/server_overriding.json'), infoHavingContactOnlySpec = path.join(__dirname, VALID_OPENAPI_PATH + '/info_having_contact_only.json'), infoHavingDescriptionOnlySpec = path.join(__dirname, VALID_OPENAPI_PATH + '/info_having_description_only.json'), @@ -43,8 +45,28 @@ describe('CONVERT FUNCTION TESTS ', function() { done(); }); }); + + it(' Fix for GITHUB#133: Should generate collection with proper Path and Collection variables', function(done) { + var openapi = fs.readFileSync(issue133, 'utf8'); + Converter.convert({ type: 'string', data: openapi }, { schemaFaker: true }, (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).to.have.property('variable'); + expect(conversionResult.output[0].data.variable).to.be.an('array'); + expect(conversionResult.output[0].data.variable[1].id).to.equal('format'); + expect(conversionResult.output[0].data.variable[2].id).to.equal('path'); + expect(conversionResult.output[0].data.variable[3].id).to.equal('new-path-variable'); + done(); + }); + }); + it('Should generate collection conforming to schema for and fail if not valid ' + - testSpec1, function(done) { + testSpec1, function(done) { Converter.convert({ type: 'file', data: testSpec1 }, { requestNameSource: 'url' }, (err, conversionResult) => { expect(err).to.be.null; expect(conversionResult.result).to.equal(true); @@ -56,6 +78,22 @@ describe('CONVERT FUNCTION TESTS ', function() { done(); }); }); + + it('Should not get stuck while resolving circular references' + + unique_items_schema, function(done) { + Converter.convert({ type: 'file', data: + unique_items_schema }, {}, (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'); + + done(); + }); + }); + it('Should generate collection with collapsing unnecessary folders ' + multipleFoldersSpec, function(done) { var openapi = fs.readFileSync(multipleFoldersSpec, 'utf8'); diff --git a/test/unit/util.test.js b/test/unit/util.test.js index d1ac0eac0..934f2217f 100644 --- a/test/unit/util.test.js +++ b/test/unit/util.test.js @@ -72,7 +72,7 @@ describe('SCHEMA UTILITY FUNCTION TESTS ', function () { done(); }); - it('should resolve circular structures', function(done) { + it('should not resolve circular references', function(done) { let schema = { '$ref': '#/components/schemas/a' }, @@ -98,10 +98,10 @@ describe('SCHEMA UTILITY FUNCTION TESTS ', function () { resolveTo = 'schema', result = SchemaUtils.safeSchemaFaker(schema, resolveTo, parameterSource, { components }), - tooManyLevelsString = result[0].c[0].c[0].c[0].c[0].c.value; + tooManyLevelsString = result[0].c.value; expect(result).to.not.equal(null); - expect(tooManyLevelsString).to.equal(''); + expect(tooManyLevelsString).to.equal(''); done(); }); @@ -2015,6 +2015,70 @@ describe('SCHEMA UTILITY FUNCTION TESTS ', function () { done(); }); }); + + describe('findPathVariablesFromPath function', function() { + it('should convert a url with scheme and path variables', function(done) { + var pathVars = SchemaUtils.findPathVariablesFromPath('/some/{{path}}'); + expect(pathVars[0]).to.equal('/{{path}}'); + + pathVars = SchemaUtils.findPathVariablesFromPath('/next/{{path}}/{{new-path-variable}}'); + expect(pathVars[0]).to.equal('/{{path}}'); + expect(pathVars[1]).to.equal('/{{new-path-variable}}'); + + pathVars = SchemaUtils.findPathVariablesFromPath('/anotherpath/{{path}}/{{new-path-variable}}.{{onemore}'); + expect(pathVars[0]).to.equal('/{{path}}'); + + pathVars = SchemaUtils.findPathVariablesFromPath('/send-sms.{{format}}'); + expect(pathVars).to.equal(null); + done(); + }); + }); + describe('findCollectionVariablesFromPath function', function() { + it('should convert a url with scheme and path variables', function(done) { + + var collVars = SchemaUtils.findCollectionVariablesFromPath('/send-sms.{{format}}'); + expect(collVars[0]).to.equal('{{format}}'); + + collVars = SchemaUtils.findCollectionVariablesFromPath('/next/:path/:new-path-variable'); + expect(collVars).to.equal(null); + + collVars = SchemaUtils.findCollectionVariablesFromPath('/anotherpath/:path/{{new-path-variable}}.{{onemore}}'); + expect(collVars[0]).to.equal('{{new-path-variable}}'); + expect(collVars[1]).to.equal('{{onemore}}'); + + done(); + }); + }); + + describe('sanitize function', function() { + it('should convert a url with scheme and path variables', function(done) { + var pathParams = [{ name: 'path', + in: 'path', + description: 'description', + required: true, + schema: { type: 'string', pattern: 'json|xml', example: 'json' } }, + { name: 'new-path-variable', + in: 'path', + description: 'description', + required: true, + schema: { type: 'string', pattern: 'json|xml', example: 'json' } }, + { name: 'onemore', + in: 'path', + description: 'description', + required: true, + schema: { type: 'string', pattern: 'json|xml', example: 'json' } }], + resultObj = SchemaUtils.sanitizeUrlPathParams('/anotherpath/{{path}}/{{new-path-variable}}.{{onemore}}', + pathParams); + + expect(resultObj).to.have.property('reqUrl'); + expect(resultObj.reqUrl).to.equal('/anotherpath/:path/{{new-path-variable}}.{{onemore}}'); + expect(resultObj).to.have.property('pathVars'); + expect(resultObj).to.have.property('collectionVars'); + + done(); + }); + }); + }); describe('Get header family function ', function() {