/** * This file contains util functions that need OAS-awareness * utils.js contains other util functions */ const async = require('async'), sdk = require('postman-collection'), schemaFaker = require('../assets/json-schema-faker.js'), parse = require('./parse.js'), deref = require('./deref.js'), _ = require('lodash'), xmlFaker = require('./xmlSchemaFaker.js'), openApiErr = require('./error.js'), ajvValidationError = require('./ajvValidationError'), utils = require('./utils.js'), defaultOptions = require('../lib/options.js').getOptions('use'), { Node, Trie } = require('./trie.js'), { validateSchema } = require('./ajvValidation'), SCHEMA_FORMATS = { DEFAULT: 'default', // used for non-request-body data and json XML: 'xml' // used for request-body XMLs }, URLENCODED = 'application/x-www-form-urlencoded', APP_JSON = 'application/json', APP_JS = 'application/javascript', TEXT_XML = 'text/xml', TEXT_PLAIN = 'text/plain', TEXT_HTML = 'text/html', FORM_DATA = 'multipart/form-data', REQUEST_TYPE = { EXAMPLE: 'EXAMPLE', ROOT: 'ROOT' }, PARAMETER_SOURCE = { REQUEST: 'REQUEST', RESPONSE: 'RESPONSE' }, HEADER_TYPE = { JSON: 'json', XML: 'xml', INVALID: 'invalid' }, PREVIEW_LANGUAGE = { JSON: 'json', XML: 'xml', TEXT: 'text', HTML: 'html' }, authMap = { basicAuth: 'basic', bearerAuth: 'bearer', digestAuth: 'digest', hawkAuth: 'hawk', oAuth1: 'oauth1', oAuth2: 'oauth2', ntlmAuth: 'ntlm', awsSigV4: 'awsv4', normal: null }, propNames = { QUERYPARAM: 'query parameter', PATHVARIABLE: 'path variable', HEADER: 'header', BODY: 'request body', RESPONSE_HEADER: 'response header', RESPONSE_BODY: 'response body' }, // Specifies types of processing Refs PROCESSING_TYPE = { VALIDATION: 'VALIDATION', CONVERSION: 'CONVERSION' }, // These are the methods supported in the PathItem schema // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#pathItemObject METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'], // These headers are to be validated explicitly // As these are not defined under usual parameters object and need special handling IMPLICIT_HEADERS = [ 'content-type', // 'content-type' is defined based on content/media-type of req/res body, 'accept', 'authorization' ], /* eslint-disable arrow-body-style */ schemaTypeToJsValidator = { 'string': (d) => typeof d === 'string', 'number': (d) => !isNaN(d), 'integer': (d) => !isNaN(d) && Number.isInteger(Number(d)), 'boolean': (d) => _.isBoolean(d) || d === 'true' || d === 'false', 'array': (d) => Array.isArray(d), 'object': (d) => typeof d === 'object' && !Array.isArray(d) }, crypto = require('crypto'); /* eslint-enable */ // See https://github.com/json-schema-faker/json-schema-faker/tree/master/docs#available-options schemaFaker.option({ requiredOnly: false, optionalsProbability: 1.0, // always add optional fields maxLength: 256, minItems: 1, // for arrays maxItems: 20, // limit on maximum number of items faked for (type: arrray) useDefaultValue: true, ignoreMissingRefs: true, avoidExampleItemsLength: true // option to avoid validating type array schema example's minItems and maxItems props. }); /** * * @param {*} input - input string that needs to be hashed * @returns {*} sha1 hash of the string */ function hash(input) { return crypto.createHash('sha1').update(input).digest('base64'); } /** * Safe wrapper for schemaFaker that resolves references and * removes things that might make schemaFaker crash * @param {*} oldSchema the schema to fake * @param {string} resolveTo The desired JSON-generation mechanism (schema: prefer using the JSONschema to generate a fake object, example: use specified examples as-is). Default: schema * @param {*} resolveFor - resolve refs for flow validation/conversion (value to be one of VALIDATION/CONVERSION) * @param {string} parameterSourceOption Specifies whether the schema being faked is from a request or response. * @param {*} components list of predefined components (with schemas) * @param {string} schemaFormat default or xml * @param {string} indentCharacter char for 1 unit of indentation * @param {object} schemaCache - object storing schemaFaker and schmeResolution caches * @param {object} stackLimit - The depth to which schema resolution will happen for nested refs * @returns {object} fakedObject */ function safeSchemaFaker(oldSchema, resolveTo, resolveFor, parameterSourceOption, components, schemaFormat, indentCharacter, schemaCache, stackLimit) { var prop, key, resolvedSchema, fakedSchema, schemaResolutionCache = _.get(schemaCache, 'schemaResolutionCache', {}), schemaFakerCache = _.get(schemaCache, 'schemaFakerCache', {}); resolvedSchema = deref.resolveRefs(oldSchema, parameterSourceOption, components, schemaResolutionCache, resolveFor, resolveTo, 0, {}, stackLimit); key = JSON.stringify(resolvedSchema); if (resolveTo === 'schema') { key = 'resolveToSchema ' + key; schemaFaker.option({ useExamplesValue: false }); } else if (resolveTo === 'example') { key = 'resolveToExample ' + key; schemaFaker.option({ useExamplesValue: true }); } if (resolveFor === PROCESSING_TYPE.VALIDATION) { schemaFaker.option({ useDefaultValue: false, avoidExampleItemsLength: false }); } key = hash(key); if (schemaFakerCache[key]) { return schemaFakerCache[key]; } if (resolvedSchema.properties) { // If any property exists with format:binary (and type: string) schemaFaker crashes // we just delete based on format=binary for (prop in resolvedSchema.properties) { if (resolvedSchema.properties.hasOwnProperty(prop)) { if (resolvedSchema.properties[prop].format === 'binary') { delete resolvedSchema.properties[prop].format; } } } } try { if (schemaFormat === SCHEMA_FORMATS.XML) { fakedSchema = xmlFaker(null, resolvedSchema, indentCharacter); schemaFakerCache[key] = fakedSchema; return fakedSchema; } // for JSON, the indentCharacter will be applied in the JSON.stringify step later on fakedSchema = schemaFaker(resolvedSchema); schemaFakerCache[key] = fakedSchema; return fakedSchema; } catch (e) { console.warn( 'Error faking a schema. Not faking this schema. Schema:', resolvedSchema, 'Error', e ); return null; } } module.exports = { safeSchemaFaker: safeSchemaFaker, /** Analyzes the spec to determine the size of the spec, * number of request that will be generated out this spec, and * number or references present in the spec. * * @param {Object} spec JSON * @return {Object} returns number of requests that will be generated, * number of refs present and size of the spec. */ analyzeSpec: function (spec) { var size, numberOfRefs = 0, specString, numberOfRequests = 0; // Stringify and add whitespaces as there would be in a normal file // To get accurate disk size specString = JSON.stringify(spec); // Size in MB size = Buffer.byteLength(specString, 'utf8') / (1024 * 1024); // No need to check for number of requests or refs if the size is greater than 8 MB // The complexity is 10. if (size < 8) { // Finds the number of requests that would be generated from this spec if (spec.paths) { Object.values(spec.paths).forEach((value) => { Object.keys(value).forEach((key) => { if (METHODS.includes(key)) { numberOfRequests++; } }); }); } // Number of times the term $ref is repeated in the spec. numberOfRefs = (specString.match(/\$ref/g) || []).length; } return { size, numberOfRefs, numberOfRequests }; }, /** Determines the complexity score and stackLimit * * @param {Object} analysis the object returned by analyzeSpec function * @param {Object} options Current options * * @returns {Object} computedOptions - contains two new options i.e. stackLimit and complexity score */ determineOptions: function (analysis, options) { let size = analysis.size, numberOfRefs = analysis.numberOfRefs, numberOfRequests = analysis.numberOfRequests; var computedOptions = _.clone(options); computedOptions.stackLimit = 10; // This is the score that is given to each spec on the basis of the // number of references present in spec and the number of requests that will be generated. // This ranges from 0-10. computedOptions.complexityScore = 0; // Anything above the size of 8MB will be considered a big spec and given the // least stack limit and the highest complexity score. if (size >= 8) { console.warn('Complexity score = 10'); computedOptions.stackLimit = 2; computedOptions.complexityScore = 10; return computedOptions; } else if (size >= 5 || numberOfRequests > 1500 || numberOfRefs > 1500) { computedOptions.stackLimit = 3; computedOptions.complexityScore = 9; return computedOptions; } else if (size >= 1 && (numberOfRequests > 1000 || numberOfRefs > 1000)) { computedOptions.stackLimit = 5; computedOptions.complexityScore = 8; return computedOptions; } else if (numberOfRefs > 500 || numberOfRequests > 500) { computedOptions.stackLimit = 6; computedOptions.complexityScore = 6; return computedOptions; } return computedOptions; }, /** * Changes the {} around scheme and path variables to :variable * @param {string} url - the url string * @returns {string} string after replacing /{pet}/ with /:pet/ */ fixPathVariablesInUrl: function (url) { // All complicated logic removed // This simply replaces all instances of {text} with {{text}} // text cannot have any of these 3 chars: /{} // {{text}} will not be converted let replacer = function (match, p1, offset, string) { if (string[offset - 1] === '{' && string[offset + match.length + 1] !== '}') { return match; } return '{' + p1 + '}'; }; return url.replace(/(\{[^\/\{\}]+\})/g, replacer); }, /** * Changes path structure that contains {var} to :var and '/' to '_' * This is done so generated collection variable is in correct format * i.e. variable '{{item/{itemId}}}' is considered separates variable in URL by collection sdk * @param {string} path - path defined in openapi spec * @returns {string} - string after replacing {itemId} with :itemId */ fixPathVariableName: function (path) { // Replaces structure like 'item/{itemId}' into 'item-itemId-Url' return path.replace(/\//g, '-').replace(/[{}]/g, '') + '-Url'; }, /** * Returns a description that's usable at the collection-level * Adds the collection description and uses any relevant contact info * @param {*} openapi The JSON representation of the OAS spec * @returns {string} description */ getCollectionDescription: function (openapi) { let description = _.get(openapi, 'info.description', ''); if (_.get(openapi, 'info.contact')) { let contact = []; if (openapi.info.contact.name) { contact.push(' Name: ' + openapi.info.contact.name); } if (openapi.info.contact.email) { contact.push(' Email: ' + openapi.info.contact.email); } if (contact.length > 0) { // why to add unnecessary lines if there is no description if (description !== '') { description += '\n\n'; } description += 'Contact Support:\n' + contact.join('\n'); } } return description; }, /** * Get the format of content type header * @param {string} cTypeHeader - the content type header string * @returns {string} type of content type header */ getHeaderFamily: function(cTypeHeader) { let mediaType = this.parseMediaType(cTypeHeader); if (mediaType.type === 'application' && (mediaType.subtype === 'json' || _.endsWith(mediaType.subtype, '+json'))) { return HEADER_TYPE.JSON; } if ((mediaType.type === 'application' || mediaType.type === 'text') && (mediaType.subtype === 'xml' || _.endsWith(mediaType.subtype, '+xml'))) { return HEADER_TYPE.XML; } return HEADER_TYPE.INVALID; }, /** * Gets the description of the parameter. * If the parameter is required, it prepends a `(Requried)` before the parameter description * If the parameter type is enum, it appends the possible enum values * @param {object} parameter - input param for which description needs to be returned * @returns {string} description of the parameters */ getParameterDescription: function(parameter) { if (!_.isObject(parameter)) { return ''; } return (parameter.required ? '(Required) ' : '') + (parameter.description || '') + (parameter.enum ? ' (This can only be one of ' + parameter.enum + ')' : ''); }, /** * Given parameter objects, it assigns example/examples of parameter object as schema example. * * @param {Object} parameter - parameter object * @returns {null} - null */ assignParameterExamples: function (parameter) { let example = _.get(parameter, 'example'), examples = _.values(_.get(parameter, 'examples')); if (example) { _.set(parameter, 'schema.example', example); } else if (examples) { let exampleToUse = _.get(examples, '[0].value'); !_.isUndefined(exampleToUse) && (_.set(parameter, 'schema.example', exampleToUse)); } }, /** * Converts the neccessary server variables to the * something that can be added to the collection * TODO: Figure out better description * @param {object} serverVariables - Object containing the server variables at the root/path-item level * @param {string} keyName - an additional key to add the serverUrl to the variable list * @param {string} serverUrl - URL from the server object * @returns {object} modified collection after the addition of the server variables */ convertToPmCollectionVariables: function(serverVariables, keyName, serverUrl = '') { var variables = []; if (serverVariables) { _.forOwn(serverVariables, (value, key) => { let description = this.getParameterDescription(value); variables.push(new sdk.Variable({ key: key, value: value.default || '', description: description })); }); } if (keyName) { variables.push(new sdk.Variable({ key: keyName, value: serverUrl, type: 'string' })); } return variables; }, /** * Parses an OAS string/object as a YAML or JSON * @param {YAML/JSON} openApiSpec - The OAS 3.x specification specified in either YAML or JSON * @returns {Object} - Contains the parsed JSON-version of the OAS spec, or an error * @no-unit-test */ parseSpec: function (openApiSpec) { var openApiObj = openApiSpec, obj, rootValidation; // If the open api specification is a string could be YAML or JSON if (typeof openApiSpec === 'string') { obj = parse.getOasObject(openApiSpec); if (obj.result) { openApiObj = obj.oasObject; } else { return obj; } } // spec is a valid JSON object at this point // Validate the root level object for semantics rootValidation = parse.validateSpec(openApiObj); if (!rootValidation.result) { return { result: false, reason: rootValidation.reason }; } // Valid openapi root object return { result: true, openapi: rootValidation.openapi }; }, /** * Returns params applied to specific operation with resolved references. Params from parent * blocks (collection/folder) are merged, so that the request has a flattened list of params needed. * OperationParams take precedence over pathParams * @param {array} operationParam operation (Postman request)-level params. * @param {array} pathParam are path parent-level params. * @param {object} components - components defined in the OAS spec. These are used to * resolve references while generating params. * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. * @returns {*} combined requestParams from operation and path params. */ getRequestParams: function(operationParam, pathParam, components, options) { options = _.merge({}, defaultOptions, options); if (!Array.isArray(operationParam)) { operationParam = []; } if (!Array.isArray(pathParam)) { pathParam = []; } pathParam.forEach((param, index, arr) => { if (param.hasOwnProperty('$ref')) { arr[index] = this.getRefObject(param.$ref, components, options); } }); operationParam.forEach((param, index, arr) => { if (param.hasOwnProperty('$ref')) { arr[index] = this.getRefObject(param.$ref, components, options); } }); if (_.isEmpty(pathParam)) { return operationParam; } else if (_.isEmpty(operationParam)) { return pathParam; } // If both path and operation params exist, // we need to de-duplicate // A param with the same name and 'in' value from operationParam // will get precedence var reqParam = operationParam.slice(); pathParam.forEach((param) => { var dupParam = operationParam.find(function(element) { return element.name === param.name && element.in === param.in && // the below two conditions because undefined === undefined returns true element.name && param.name && element.in && param.in; }); if (!dupParam) { // if there's no duplicate param in operationParam, // use the one from the common pathParam list // this ensures that operationParam is given precedence reqParam.push(param); } }); return reqParam; }, /** * Generates a Trie-like folder structure from the root path object of the OpenAPI specification. * @param {Object} spec - specification in json format * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. * @returns {Object} - The final object consists of the tree structure */ generateTrieFromPaths: function (spec, options) { options = _.merge({}, defaultOptions, options); var paths = spec.paths, // the first level of paths currentPath = '', currentPathObject = '', commonParams = '', collectionVariables = {}, operationItem, pathLevelServers = '', pathLength, currentPathRequestCount, currentNode, i, summary, path, componentsAndPaths = { components: spec.components, paths: spec.paths }, pathMethods = [], // creating a root node for the trie (serves as the root dir) trie = new Trie(new Node({ name: '/' })), // returns a list of methods supported at each pathItem // some pathItem props are not methods // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#pathItemObject getPathMethods = function(pathKeys) { var methods = []; // TODO: Show warning for incorrect schema if !pathKeys pathKeys && pathKeys.forEach(function(element) { if (METHODS.includes(element)) { methods.push(element); } }); return methods; }; for (path in paths) { if (paths.hasOwnProperty(path)) { currentPathObject = paths[path]; // discard the leading slash, if it exists if (path[0] === '/') { path = path.substring(1); } // split the path into indiv. segments for trie generation // unless path it the root endpoint currentPath = path === '' ? ['(root)'] : path.split('/').filter((pathItem) => { // remove any empty pathItems that might have cropped in // due to trailing or double '/' characters return pathItem !== ''; }); pathLength = currentPath.length; // get method names available for this path pathMethods = getPathMethods(Object.keys(currentPathObject)); // the number of requests under this node currentPathRequestCount = pathMethods.length; currentNode = trie.root; // adding children for the nodes in the trie // start at the top-level and do a DFS for (i = 0; i < pathLength; i++) { if (!currentNode.children[currentPath[i]]) { // if the currentPath doesn't already exist at this node, // add it as a folder currentNode.addChildren(currentPath[i], new Node({ name: currentPath[i], requestCount: 0, requests: [], children: {}, type: 'item-group', childCount: 0 })); // We are keeping the count children in a folder which can be a request or folder // For ex- In case of /pets/a/b, pets has 1 childCount (i.e a) currentNode.childCount += 1; } // requestCount increment for the node we just added currentNode.children[currentPath[i]].requestCount += currentPathRequestCount; currentNode = currentNode.children[currentPath[i]]; } // extracting common parameters for all the methods in the current path item if (currentPathObject.hasOwnProperty('parameters')) { commonParams = currentPathObject.parameters; } // storing common path/collection vars from the server object at the path item level if (currentPathObject.hasOwnProperty('servers')) { pathLevelServers = currentPathObject.servers; collectionVariables[this.fixPathVariableName(path)] = pathLevelServers[0]; delete currentPathObject.servers; } // add methods to node // eslint-disable-next-line no-loop-func _.each(pathMethods, (method) => { // base operationItem operationItem = currentPathObject[method] || {}; // params - these contain path/header/body params operationItem.parameters = this.getRequestParams(operationItem.parameters, commonParams, componentsAndPaths, options); summary = operationItem.summary || operationItem.description; currentNode.addMethod({ name: summary, method: method, path: path, properties: operationItem, type: 'item', servers: pathLevelServers || undefined }); currentNode.childCount += 1; }); pathLevelServers = undefined; commonParams = []; } } return { tree: trie, variables: collectionVariables // server variables that are to be converted into collection variables. }; }, /** * Adds Postman Collection Items using paths. * Folders are grouped based on trie that's generated using all paths. * * @param {object} spec - openAPI spec object * @param {object} generatedStore - the store that holds the generated collection. Modified in-place * @param {object} components - components defined in the OAS spec. These are used to * 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 * @returns {void} - generatedStore is modified in-place */ addCollectionItemsUsingPaths: function (spec, generatedStore, components, options, schemaCache) { var folderTree, folderObj, child, key, variableStore = {}; /** We need a trie because the decision of whether or not a node is a folder or request can only be made once the whole trie is generated This has a .trie and a .variables prop */ folderObj = this.generateTrieFromPaths(spec, options); folderTree = folderObj.tree; /* * these are variables identified at the collection level * they need to be added explicitly to collection variables * deeper down in the trie, variables will be added directly to folders * If the folderObj.variables have their own variables, we add * them to the collectionVars */ if (folderObj.variables) { _.forOwn(folderObj.variables, (server, key) => { // TODO: Figure out what this does this.convertToPmCollectionVariables( server.variables, // these are path variables in the server block key, // the name of the variable this.fixPathVariablesInUrl(server.url) ).forEach((element) => { generatedStore.collection.variables.add(element); }); }); } // Adds items from the trie into the collection that's in the store 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 // requestCount is a property added to each node (folder/request) while constructing the trie if (folderTree.root.children.hasOwnProperty(child) && folderTree.root.children[child].requestCount > 0) { generatedStore.collection.items.add( this.convertChildToItemGroup(spec, folderTree.root.children[child], 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); } } }, /** * Adds Postman Collection Items using tags. * Each tag from OpenAPI tags object is mapped to a collection item-group (Folder), and all operation that has * corresponding tag in operation object's tags array is included in mapped item-group. * * @param {object} spec - openAPI spec object * @param {object} generatedStore - the store that holds the generated collection. Modified in-place * @param {object} components - components defined in the OAS spec. These are used to * 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 * @returns {object} returns an object containing objects of tags and their requests */ addCollectionItemsUsingTags: function(spec, generatedStore, components, options, schemaCache) { var globalTags = spec.tags || [], paths = spec.paths || {}, pathMethods, variableStore = {}, tagFolders = {}; // adding globalTags in the tagFolder object that are defined at the root level _.forEach(globalTags, (globalTag) => { tagFolders[globalTag.name] = { description: _.get(globalTag, 'description', ''), requests: [] }; }); _.forEach(paths, (currentPathObject, path) => { var commonParams = [], collectionVariables, pathLevelServers = ''; // discard the leading slash, if it exists if (path[0] === '/') { path = path.substring(1); } // extracting common parameters for all the methods in the current path item if (currentPathObject.hasOwnProperty('parameters')) { commonParams = currentPathObject.parameters; } // storing common path/collection vars from the server object at the path item level if (currentPathObject.hasOwnProperty('servers')) { pathLevelServers = currentPathObject.servers; // add path level server object's URL as collection variable collectionVariables = this.convertToPmCollectionVariables( pathLevelServers[0].variables, // these are path variables in the server block this.fixPathVariableName(path), // the name of the variable this.fixPathVariablesInUrl(pathLevelServers[0].url) ); _.forEach(collectionVariables, (collectionVariable) => { generatedStore.collection.variables.add(collectionVariable); }); delete currentPathObject.servers; } // get available method names for this path (path item object can have keys apart from operations) pathMethods = _.filter(_.keys(currentPathObject), (key) => { return _.includes(METHODS, key); }); _.forEach(pathMethods, (pathMethod) => { var summary, operationItem = currentPathObject[pathMethod] || {}, localTags = operationItem.tags; // params - these contain path/header/body params operationItem.parameters = this.getRequestParams(operationItem.parameters, commonParams, components, options); summary = operationItem.summary || operationItem.description; // add the request which has not any tags if (_.isEmpty(localTags)) { let tempRequest = { name: summary, method: pathMethod, path: path, properties: operationItem, type: 'item', servers: pathLevelServers || undefined }; generatedStore.collection.items.add(this.convertRequestToItem( spec, tempRequest, components, options, schemaCache, variableStore)); } else { _.forEach(localTags, (localTag) => { // add undefined tag object with empty description if (!_.has(tagFolders, localTag)) { tagFolders[localTag] = { description: '', requests: [] }; } tagFolders[localTag].requests.push({ name: summary, method: pathMethod, path: path, properties: operationItem, type: 'item', servers: pathLevelServers || undefined }); }); } }); }); // Add all folders created from tags and corresponding operations // Iterate from bottom to top order to maintain tag order in spec _.forEachRight(tagFolders, (tagFolder, tagName) => { var itemGroup = new sdk.ItemGroup({ name: tagName, description: tagFolder.description }); _.forEach(tagFolder.requests, (request) => { itemGroup.items.add(this.convertRequestToItem(spec, request, components, options, schemaCache, variableStore)); }); // Add folders first (before requests) in generated collection generatedStore.collection.items.prepend(itemGroup); }); // variableStore contains all the kinds of variable created. // Add only the variables with type 'collection' to generatedStore.collection.variables _.forEach(variableStore, (variable) => { if (variable.type === 'collection') { const collectionVar = new sdk.Variable(variable); generatedStore.collection.variables.add(collectionVar); } }); }, /** * Generates an array of SDK Variables from the common and provided path vars * @param {string} type - Level at the tree root/path level. Can be method/root/param. * method: request(operation)-level, root: spec-level, param: url-level * @param {Array} providedPathVars - Array of path variables * @param {object|array} commonPathVars - Object of path variables taken from the specification * @param {object} components - components defined in the OAS spec. These are used to * 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 * @returns {Array} returns an array of sdk.Variable */ convertPathVariables: function(type, providedPathVars, commonPathVars, components, options, schemaCache) { options = _.merge({}, defaultOptions, options); var variables = providedPathVars; // converting the base uri path variables, if any // commonPathVars is an object for type = root/method // array otherwise if (type === 'root' || type === 'method') { _.forOwn(commonPathVars, (value, key) => { let description = this.getParameterDescription(value); variables.push({ key: key, value: type === 'root' ? '{{' + key + '}}' : value.default, description: description }); }); } else { _.forEach(commonPathVars, (variable) => { let fakedData, convertedPathVar; this.assignParameterExamples(variable); fakedData = options.schemaFaker ? safeSchemaFaker(variable.schema || {}, options.requestParametersResolution, PROCESSING_TYPE.CONVERSION, PARAMETER_SOURCE.REQUEST, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache, options.stackLimit) : ''; convertedPathVar = this.convertParamsWithStyle(variable, fakedData, PARAMETER_SOURCE.REQUEST, components, schemaCache, options); variables = _.concat(variables, convertedPathVar); }); } return variables; }, /** * convert childItem from OpenAPI to Postman itemGroup if requestCount(no of requests inside childitem)>1 * otherwise return postman request * @param {*} openapi object with root-level data like pathVariables baseurl * @param {*} child object is of type itemGroup or request * resolve references while generating params. * @param {object} components - components defined in the OAS spec. These are used to * 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, variableStore) { options = _.merge({}, defaultOptions, options); var resource = child, itemGroup, subChild, i, requestCount; // 3 options: // 1. folder with more than one request in its subtree // (immediate children or otherwise) if (resource.requestCount > 1) { // only return a Postman folder if this folder has>1 children in its subtree // otherwise we can end up with 10 levels of folders with 1 request in the end itemGroup = new sdk.ItemGroup({ name: utils.insertSpacesInName(resource.name) // TODO: have to add auth here (but first, auth to be put into the openapi tree) }); // If a folder has only one child which is a folder then we collapsed the child folder // with parent folder. /* eslint-disable max-depth */ if (resource.childCount === 1 && options.collapseFolders) { let subChild = Object.keys(resource.children)[0], resourceSubChild = resource.children[subChild]; resourceSubChild.name = resource.name + '/' + resourceSubChild.name; 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, variableStore) ); } // recurse over child folders // and add as child folders to this folder /* eslint-disable max-depth*/ 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, variableStore) ); } } /* eslint-enable */ return itemGroup; } // 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, 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, variableStore); } } }, /** * Gets helper object based on the root spec and the operation.security object * @param {*} openapi - the json object representing the OAS spec * @param {Array} securitySet - the security object at an operation level * @returns {object} The authHelper to use while constructing the Postman Request. This is * not directly supported in the SDK - the caller needs to determine the header/body based on the return * value * @no-unit-test */ getAuthHelper: function(openapi, securitySet) { var securityDef, helper; // return false if security set is not defined // or is an empty array // this will set the request's auth to null - which is 'inherit from parent' if (!securitySet || (Array.isArray(securitySet) && securitySet.length === 0)) { return null; } _.forEach(securitySet, (security) => { if (_.isObject(security) && _.isEmpty(security)) { helper = { type: 'noauth' }; return false; } securityDef = _.get(openapi, ['securityDefs', Object.keys(security)[0]]); if (!_.isObject(securityDef)) { return; } else if (securityDef.type === 'http') { if (_.toLower(securityDef.scheme) === 'basic') { helper = { type: 'basic', basic: [ { key: 'username', value: '' }, { key: 'password', value: '' } ] }; } else if (_.toLower(securityDef.scheme) === 'bearer') { helper = { type: 'bearer', bearer: [{ key: 'token', value: '' }] }; } else if (_.toLower(securityDef.scheme) === 'digest') { helper = { type: 'digest', digest: [ { key: 'username', value: '' }, { key: 'password', value: '' }, { key: 'realm', value: '' } ] }; } else if (_.toLower(securityDef.scheme) === 'oauth' || _.toLower(securityDef.scheme) === 'oauth1') { helper = { type: 'oauth1', oauth1: [ { key: 'consumerSecret', value: '' }, { key: 'consumerKey', value: '' }, { key: 'addParamsToHeader', value: true } ] }; } } else if (securityDef.type === 'oauth2') { helper = { type: 'oauth2' }; } else if (securityDef.type === 'apiKey') { helper = { type: 'apikey', apikey: [ { key: 'key', value: _.isString(securityDef.name) ? securityDef.name : '' }, { key: 'value', value: '' }, { key: 'in', value: _.includes(['query', 'header'], securityDef.in) ? securityDef.in : 'header' } ] }; } // stop searching for helper if valid auth scheme is found if (!_.isEmpty(helper)) { return false; } }); return helper; }, /** * Generates appropriate collection element based on parameter location * * @param {Object} param - Parameter object habing key, value and description (optional) * @param {String} location - Parameter location ("in" property of OAS defined parameter object) * @returns {Object} - SDK element */ generateSdkParam: function (param, location) { const sdkElementMap = { 'query': sdk.QueryParam, 'header': sdk.Header, 'path': sdk.Variable }; let generatedParam = { key: param.key, value: param.value }; _.has(param, 'disabled') && (generatedParam.disabled = param.disabled); // use appropriate sdk element based on location parmaeter is in for param generation if (sdkElementMap[location]) { generatedParam = new sdkElementMap[location](generatedParam); } param.description && (generatedParam.description = param.description); return generatedParam; }, /** * Generates Auth helper for response, params (query, headers) in helper object is added in * request (originalRequest) part of example. * * @param {*} requestAuthHelper - Auth helper object of corresponding request * @returns {Object} - Response Auth helper object containing params to be added */ getResponseAuthHelper: function (requestAuthHelper) { var responseAuthHelper = { query: [], header: [] }, getValueFromHelper = function (authParams, keyName) { return _.find(authParams, { key: keyName }).value; }, paramLocation, description; if (!_.isObject(requestAuthHelper)) { return responseAuthHelper; } description = 'Added as a part of security scheme: ' + requestAuthHelper.type; switch (requestAuthHelper.type) { case 'apikey': // find location of parameter from auth helper paramLocation = getValueFromHelper(requestAuthHelper.apikey, 'in'); responseAuthHelper[paramLocation].push({ key: getValueFromHelper(requestAuthHelper.apikey, 'key'), value: '', description }); break; case 'basic': responseAuthHelper.header.push({ key: 'Authorization', value: 'Basic ', description }); break; case 'bearer': responseAuthHelper.header.push({ key: 'Authorization', value: 'Bearer ', description }); break; case 'digest': responseAuthHelper.header.push({ key: 'Authorization', value: 'Digest ', description }); break; case 'oauth1': responseAuthHelper.header.push({ key: 'Authorization', value: 'OAuth ', description }); break; case 'oauth2': responseAuthHelper.header.push({ key: 'Authorization', value: '', description }); break; default: break; } return responseAuthHelper; }, /** * Converts a 'content' object into Postman response body. Any content-type header determined * from the body is returned as well * @param {*} contentObj response content - this is the content property of the response body * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responseObject * @param {object} components - components defined in the OAS spec. These are used to * 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 * @return {object} responseBody, contentType header needed */ convertToPmResponseBody: function(contentObj, components, options, schemaCache) { options = _.merge({}, defaultOptions, options); var responseBody, cTypeHeader, hasComputedType, cTypes; if (!contentObj) { return { contentTypeHeader: null, responseBody: '' }; } let headers = Object.keys(contentObj); for (let i = 0; i < headers.length; i++) { let headerFamily = this.getHeaderFamily(headers[i]); if (headerFamily !== HEADER_TYPE.INVALID) { cTypeHeader = headers[i]; hasComputedType = true; if (headerFamily === HEADER_TYPE.JSON) { break; } } } // if no JSON or XML, take whatever we have if (!hasComputedType) { cTypes = Object.keys(contentObj); if (cTypes.length > 0) { cTypeHeader = cTypes[0]; hasComputedType = true; } else { // just an empty object - can't convert anything return { contentTypeHeader: null, responseBody: '' }; } } responseBody = this.convertToPmBodyData(contentObj[cTypeHeader], REQUEST_TYPE.EXAMPLE, cTypeHeader, PARAMETER_SOURCE.RESPONSE, options.indentCharacter, components, options, schemaCache); if (this.getHeaderFamily(cTypeHeader) === HEADER_TYPE.JSON) { responseBody = JSON.stringify(responseBody, null, options.indentCharacter); } else if (typeof responseBody !== 'string') { // since the collection v2 schema only supports body being a string responseBody = ''; } return { contentTypeHeader: cTypeHeader, responseBody: responseBody }; }, /** * Create parameters specific for a request * @param {*} localParams parameters array * @returns {Object} with three arrays of query, header and path as keys. * @no-unit-test */ getParametersForPathItem: function(localParams) { var tempParam, params = { query: [], header: [], path: [] }; _.forEach(localParams, (param) => { tempParam = param; if (tempParam.in === 'query') { params.query.push(tempParam); } else if (tempParam.in === 'header') { params.header.push(tempParam); } else if (tempParam.in === 'path') { params.path.push(tempParam); } }); return params; }, /** * returns first example in the input map * @param {*} exampleObj map[string, exampleObject] * @param {object} components - components defined in the OAS spec. These are used to * resolve references while generating params. * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. * @returns {*} first example in the input map type */ getExampleData: function(exampleObj, components, options) { options = _.merge({}, defaultOptions, options); var example, exampleKey; if (typeof exampleObj !== 'object') { return ''; } exampleKey = Object.keys(exampleObj)[0]; example = exampleObj[exampleKey]; // return example value if present else example is returned if (_.has(example, '$ref')) { example = this.getRefObject(example.$ref, components, options); } if (_.has(example, 'value')) { example = example.value; } return example; }, /** * converts one of the eamples or schema in Media Type object to postman data * @param {*} bodyObj is MediaTypeObject * @param {*} requestType - Specifies whether the request body is of example request or root request * @param {*} contentType - content type header * @param {string} parameterSourceOption tells that the schema object is of request or response * @param {string} indentCharacter is needed for XML/JSON bodies only * @param {object} components - components defined in the OAS spec. These are used to * 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 * @returns {*} postman body data */ // TODO: We also need to accept the content type // and generate the body accordingly // right now, even if the content-type was XML, we'll generate // a JSON example/schema convertToPmBodyData: function(bodyObj, requestType, contentType, parameterSourceOption, indentCharacter, components, options, schemaCache) { options = _.merge({}, defaultOptions, options); var bodyData = '', schemaFormat = SCHEMA_FORMATS.DEFAULT, schemaType, resolveTo = this.resolveToExampleOrSchema(requestType, options.requestParametersResolution, options.exampleParametersResolution); if (_.isEmpty(bodyObj)) { return bodyData; } if (bodyObj.example && (resolveTo === 'example' || !bodyObj.schema)) { if (bodyObj.example.hasOwnProperty('$ref')) { bodyObj.example = this.getRefObject(bodyObj.example.$ref, components, options); if (this.getHeaderFamily(contentType) === HEADER_TYPE.JSON) { // try to parse the example as JSON. OK if this fails // eslint-disable-next-line max-depth try { bodyObj.example = JSON.parse(bodyObj.example); } // eslint-disable-next-line no-empty catch (e) {} } } bodyData = bodyObj.example; // return example value if present else example is returned if (bodyData.hasOwnProperty('value')) { bodyData = bodyData.value; } } else if (!_.isEmpty(bodyObj.examples) && (resolveTo === 'example' || !bodyObj.schema)) { // take one of the examples as the body and not all bodyData = this.getExampleData(bodyObj.examples, components, options); } else if (bodyObj.schema) { if (bodyObj.schema.hasOwnProperty('$ref')) { bodyObj.schema = this.getRefObject(bodyObj.schema.$ref, components, options); } if (options.schemaFaker) { if (this.getHeaderFamily(contentType) === HEADER_TYPE.XML) { schemaFormat = SCHEMA_FORMATS.XML; } // Do not fake schemas if the complexity score is 10 if (options.complexityScore === 10) { schemaType = bodyObj.schema.type; if (schemaType === 'object') { return { value: '' }; } if (schemaType === 'array') { return [ '' ]; } return ''; } // Do not fake the bodyData if the complexity is 10. bodyData = safeSchemaFaker(bodyObj.schema || {}, resolveTo, PROCESSING_TYPE.CONVERSION, parameterSourceOption, components, schemaFormat, indentCharacter, schemaCache, options.stackLimit); } else { // do not fake if the option is false bodyData = ''; } } return bodyData; }, /** * returns whether to resolve to example or schema * @param {string} requestType - Specifies whether the request body is of example request or root request * @param {string} requestParametersResolution - the option value of requestParametersResolution * @param {string} exampleParametersResolution - the option value of exampleParametersResolution * @returns {string} Whether to resolve to example or schema */ resolveToExampleOrSchema(requestType, requestParametersResolution, exampleParametersResolution) { if (requestType === REQUEST_TYPE.ROOT) { if (requestParametersResolution === 'example') { return 'example'; } else if (requestParametersResolution === 'schema') { return 'schema'; } } if (requestType === REQUEST_TYPE.EXAMPLE) { if (exampleParametersResolution === 'example') { return 'example'; } else if (exampleParametersResolution === 'schema') { return 'schema'; } } return 'schema'; }, /** * convert param with in='query' to string considering style and type * @param {*} param with in='query' * @param {*} requestType Specifies whether the request body is of example request or root request * @param {object} components - components defined in the OAS spec. These are used to * 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 * @returns {array} converted queryparam */ convertToPmQueryParameters: function(param, requestType, components, options, schemaCache) { options = _.merge({}, defaultOptions, options); var pmParams = [], paramValue, resolveTo = this.resolveToExampleOrSchema(requestType, options.requestParametersResolution, options.exampleParametersResolution); if (!param) { return []; } // check for existence of schema if (param.hasOwnProperty('schema')) { this.assignParameterExamples(param); // fake data generated paramValue = options.schemaFaker ? safeSchemaFaker(param.schema, resolveTo, PROCESSING_TYPE.CONVERSION, PARAMETER_SOURCE.REQUEST, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache, options.stackLimit) : ''; // paramType = param.schema.type; if (typeof paramValue === 'number' || typeof paramValue === 'boolean') { // the SDK will keep the number-ness, // which will be rejected by the collection v2 schema // converting to string to prevent issues like // https://github.com/postmanlabs/postman-app-support/issues/6500 paramValue = paramValue.toString(); } return this.convertParamsWithStyle(param, paramValue, PARAMETER_SOURCE.REQUEST, components, schemaCache, options); } let description = this.getParameterDescription(param); // since no schema present add the parameter with no value pmParams.push({ key: param.name, value: '', description: description }); return pmParams; }, /** * Recursicely extracts key-value pair from deep objects. * * @param {*} deepObject - Deep object * @param {*} objectKey - key associated with deep object * @returns {Array} array of param key-value pairs */ extractDeepObjectParams: function (deepObject, objectKey) { let extractedParams = []; // console.log(deepObject); _.forEach(deepObject, (value, key) => { if (typeof value === 'object') { extractedParams = _.concat(extractedParams, this.extractDeepObjectParams(value, objectKey + '[' + key + ']')); } else { extractedParams.push({ key: objectKey + '[' + key + ']', value }); } }); return extractedParams; }, /** * Returns an array of parameters * Handles array/object/string param types * @param {*} param - the param object, as defined in * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject * @param {any} paramValue - the value to use (from schema or example) for the given param. * This will be exploded/parsed according to the param type * @param {*} parameterSource — Specifies whether the schema being faked is from a request or response. * @param {object} components - components defined in the OAS spec. These are used to * resolve references while generating params. * @param {object} schemaCache - object storing schemaFaker and schmeResolution caches * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. * @returns {array} parameters. One param with type=array might lead to multiple params * in the return value * The styles are documented at * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#style-values */ convertParamsWithStyle: function(param, paramValue, parameterSource, components, schemaCache, options) { var paramName = _.get(param, 'name'), pmParams = [], serialisedValue = '', description = this.getParameterDescription(param), disabled = false; // for invalid param object return null if (!_.isObject(param)) { return null; } let { style, explode, startValue, propSeparator, keyValueSeparator, isExplodable } = this.getParamSerialisationInfo(param, parameterSource, components, schemaCache); if (options && options.disableOptionalParameters) { disabled = !param.required; } // decide explodable params, starting value and separators between key-value and properties for serialisation switch (style) { case 'form': if (explode && _.isObject(paramValue)) { _.forEach(paramValue, (value, key) => { pmParams.push(this.generateSdkParam({ key: _.isArray(paramValue) ? paramName : key, value: (value === undefined ? '' : value), description, disabled }, _.get(param, 'in'))); }); return pmParams; } // handle free-form parameter correctly if (explode && (_.get(param, 'schema.type') === 'object') && _.isEmpty(_.get(param, 'schema.properties'))) { return pmParams; } break; case 'deepObject': if (_.isObject(paramValue)) { let extractedParams = this.extractDeepObjectParams(paramValue, paramName); _.forEach(extractedParams, (extractedParam) => { pmParams.push(this.generateSdkParam({ key: extractedParam.key, value: extractedParam.value || '', description, disabled }, _.get(param, 'in'))); }); } return pmParams; default: break; } // for array and object, serialize value if (_.isObject(paramValue)) { _.forEach(paramValue, (value, key) => { // add property separator for all index/keys except first !_.isEmpty(serialisedValue) && (serialisedValue += propSeparator); // append key for param that can be exploded isExplodable && (serialisedValue += (key + keyValueSeparator)); serialisedValue += (value === undefined ? '' : value); }); } // for non-object and non-empty value append value as is to string else if (!_.isNil(paramValue)) { serialisedValue += paramValue; } // prepend starting value to serialised value (valid for empty value also) serialisedValue = startValue + serialisedValue; pmParams.push(this.generateSdkParam({ key: paramName, value: serialisedValue, description, disabled }, _.get(param, 'in'))); return pmParams; }, /** * converts params with in='header' to a Postman header object * @param {*} header param with in='header' * @param {*} requestType Specifies whether the request body is of example request or root request * @param {*} parameterSource — Specifies whether the schema being faked is from a request or response. * @param {object} components - components defined in the OAS spec. These are used to * 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 * @returns {Object} instance of a Postman SDK Header */ convertToPmHeader: function(header, requestType, parameterSource, components, options, schemaCache) { options = _.merge({}, defaultOptions, options); var fakeData, convertedHeader, reqHeader, resolveTo = this.resolveToExampleOrSchema(requestType, options.requestParametersResolution, options.exampleParametersResolution); if (header.hasOwnProperty('schema')) { if (!options.schemaFaker) { fakeData = ''; } else { this.assignParameterExamples(header); fakeData = safeSchemaFaker(header.schema || {}, resolveTo, PROCESSING_TYPE.CONVERSION, parameterSource, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache, options.stackLimit); } } else { fakeData = ''; } convertedHeader = _.get(this.convertParamsWithStyle(header, fakeData, parameterSource, components, schemaCache, options), '[0]'); reqHeader = new sdk.Header(convertedHeader); reqHeader.description = this.getParameterDescription(header); return reqHeader; }, /** * converts operation item requestBody to a Postman request body * @param {*} requestBody in operationItem * @param {*} requestType - Specifies whether the request body is of example request or root request * @param {object} components - components defined in the OAS spec. These are used to * 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 * @returns {Object} - Postman requestBody and Content-Type Header */ convertToPmBody: function(requestBody, requestType, components, options, schemaCache) { options = _.merge({}, defaultOptions, options); var contentObj, // content is required bodyData, param, paramArray = [], updateOptions = {}, reqBody = new sdk.RequestBody(), contentHeader, rDataMode, params, encoding, cType, description, required, enumValue, formHeaders = []; // @TODO: how do we support multiple content types contentObj = requestBody.content; // to handle cases of malformed request body, where contentObj is null if (!contentObj) { return { body: reqBody, contentHeader: null, formHeaders: null }; } // handling for the urlencoded media type if (contentObj.hasOwnProperty(URLENCODED)) { rDataMode = 'urlencoded'; if (contentObj[URLENCODED].hasOwnProperty('schema') && contentObj[URLENCODED].schema.hasOwnProperty('$ref')) { contentObj[URLENCODED].schema = this.getRefObject(contentObj[URLENCODED].schema.$ref, components, options); } bodyData = this.convertToPmBodyData(contentObj[URLENCODED], requestType, URLENCODED, PARAMETER_SOURCE.REQUEST, options.indentCharacter, components, options, schemaCache); encoding = contentObj[URLENCODED].encoding ? contentObj[URLENCODED].encoding : {}; // create query parameters and add it to the request body object _.forOwn(bodyData, (value, key) => { if (_.get(contentObj[URLENCODED], 'schema.type') === 'object') { description = _.get(contentObj[URLENCODED], ['schema', 'properties', key, 'description'], ''); required = _.includes(_.get(contentObj[URLENCODED], ['schema', 'required']), key); enumValue = _.get(contentObj[URLENCODED], ['schema', 'properties', key, 'enum']); } !encoding[key] && (encoding[key] = {}); encoding[key].name = key; encoding[key].schema = { type: typeof value }; // for urlencoded body serialisation is treated similar to query param // reference https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#fixed-fields-13 encoding[key].in = 'query'; _.isBoolean(required) && (encoding[key].required = required); encoding[key].description = description; params = this.convertParamsWithStyle(encoding[key], value, PARAMETER_SOURCE.REQUEST, components, schemaCache, options); // TODO: Show warning for incorrect schema if !params params && params.forEach((element) => { // Collection v2.1 schema allows urlencoded param value to be only string if (typeof element.value !== 'string') { try { // convert other datatype to string (i.e. number, boolean etc) element.value = JSON.stringify(element.value); } catch (e) { // JSON.stringify can fail in few cases, suggest invalid type for such case // eslint-disable-next-line max-len // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Exceptions element.value = 'INVALID_URLENCODED_PARAM_TYPE'; } } }); paramArray.push(...params); }); updateOptions = { mode: rDataMode, urlencoded: paramArray }; // add a content type header for each media type for the request body contentHeader = new sdk.Header({ key: 'Content-Type', value: URLENCODED }); // update the request body with the options reqBody.update(updateOptions); } else if (contentObj.hasOwnProperty(FORM_DATA)) { rDataMode = 'formdata'; bodyData = this.convertToPmBodyData(contentObj[FORM_DATA], requestType, FORM_DATA, PARAMETER_SOURCE.REQUEST, options.indentCharacter, components, options, schemaCache); encoding = contentObj[FORM_DATA].encoding ? contentObj[FORM_DATA].encoding : {}; // create the form parameters and add it to the request body object _.forOwn(bodyData, (value, key) => { if (_.get(contentObj[FORM_DATA], 'schema.type') === 'object') { description = _.get(contentObj[FORM_DATA], ['schema', 'properties', key, 'description'], ''); required = _.includes(_.get(contentObj[FORM_DATA], ['schema', 'required']), key); enumValue = _.get(contentObj[FORM_DATA], ['schema', 'properties', key, 'enum']); } description = (required ? '(Required) ' : '') + description + (enumValue ? ' (This can only be one of ' + enumValue + ')' : ''); if (encoding.hasOwnProperty(key)) { _.forOwn(encoding[key].headers, (value, key) => { if (key !== 'Content-Type') { if (encoding[key].headers[key].hasOwnProperty('$ref')) { encoding[key].headers[key] = getRefObject(encoding[key].headers[key].$ref, components, options); } encoding[key].headers[key].name = key; // this is only for ROOT request because we are adding the headers for example request later formHeaders.push(this.convertToPmHeader(encoding[key].headers[key], REQUEST_TYPE.ROOT, PARAMETER_SOURCE.REQUEST, components, options, schemaCache)); } }); } // Collection v2.1 schema allows form param value to be only string if (typeof value !== 'string') { try { // convert other datatype to string (i.e. number, boolean etc) value = JSON.stringify(value); } catch (e) { // JSON.stringify can fail in few cases, suggest invalid type for such case // eslint-disable-next-line max-len // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Exceptions value = 'INVALID_FORM_PARAM_TYPE'; } } param = new sdk.FormParam({ key: key, value: value, type: 'text' }); param.description = description; paramArray.push(param); }); updateOptions = { mode: rDataMode, formdata: paramArray }; // add a content type header for the pertaining media type contentHeader = new sdk.Header({ key: 'Content-Type', value: FORM_DATA }); // update the request body reqBody.update(updateOptions); } else { rDataMode = 'raw'; let bodyType; // checking for all possible raw types if (contentObj.hasOwnProperty(APP_JS)) { bodyType = APP_JS; } else if (contentObj.hasOwnProperty(APP_JSON)) { bodyType = APP_JSON; } else if (contentObj.hasOwnProperty(TEXT_HTML)) { bodyType = TEXT_HTML; } else if (contentObj.hasOwnProperty(TEXT_PLAIN)) { bodyType = TEXT_PLAIN; } else if (contentObj.hasOwnProperty(TEXT_XML)) { bodyType = TEXT_XML; } else { // take the first property it has // types like image/png etc for (cType in contentObj) { if (contentObj.hasOwnProperty(cType)) { bodyType = cType; break; } } } bodyData = this.convertToPmBodyData(contentObj[bodyType], requestType, bodyType, PARAMETER_SOURCE.REQUEST, options.indentCharacter, components, options, schemaCache); updateOptions = { mode: rDataMode, raw: JSON.stringify(bodyData, null, 4) }; contentHeader = new sdk.Header({ key: 'Content-Type', value: bodyType }); reqBody.update(updateOptions); } return { body: reqBody, contentHeader: contentHeader, formHeaders: formHeaders }; }, /** * @param {*} response in operationItem responses * @param {*} code - response Code * @param {*} originalRequest - the request for the example * @param {object} components - components defined in the OAS spec. These are used to * 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 * @returns {Object} postman response */ convertToPmResponse: function(response, code, originalRequest, components, options, schemaCache) { options = _.merge({}, defaultOptions, options); var responseHeaders = [], previewLanguage = 'text', responseBodyWrapper, header, sdkResponse; if (!response) { return null; } _.forOwn(response.headers, (value, key) => { if (_.toLower(key) !== 'content-type') { if (value.$ref) { // the convert to PmHeader function handles the // schema-faking header = this.getRefObject(value.$ref, components, options); } else { header = value; } header.name = key; if (!_.includes(IMPLICIT_HEADERS, _.toLower(key))) { responseHeaders.push(this.convertToPmHeader(header, REQUEST_TYPE.EXAMPLE, PARAMETER_SOURCE.RESPONSE, components, options, schemaCache)); } } }); responseBodyWrapper = this.convertToPmResponseBody(response.content, components, options, schemaCache); if (responseBodyWrapper.contentTypeHeader) { // we could infer the content-type header from the body responseHeaders.push({ key: 'Content-Type', value: responseBodyWrapper.contentTypeHeader }); if (this.getHeaderFamily(responseBodyWrapper.contentTypeHeader) === HEADER_TYPE.JSON) { previewLanguage = PREVIEW_LANGUAGE.JSON; } else if (this.getHeaderFamily(responseBodyWrapper.contentTypeHeader) === HEADER_TYPE.XML) { previewLanguage = PREVIEW_LANGUAGE.XML; } } else if (response.content && Object.keys(response.content).length > 0) { responseHeaders.push({ key: 'Content-Type', value: Object.keys(response.content)[0] }); if (this.getHeaderFamily(Object.keys(response.content)[0]) === HEADER_TYPE.JSON) { previewLanguage = PREVIEW_LANGUAGE.JSON; } else if (this.getHeaderFamily(Object.keys(response.content)[0]) === HEADER_TYPE.XML) { previewLanguage = PREVIEW_LANGUAGE.XML; } } else { responseHeaders.push({ key: 'Content-Type', value: TEXT_PLAIN }); } // replace 'X' char with '0' code = code.replace(/X/g, '0'); code = code === 'default' ? 500 : _.toSafeInteger(code); sdkResponse = new sdk.Response({ name: response.description, code: code || 500, header: responseHeaders, body: responseBodyWrapper.responseBody, originalRequest: originalRequest }); sdkResponse._postman_previewlanguage = previewLanguage; return sdkResponse; }, /** * @param {*} $ref reference object * @param {object} components - components defined in the OAS spec. These are used to * resolve references while generating params. * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. * @returns {Object} reference object from the saved components * @no-unit-tests */ getRefObject: function($ref, components, options) { options = _.merge({}, defaultOptions, options); var refObj, savedSchema; savedSchema = $ref.split('/').slice(1).map((elem) => { // https://swagger.io/docs/specification/using-ref#escape // since / is the default delimiter, slashes are escaped with ~1 return decodeURIComponent( elem .replace(/~1/g, '/') .replace(/~0/g, '~') ); }); // at this stage, savedSchema is [components, part1, parts] // must have min. 2 segments after "#/components" if (savedSchema.length < 3) { console.warn(`ref ${$ref} not found.`); return { value: `reference ${$ref} not found in the given specification` }; } if (savedSchema[0] !== 'components' && savedSchema[0] !== 'paths') { console.warn(`Error reading ${$ref}. Can only use references from components and paths`); return { value: `Error reading ${$ref}. Can only use references from components and paths` }; } // at this point, savedSchema is similar to ['components', 'schemas','Address'] // components is actually components and paths (an object with components + paths as 1st-level-props) refObj = _.get(components, savedSchema); if (!refObj) { console.warn(`ref ${$ref} not found.`); return { value: `reference ${$ref} not found in the given specification` }; } if (refObj.$ref) { return this.getRefObject(refObj.$ref, components, options); } 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); }, /** * Finds all the possible path variables conversion from schema path, * and excludes path variable that are converted to collection variable * @param {string} path Path string * @returns {array} Array of path variables. */ findPathVariablesFromSchemaPath: function (path) { // /{path}/{file}.{format}/{hello} return [ '/{path}', '/{hello}' ] // https://regex101.com/r/aFRWQD/4 let matches = path.match(/(\/\{[^\/\{\}]+\}(?=[\/\0]|$))/g); // remove leading '/' and starting and ending curly braces return _.map(matches, (match) => { return match.slice(2, -1); }); }, /** * Finds fixed parts present in path segment of collection or schema. * * @param {String} segment - Path segment * @param {String} pathType - Path type (one of 'collection' / 'schema') * @returns {Array} - Array of strings where each element is fixed part in order of their occurence */ getFixedPartsFromPathSegment: function (segment, pathType = 'collection') { var tempSegment = segment, // collection is default varMatches = segment.match(pathType === 'schema' ? /(\{[^\/\{\}]+\})/g : /(\{\{[^\/\{\}]+\}\})/g), fixedParts = []; _.forEach(varMatches, (match) => { let matchedIndex = tempSegment.indexOf(match); // push fixed part before collection variable if present (matchedIndex !== 0) && (fixedParts.push(tempSegment.slice(0, matchedIndex))); // substract starting fixed and variable part from tempSegment tempSegment = tempSegment.substr(matchedIndex + match.length); }); // add last fixed part if present (tempSegment.length > 0) && (fixedParts.push(tempSegment)); return fixedParts; }, /** Separates out 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 { url: reqUrl, pathVars, collectionVars }; }, /** * function to convert an openapi path item to postman item * @param {*} openapi openapi object with root properties * @param {*} operationItem path operationItem from tree structure * @param {object} components - components defined in the OAS spec. These are used to * 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, variableStore) { options = _.merge({}, defaultOptions, options); var reqName, pathVariables = openapi.baseUrlVariables, operation = operationItem.properties, reqBody = operationItem.properties.requestBody, itemParams = operationItem.properties.parameters, reqParams = this.getParametersForPathItem(itemParams), baseUrl = '{{baseUrl}}', pathVarArray = [], authHelper, item, serverObj, displayUrl, reqUrl = '/' + operationItem.path, pmBody, authMeta, swagResponse, localServers = _.get(operationItem, 'properties.servers'), exampleRequestBody, sanitizeResult, globalServers = _.get(operationItem, 'servers'); // handling path templating in request url if any // 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.url; // Updated reqParams.path reqParams.path = sanitizeResult.pathVars; // Add collection variables to the variableStore. sanitizeResult.collectionVars.forEach((element) => { if (!variableStore[element.name]) { let fakedData = options.schemaFaker ? safeSchemaFaker(element.schema || {}, options.requestParametersResolution, PROCESSING_TYPE.CONVERSION, PARAMETER_SOURCE.REQUEST, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache, options.stackLimit) : '', convertedPathVar = _.get(this.convertParamsWithStyle(element, fakedData, PARAMETER_SOURCE.REQUEST, components, schemaCache, options), '[0]', {}); variableStore[element.name] = _.assign(convertedPathVar, { key: element.name, 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) { serverObj = operationItem.properties.servers[0]; // convert all {anything} to {{anything}} baseUrl = this.fixPathVariablesInUrl(serverObj.url); // add serverObj variables to pathVarArray if (serverObj.variables) { _.forOwn(serverObj.variables, (value, key) => { pathVarArray.push({ name: key, value: value.default || '', description: this.getParameterDescription(value) }); }); // use pathVarAray to sanitize url for path params and collection variables. sanitizeResult = this.sanitizeUrlPathParams(baseUrl, pathVarArray); // update the base url with update url baseUrl = sanitizeResult.url; // Add new collection variables to the variableStore sanitizeResult.collectionVars.forEach((element) => { if (!variableStore[element.name]) { variableStore[element.name] = { key: element.name, value: element.value || '', description: element.description, type: 'collection' }; } }); // remove all the collection variables from serverObj.variables serverObj.pathVariables = {}; sanitizeResult.pathVars.forEach((element) => { serverObj.pathVariables[element.name] = serverObj.variables[element.name]; }); // use this new filterd serverObj.pathVariables // to generate pm path variables. pathVarArray = this.convertPathVariables('method', [], serverObj.pathVariables, components, options, schemaCache); } baseUrl += reqUrl; } else { // accounting for the overriding of the root level servers object if present at the path level if (Array.isArray(globalServers) && globalServers.length) { // All the global servers present at the path level are taken care of in generateTrieFromPaths // Just adding the same structure of the url as the display URL. displayUrl = '{{' + this.fixPathVariableName(operationItem.path) + '}}' + reqUrl; } // In case there are no server available use the baseUrl else { baseUrl += reqUrl; if (pathVariables) { displayUrl = baseUrl; } else { displayUrl = '{{baseUrl}}' + reqUrl; } } pathVarArray = this.convertPathVariables('root', [], pathVariables, components, options, schemaCache); } switch (options.requestNameSource) { case 'fallback' : { // operationId is usually camelcase or snake case reqName = operation.summary || utils.insertSpacesInName(operation.operationId) || reqUrl; break; } case 'url' : { reqName = displayUrl || baseUrl; break; } default : { reqName = operation[options.requestNameSource]; break; } } if (!reqName) { throw new openApiErr(`requestNameSource (${options.requestNameSource})` + ` in options is invalid or property does not exist in ${operationItem.path}`); } // handling authentication here (for http type only) authHelper = this.getAuthHelper(openapi, operation.security); // creating the request object item = new sdk.Item({ name: reqName, request: { description: operation.description, url: displayUrl || baseUrl, name: reqName, method: operationItem.method.toUpperCase() } }); // using the auth helper authMeta = operation['x-postman-meta']; if (authMeta && authMeta.currentHelper && authMap[authMeta.currentHelper]) { let thisAuthObject = { type: authMap[authMeta.currentHelper] }; thisAuthObject[authMap[authMeta.currentHelper]] = authMeta.helperAttributes; item.request.auth = new sdk.RequestAuth(thisAuthObject); } else { item.request.auth = authHelper; } // adding query params to postman request url. _.forEach(reqParams.query, (queryParam) => { this.convertToPmQueryParameters(queryParam, REQUEST_TYPE.ROOT, components, options, schemaCache) .forEach((pmParam) => { item.request.url.addQueryParams(pmParam); }); }); item.request.url.query.members.forEach((query) => { // Collection v2.1 schema allows query param value to be string/null if (typeof query.value !== 'string') { try { // convert other datatype to string (i.e. number, boolean etc) query.value = JSON.stringify(query.value); } catch (e) { // JSON.stringify can fail in few cases, suggest invalid type for such case // eslint-disable-next-line max-len // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Exceptions query.value = 'INVALID_QUERY_PARAM_TYPE'; } } }); item.request.url.variables.clear(); item.request.url.variables.assimilate(this.convertPathVariables('param', pathVarArray, reqParams.path, components, options, schemaCache)); // Making sure description never goes out as an object // App / Collection transformer fail with the object syntax if (item.request.url.variables.members && item.request.url.variables.members.length > 0) { item.request.url.variables.members = _.map(item.request.url.variables.members, (m) => { if (typeof m.description === 'object' && m.description.content) { m.description = m.description.content; } return m; }); } // adding headers to request from reqParam _.forEach(reqParams.header, (header) => { if (options.keepImplicitHeaders || !_.includes(IMPLICIT_HEADERS, _.toLower(_.get(header, 'name')))) { item.request.addHeader(this.convertToPmHeader(header, REQUEST_TYPE.ROOT, PARAMETER_SOURCE.REQUEST, components, options, schemaCache)); } }); // adding Request Body and Content-Type header if (reqBody) { if (reqBody.$ref) { reqBody = this.getRefObject(reqBody.$ref, components, options); } pmBody = this.convertToPmBody(reqBody, REQUEST_TYPE.ROOT, components, options, schemaCache); item.request.body = pmBody.body; if (!options.keepImplicitHeaders || (!_.find(reqParams.header, (h) => { return _.toLower(_.get(h, 'name')) === 'content-type'; }))) { // Add detected content-type header item.request.addHeader(pmBody.contentHeader); } // extra form headers if encoding is present in request Body. // TODO: Show warning for incorrect schema if !pmBody.formHeaders pmBody.formHeaders && pmBody.formHeaders.forEach((element) => { item.request.addHeader(element); }); } // adding responses to request item if (operation.responses) { let thisOriginalRequest = {}, responseAuthHelper, authQueryParams, convertedResponse; if (options.includeAuthInfoInExample) { responseAuthHelper = this.getResponseAuthHelper(authHelper); // override auth helper with global security definition if no operation security definition found if (_.isEmpty(authHelper)) { responseAuthHelper = this.getResponseAuthHelper(this.getAuthHelper(openapi, openapi.security)); } authQueryParams = _.map(responseAuthHelper.query, (queryParam) => { // key-value pair will be added as transformed query string return queryParam.key + '=' + queryParam.value; }); } _.forOwn(operation.responses, (response, code) => { let originalRequestHeaders = [], originalRequestQueryParams = this.convertToPmQueryArray(reqParams, REQUEST_TYPE.EXAMPLE, components, options, schemaCache); swagResponse = response; if (_.get(response, '$ref')) { swagResponse = this.getRefObject(response.$ref, components, options); } if (options.includeAuthInfoInExample) { // add Authorization params if present originalRequestQueryParams = _.concat(originalRequestQueryParams, authQueryParams); originalRequestHeaders = _.concat(originalRequestHeaders, responseAuthHelper.header); } // Try and set fields for originalRequest (example.request) thisOriginalRequest.method = item.request.method; // setting URL const clonedItemURL = _.cloneDeep(item.request.url); thisOriginalRequest.url = clonedItemURL; /** * Setting variable * overriding `thisOriginalRequest.url.variable` as the url defination expects * 1. Field variable and not variables * 2. Field variable to be array of objects which maps to `clonedItemURL.variables.members` */ thisOriginalRequest.url.variable = clonedItemURL.variables.members; thisOriginalRequest.url.query = []; // setting query params if (originalRequestQueryParams.length) { thisOriginalRequest.url.query = originalRequestQueryParams.join('&'); } // setting headers _.forEach(reqParams.header, (header) => { originalRequestHeaders.push(this.convertToPmHeader(header, REQUEST_TYPE.EXAMPLE, PARAMETER_SOURCE.REQUEST, components, options, schemaCache)); }); thisOriginalRequest.header = originalRequestHeaders; // setting request body try { exampleRequestBody = this.convertToPmBody(operationItem.properties.requestBody, REQUEST_TYPE.EXAMPLE, components, options, schemaCache); thisOriginalRequest.body = exampleRequestBody.body ? exampleRequestBody.body.toJSON() : {}; } catch (e) { // console.warn('Exception thrown while trying to json-ify body for item.request.body:', item.request.body, // 'Exception:', e); thisOriginalRequest.body = {}; } convertedResponse = this.convertToPmResponse(swagResponse, code, thisOriginalRequest, components, options, schemaCache); convertedResponse && item.responses.add(convertedResponse); }); } return item; }, /** * function to convert an openapi query params object to array of query params * @param {*} reqParams openapi query params object * @param {*} requestType Specifies whether the request body is of example request or root request * @param {object} components - components defined in the OAS spec. These are used to * 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 * @returns {*} array of all query params */ convertToPmQueryArray: function(reqParams, requestType, components, options, schemaCache) { options = _.merge({}, defaultOptions, options); let requestQueryParams = []; _.forEach(reqParams.query, (queryParam) => { this.convertToPmQueryParameters(queryParam, requestType, components, options, schemaCache).forEach((pmParam) => { requestQueryParams.push(pmParam.key + '=' + pmParam.value); }); }); return requestQueryParams; }, // along with the path object, this also returns the values of the // path variable's values // also, any endpoint-level params are merged into the returned pathItemObject findMatchingRequestFromSchema: function (method, url, schema, options) { // first step - get array of requests from schema let parsedUrl = require('url').parse(url), retVal = [], pathToMatch, matchedPath, matchedPathJsonPath, schemaPathItems = schema.paths, filteredPathItemsArray = []; // Return no matches for invalid url (if unable to decode parsed url) try { pathToMatch = decodeURI(parsedUrl.pathname); } catch (e) { console.warn( 'Error decoding request URI endpoint. URI: ', url, 'Error', e ); return retVal; } // if pathToMatch starts with '/', we assume it's the correct path // if not, we assume the segment till the first '/' is the host // this is because a Postman URL like "{{url}}/a/b" will // likely have {{url}} as the host segment if (!pathToMatch.startsWith('/')) { pathToMatch = pathToMatch.substring(pathToMatch.indexOf('/')); } // Here, only take pathItemObjects that have the right method // of those that do, determine a score // then just pick that key-value pair from schemaPathItems _.forOwn(schemaPathItems, (pathItemObject, path) => { if (!pathItemObject) { // invalid schema. schema.paths had an invalid entry return true; } if (!pathItemObject.hasOwnProperty(method.toLowerCase())) { // the required method was not found at this path return true; } // filter empty parameters pathItemObject.parameters = _.reduce(pathItemObject.parameters, (accumulator, param) => { if (!_.isEmpty(param)) { accumulator.push(param); } return accumulator; }, []); // check if path and pathToMatch match (non-null) let schemaMatchResult = this.getPostmanUrlSchemaMatchScore(pathToMatch, path, options); if (!schemaMatchResult.match) { // there was no reasonable match b/w the postman path and this schema path return true; } filteredPathItemsArray.push({ path, pathItem: pathItemObject, matchScore: schemaMatchResult.score, pathVars: schemaMatchResult.pathVars, // No. of fixed segment matches between schema and postman url path // i.e. schema path /user/{userId} and request path /user/{{userId}} has 1 fixed segment match ('user') fixedMatchedSegments: schemaMatchResult.fixedMatchedSegments, // No. of variable segment matches between schema and postman url path // i.e. schema path /user/{userId} and request path /user/{{userId}} has 1 variable segment match ('{userId}') variableMatchedSegments: schemaMatchResult.variableMatchedSegments }); }); // order endpoints with more fix matched segments and variable matched segments (for tie in former) first in result filteredPathItemsArray = _.orderBy(filteredPathItemsArray, ['fixedMatchedSegments', 'variableMatchedSegments'], ['desc', 'desc']); _.each(filteredPathItemsArray, (fp) => { let path = fp.path, pathItemObject = fp.pathItem, score = fp.matchScore, pathVars = fp.pathVars; matchedPath = pathItemObject[method.toLowerCase()]; if (!matchedPath) { // method existed at the path, but was a falsy value return true; } matchedPathJsonPath = `$.paths[${path}]`; // filter empty parameters matchedPath.parameters = _.reduce(matchedPath.parameters, (accumulator, param) => { if (!_.isEmpty(param)) { accumulator.push(param); } return accumulator; }, []); // aggregate local + global parameters for this path matchedPath.parameters = _.map(matchedPath.parameters, (commonParam) => { // for path-specifix params that are added to the path, have a way to identify them // when the schemaPath is required // method is lowercased because OAS methods are always lowercase commonParam.pathPrefix = `${matchedPathJsonPath}.${method.toLowerCase()}.parameters`; return commonParam; }).concat( _.map(pathItemObject.parameters || [], (commonParam) => { // for common params that are added to the path, have a way to identify them // when the schemaPath is required commonParam.pathPrefix = matchedPathJsonPath + '.parameters'; return commonParam; }) ); retVal.push({ // using path instead of operationId / sumamry since it's widely understood name: method + ' ' + path, // assign path as schemaPathName property to use path in path object path: _.assign(matchedPath, { schemaPathName: path }), jsonPath: matchedPathJsonPath + '.' + method.toLowerCase(), pathVariables: pathVars, score: score }); // code reaching here indicates the given method was not found return true; }); return retVal; }, /** * Checks if value is postman variable or not * * @param {*} value - Value to check for * @returns {Boolean} postman variable or not */ isPmVariable: function (value) { // collection/environment variables are in format - {{var}} return _.isString(value) && _.startsWith(value, '{{') && _.endsWith(value, '}}'); }, /** * This function is little modified version of lodash _.get() * where if path is empty it will return source object instead undefined/fallback value * * @param {Object} sourceValue - source from where value is to be extracted * @param {String} dataPath - json path to value that is to be extracted * @param {*} fallback - fallback value if sourceValue doesn't contain value at dataPath * @returns {*} extracted value */ getPathValue: function (sourceValue, dataPath, fallback) { return (dataPath === '' ? sourceValue : _.get(sourceValue, dataPath, fallback)); }, /** * Returns all security params that can be applied during converion. * * @param {Object} components - OpenAPI components * @param {String} location - location for which we want to get security params (i.e. 'header' | 'query') * @returns {Array} applicable security params */ getSecurityParams: function (components, location) { let securityDefs = _.get(components, 'securitySchemes', {}), securityParams = []; _.forEach(securityDefs, (securityDef) => { // Currently we only apply header and query for apiKey type of security param during conversion if (_.get(securityDef, 'type') === 'apiKey' && _.get(securityDef, 'in') === location) { securityParams.push(securityDef); } }); return securityParams; }, /** * Provides information regarding serialisation of param * * @param {*} param - OpenAPI Parameter object * @param {String} parameterSource - Specifies whether the schema being faked is from a request or response. * @param {Object} components - OpenAPI components defined in the OAS spec. These are used to * resolve references while generating params. * @param {Object} schemaCache - object storing schemaFaker and schmeResolution caches * @returns {Object} - Information regarding parameter serialisation. Contains following properties. * { * style - style property defined/inferred from schema * explode - explode property defined/inferred from schema * startValue - starting value that is prepended to serialised value * propSeparator - Character that separates two properties or values in serialised string of respective param * keyValueSeparator - Character that separates key from values in serialised string of respective param * isExplodable - whether params can be exploded (serialised value can contain key and value) * } */ getParamSerialisationInfo: function (param, parameterSource, components, schemaCache) { var paramName = _.get(param, 'name'), paramSchema = deref.resolveRefs(_.cloneDeep(param.schema), parameterSource, components, _.get(schemaCache, 'schemaResolutionCache')), style, // style property defined/inferred from schema explode, // explode property defined/inferred from schema propSeparator, // separates two properties or values keyValueSeparator, // separats key from value startValue = '', // starting value that is unique to each style // following prop represents whether param can be truly exploded, as for some style even when explode is true, // serialisation doesn't separate key-value isExplodable = paramSchema.type === 'object'; // for invalid param object return null if (!_.isObject(param)) { return null; } // decide allowed / default style for respective param location switch (param.in) { case 'path': style = _.includes(['matrix', 'label', 'simple'], param.style) ? param.style : 'simple'; break; case 'query': style = _.includes(['form', 'spaceDelimited', 'pipeDelimited', 'deepObject'], param.style) ? param.style : 'form'; break; case 'header': style = 'simple'; break; default: style = 'simple'; break; } // decide allowed / default explode property for respective param location explode = (_.isBoolean(param.explode) ? param.explode : (_.includes(['form', 'deepObject'], style))); // decide explodable params, starting value and separators between key-value and properties for serialisation switch (style) { case 'matrix': isExplodable = paramSchema.type === 'object' || explode; startValue = ';' + ((paramSchema.type === 'object' && explode) ? '' : (paramName + '=')); propSeparator = explode ? ';' : ','; keyValueSeparator = explode ? '=' : ','; break; case 'label': startValue = '.'; propSeparator = '.'; keyValueSeparator = explode ? '=' : '.'; break; case 'form': // for 'form' when explode is true, query is devided into different key-value pairs propSeparator = keyValueSeparator = ','; break; case 'simple': propSeparator = ','; keyValueSeparator = explode ? '=' : ','; break; case 'spaceDelimited': explode = false; propSeparator = keyValueSeparator = '%20'; break; case 'pipeDelimited': explode = false; propSeparator = keyValueSeparator = '|'; break; case 'deepObject': // for 'deepObject' query is devided into different key-value pairs explode = true; break; default: break; } return { style, explode, startValue, propSeparator, keyValueSeparator, isExplodable }; }, /** * This functiom deserialises parameter value based on param schema * * @param {*} param - OpenAPI Parameter object * @param {String} paramValue - Parameter value to be deserialised * @param {String} parameterSource - Specifies whether the schema being faked is from a request or response. * @param {Object} components - OpenAPI components defined in the OAS spec. These are used to * resolve references while generating params. * @param {Object} schemaCache - object storing schemaFaker and schmeResolution caches * @returns {*} - deserialises parameter value */ deserialiseParamValue: function (param, paramValue, parameterSource, components, schemaCache) { var constructedValue, paramSchema = deref.resolveRefs(_.cloneDeep(param.schema), parameterSource, components, _.get(schemaCache, 'schemaResolutionCache')), isEvenNumber = (num) => { return (num % 2 === 0); }, convertToDataType = (value) => { try { return JSON.parse(value); } catch (e) { return value; } }; // for invalid param object return null if (!_.isObject(param) || !_.isString(paramValue)) { return null; } let { startValue, propSeparator, keyValueSeparator, isExplodable } = this.getParamSerialisationInfo(param, parameterSource, components, schemaCache); // as query params are constructed from url, during conversion we use decodeURI which converts ('%20' into ' ') (keyValueSeparator === '%20') && (keyValueSeparator = ' '); (propSeparator === '%20') && (propSeparator = ' '); // remove start value from serialised value paramValue = paramValue.slice(paramValue.indexOf(startValue) === 0 ? startValue.length : 0); // define value to constructed according to type paramSchema.type === 'object' && (constructedValue = {}); paramSchema.type === 'array' && (constructedValue = []); if (constructedValue) { let allProps = paramValue.split(propSeparator); _.forEach(allProps, (element, index) => { let keyValArray; if (propSeparator === keyValueSeparator && isExplodable) { if (isEvenNumber(index)) { keyValArray = _.slice(allProps, index, index + 2); } else { return; } } else if (isExplodable) { keyValArray = element.split(keyValueSeparator); } if (paramSchema.type === 'object') { _.set(constructedValue, keyValArray[0], convertToDataType(keyValArray[1])); } else if (paramSchema.type === 'array') { constructedValue.push(convertToDataType(_.get(keyValArray, '[1]', element))); } }); } else { constructedValue = paramValue; } return constructedValue; }, /** * Parses media type from given content-type header or media type * from content object into type and subtype * * @param {String} str - string to be parsed * @returns {Object} - Parsed media type into type and subtype */ parseMediaType: function (str) { let simpleMediaTypeRegExp = /^\s*([^\s\/;]+)\/([^;\s]+)\s*(?:;(.*))?$/, match = simpleMediaTypeRegExp.exec(str), type = '', subtype = ''; if (match) { // as mediatype name are case-insensitive keep it in lower case for uniformity type = _.toLower(match[1]); subtype = _.toLower(match[2]); } return { type, subtype }; }, /** * Extracts all child parameters from explodable param * * @param {*} schema - Corresponding schema object of parent parameter to be devided into child params * @param {*} paramKey - Parameter name of parent param object * @param {*} metaInfo - meta information of param (i.e. required) * @returns {Array} - Extracted child parameters */ extractChildParamSchema: function (schema, paramKey, metaInfo) { let childParamSchemas = []; _.forEach(_.get(schema, 'properties', {}), (value, key) => { if (_.get(value, 'type') === 'object') { childParamSchemas = _.concat(childParamSchemas, this.extractChildParamSchema(value, `${paramKey}[${key}]`, metaInfo)); } else { let required = _.get(metaInfo, 'required') || false, description = _.get(metaInfo, 'description') || '', pathPrefix = _.get(metaInfo, 'pathPrefix'); childParamSchemas.push({ name: `${paramKey}[${key}]`, schema: value, description, required, isResolvedParam: true, pathPrefix }); } }); return childParamSchemas; }, /** * Tests whether given parameter is of complex array type from param key * * @param {*} paramKey - Parmaeter key that is to be tested * @returns {Boolean} - result */ isParamComplexArray: function (paramKey) { // this checks if parameter key numbered element (i.e. itemArray[1] is complex array param) let regex = /\[[\d]+\]/gm; return regex.test(paramKey); }, /** * Finds valid JSON media type object from content object * * @param {*} contentObj - Content Object from schema * @returns {*} - valid JSON media type if exists */ getJsonContentType: function (contentObj) { let jsonContentType = _.find(_.keys(contentObj), (contentType) => { let mediaType = this.parseMediaType(contentType); return mediaType.type === 'application' && ( mediaType.subtype === 'json' || _.endsWith(mediaType.subtype, '+json') ); }); return jsonContentType; }, /** * * @param {String} property - one of QUERYPARAM, PATHVARIABLE, HEADER, BODY, RESPONSE_HEADER, RESPONSE_BODY * @param {String} jsonPathPrefix - this will be prepended to all JSON schema paths on the request * @param {String} txnParamName - Optional - The name of the param being validated (useful for query params, * req headers, res headers) * @param {*} value - the value of the property in the request * @param {String} schemaPathPrefix - this will be prepended to all JSON schema paths on the schema * @param {Object} openApiSchemaObj - The OpenAPI schema object against which to validate * @param {String} parameterSourceOption tells that the schema object is of request or response * @param {Object} components - Components in the spec that the schema might refer to * @param {Object} options - Global options * @param {Object} schemaCache object storing schemaFaker and schmeResolution caches * @param {Function} callback - For return * @returns {Array} array of mismatches */ checkValueAgainstSchema: function (property, jsonPathPrefix, txnParamName, value, schemaPathPrefix, openApiSchemaObj, parameterSourceOption, components, options, schemaCache, callback) { let mismatches = [], jsonValue, humanPropName = propNames[property], needJsonMatching = (property === 'BODY' || property === 'RESPONSE_BODY'), invalidJson = false, valueToUse = value, // This is dereferenced schema (converted to JSON schema for validation) schema = deref.resolveRefs(openApiSchemaObj, parameterSourceOption, components, schemaCache.schemaResolutionCache, PROCESSING_TYPE.VALIDATION, 'example', 0, {}, options.stackLimit); if (needJsonMatching) { try { jsonValue = JSON.parse(value); // If valid JSON is detected, the parsed value should be used // to determine mismatches valueToUse = jsonValue; } catch (e) { jsonValue = ''; invalidJson = true; } } // When processing a reference, schema.type could also be undefined if (schema && schema.type) { if (typeof schemaTypeToJsValidator[schema.type] === 'function') { let isCorrectType; // Treat unresolved postman collection/environment variable as correct type if (options.ignoreUnresolvedVariables && this.isPmVariable(valueToUse)) { isCorrectType = true; } else { isCorrectType = schemaTypeToJsValidator[schema.type](valueToUse); } if (!isCorrectType) { // if type didn't match, no point checking for AJV let reason = '', mismatchObj; // exclude mismatch errors for nested objects in parameters (at this point simple objects and array should // be already converted to primitive schema and only nested objects remains as type object/array) if (_.includes(['QUERYPARAM', 'PATHVARIABLE', 'HEADER'], property) && (schema.type === 'object' || schema.type === 'array')) { return callback(null, []); } if (property === 'RESPONSE_BODY' || property === 'BODY') { // we don't have names for the body, but there's only one reason = 'The ' + humanPropName; } else if (txnParamName) { // for query params, req/res headers, path vars, we have a name. Praise the lord. reason = `The ${humanPropName} "${txnParamName}"`; } else { // for query params, req/res headers, path vars, we might not ALWAYS have a name. reason = `A ${humanPropName}`; } reason += ` needs to be of type ${schema.type}, but we found `; if (!options.shortValidationErrors) { reason += `"${valueToUse}"`; } else if (invalidJson) { reason += 'invalid JSON'; } else if (Array.isArray(valueToUse)) { reason += 'an array instead'; } else if (typeof valueToUse === 'object') { reason += 'an object instead'; } else { reason += `a ${typeof valueToUse} instead`; } mismatchObj = { property, transactionJsonPath: jsonPathPrefix, schemaJsonPath: schemaPathPrefix, reasonCode: 'INVALID_TYPE', reason }; if (options.suggestAvailableFixes) { mismatchObj.suggestedFix = { key: txnParamName, actualValue: valueToUse, suggestedValue: safeSchemaFaker(openApiSchemaObj || {}, 'example', PROCESSING_TYPE.VALIDATION, parameterSourceOption, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache, options.stackLimit) }; } return callback(null, [mismatchObj]); } // only do AJV if type is array or object // simpler cases are handled by a type check if (isCorrectType && needJsonMatching) { let filteredValidationError = validateSchema(schema, valueToUse, options); if (!_.isEmpty(filteredValidationError)) { let mismatchObj, suggestedValue, fakedValue = safeSchemaFaker(openApiSchemaObj || {}, 'example', PROCESSING_TYPE.VALIDATION, parameterSourceOption, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache, options.stackLimit); // Show detailed validation mismatches for only request/response body if (options.detailedBlobValidation && needJsonMatching) { _.forEach(filteredValidationError, (ajvError) => { let localSchemaPath = ajvError.schemaPath.replace(/\//g, '.').slice(2), dataPath = ajvError.dataPath || ''; // discard the leading '.' if it exists if (dataPath[0] === '.') { dataPath = dataPath.slice(1); } mismatchObj = _.assign({ property: property, transactionJsonPath: jsonPathPrefix + ajvError.dataPath, schemaJsonPath: schemaPathPrefix + '.' + localSchemaPath }, ajvValidationError(ajvError, { property, humanPropName })); if (options.suggestAvailableFixes) { mismatchObj.suggestedFix = { key: _.split(dataPath, '.').pop(), actualValue: this.getPathValue(valueToUse, dataPath, null), suggestedValue: this.getSuggestedValue(fakedValue, valueToUse, ajvError) }; } mismatches.push(mismatchObj); }); } else { mismatchObj = { reason: `The ${humanPropName} didn\'t match the specified schema`, reasonCode: 'INVALID_TYPE' }; // assign proper reason codes for invalid body if (property === 'BODY') { mismatchObj.reasonCode = 'INVALID_BODY'; } else if (property === 'RESPONSE_BODY') { mismatchObj.reasonCode = 'INVALID_RESPONSE_BODY'; } if (options.suggestAvailableFixes) { suggestedValue = _.cloneDeep(valueToUse); // Apply each fix individually to respect existing values in request _.forEach(filteredValidationError, (ajvError) => { let dataPath = ajvError.dataPath || ''; // discard the leading '.' if it exists if (dataPath[0] === '.') { dataPath = dataPath.slice(1); } // for empty string _.set creates new key with empty string '', so separate handling if (dataPath === '') { suggestedValue = this.getSuggestedValue(fakedValue, suggestedValue, ajvError); } else { _.set(suggestedValue, dataPath, this.getSuggestedValue(fakedValue, suggestedValue, ajvError)); } }); mismatchObj.suggestedFix = { key: property.toLowerCase(), actualValue: valueToUse, suggestedValue }; } mismatches.push(_.assign({ property: property, transactionJsonPath: jsonPathPrefix, schemaJsonPath: schemaPathPrefix }, mismatchObj)); } // only return AJV mismatches return callback(null, mismatches); } // result passed. No AJV mismatch } // Schema was not AJV or object // Req/Res Body was non-object but content type is application/json else if (needJsonMatching) { return callback(null, [{ property, transactionJsonPath: jsonPathPrefix, schemaJsonPath: schemaPathPrefix, reasonCode: 'INVALID_TYPE', reason: `The ${humanPropName} needs to be of type object/array, but we found "${valueToUse}"`, suggestedFix: { key: null, actualValue: valueToUse, suggestedValue: {} // suggest value to be object } }]); } } else { // unknown schema.type found // TODO: Decide how to handle. Log? } } // Schema not defined return callback(null, []); // if (!schemaTypeToJsValidator[schema.type](value)) { // callback(null, [{ // property, // transactionJsonPath: jsonPathPrefix, // schemaJsonPath: schemaPathPrefix, // reasonCode: 'INVALID_TYPE', // reason: `Value must be a token of type ${schema.type}, found ${value}` // }]); // } // TODO: Further checks for object type // else { // callback(null, []); // } }, /** * * @param {*} determinedPathVariables the key/determined-value pairs of the path variables (from Postman) * @param {*} transactionPathPrefix the jsonpath for this validation (will be prepended to all identified mismatches) * @param {*} schemaPath the applicable pathItem defined at the schema level * @param {*} components the components + paths from the OAS spec that need to be used to resolve $refs * @param {*} options OAS options * @param {*} schemaCache object storing schemaFaker and schmeResolution caches * @param {*} callback Callback * @returns {array} mismatches (in the callback) */ checkPathVariables: function ( determinedPathVariables, transactionPathPrefix, schemaPath, components, options, schemaCache, callback) { // schema path should have all parameters needed // components need to be stored globally var mismatchProperty = 'PATHVARIABLE', // all path variables defined in this path. acc. to the spec, all path params are required schemaPathVariables, pmPathVariables; if (options.validationPropertiesToIgnore.includes(mismatchProperty)) { return callback(null, []); } // find all schema path variables that can be present as collection path variables pmPathVariables = this.findPathVariablesFromSchemaPath(schemaPath.schemaPathName); schemaPathVariables = _.filter(schemaPath.parameters, (param) => { // exclude path variables stored as collection variable from being validated further return (param.in === 'path' && _.includes(pmPathVariables, param.name)); }); async.map(determinedPathVariables, (pathVar, cb) => { let mismatches = [], resolvedParamValue, index = _.findIndex(determinedPathVariables, pathVar); const schemaPathVar = _.find(schemaPathVariables, (param) => { return param.name === pathVar.key; }); if (!schemaPathVar) { // extra pathVar present in given request. if (options.showMissingInSchemaErrors) { mismatches.push({ property: mismatchProperty, // not adding the pathVar name to the jsonPath because URL is just a string transactionJsonPath: transactionPathPrefix + `[${index}]`, schemaJsonPath: null, reasonCode: 'MISSING_IN_SCHEMA', reason: `The path variable "${pathVar.key}" was not found in the schema` }); } return cb(null, mismatches); } // assign parameter example(s) as schema examples this.assignParameterExamples(schemaPathVar); resolvedParamValue = this.deserialiseParamValue(schemaPathVar, pathVar.value, PARAMETER_SOURCE.REQUEST, components, schemaCache); setTimeout(() => { if (!(schemaPathVar && schemaPathVar.schema)) { // no errors to show if there's no schema present in the spec return cb(null, []); } this.checkValueAgainstSchema(mismatchProperty, transactionPathPrefix + `[${index}].value`, pathVar.key, resolvedParamValue, schemaPathVar.pathPrefix + '[?(@.name==\'' + schemaPathVar.name + '\')]', schemaPathVar.schema, PARAMETER_SOURCE.REQUEST, components, options, schemaCache, cb); }, 0); }, (err, res) => { let mismatches = [], mismatchObj; if (err) { return callback(err); } // go through required schemaPathVariables, and params that aren't found in the given transaction are errors _.each(schemaPathVariables, (pathVar) => { if (!_.find(determinedPathVariables, (param) => { return param.key === pathVar.name; })) { // assign parameter example(s) as schema examples; this.assignParameterExamples(pathVar); mismatchObj = { property: mismatchProperty, transactionJsonPath: transactionPathPrefix, schemaJsonPath: pathVar.pathPrefix, reasonCode: 'MISSING_IN_REQUEST', reason: `The required path variable "${pathVar.name}" was not found in the transaction` }; if (options.suggestAvailableFixes) { mismatchObj.suggestedFix = { key: pathVar.name, actualValue: null, suggestedValue: { key: pathVar.name, value: safeSchemaFaker(pathVar.schema || {}, 'example', PROCESSING_TYPE.VALIDATION, PARAMETER_SOURCE.REQUEST, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache, options.stackLimit), description: this.getParameterDescription(pathVar) } }; } mismatches.push(mismatchObj); } }); // res is an array of mismatches (also an array) from all checkValueAgainstSchema calls return callback(null, _.concat(_.flatten(res), mismatches)); }); }, /** * * @param {*} transaction Transaction with which to compare * @param {*} transactionPathPrefix the jsonpath for this validation (will be prepended to all identified mismatches) * @param {*} schemaPath the applicable pathItem defined at the schema level * @param {*} pathRoute Route to applicable pathItem (i.e. 'GET /users/{userID}') * @param {*} options OAS options * @param {*} callback Callback * @returns {array} mismatches (in the callback) */ checkMetadata(transaction, transactionPathPrefix, schemaPath, pathRoute, options, callback) { let expectedReqName, expectedReqDesc, reqNameMismatch, actualReqName = _.get(transaction, 'name'), actualReqDesc, mismatches = [], mismatchObj, reqUrl; if (!options.validateMetadata) { return callback(null, []); } // handling path templating in request url if any // convert all {anything} to {{anything}} reqUrl = this.fixPathVariablesInUrl(pathRoute.slice(pathRoute.indexOf('/'))); // convert all /{{one}}/{{two}} to /:one/:two // Doesn't touch /{{file}}.{{format}} reqUrl = this.sanitizeUrlPathParams(reqUrl, []).url; // description can be one of following two actualReqDesc = _.isObject(_.get(transaction, 'request.description')) ? _.get(transaction, 'request.description.content') : _.get(transaction, 'request.description'); expectedReqDesc = schemaPath.description; switch (options.requestNameSource) { case 'fallback' : { // operationId is usually camelcase or snake case expectedReqName = schemaPath.summary || utils.insertSpacesInName(schemaPath.operationId) || reqUrl; reqNameMismatch = (actualReqName !== expectedReqName); break; } case 'url' : { // actual value may differ in conversion as it uses local/global servers info to generate it // for now suggest actual path as request name expectedReqName = reqUrl; reqNameMismatch = !_.endsWith(actualReqName, reqUrl); break; } default : { expectedReqName = schemaPath[options.requestNameSource]; reqNameMismatch = (actualReqName !== expectedReqName); break; } } if (reqNameMismatch) { mismatchObj = { property: 'REQUEST_NAME', transactionJsonPath: transactionPathPrefix + '.name', schemaJsonPath: null, reasonCode: 'INVALID_VALUE', reason: 'The request name didn\'t match with specified schema' }; options.suggestAvailableFixes && (mismatchObj.suggestedFix = { key: 'name', actualValue: actualReqName || null, suggestedValue: expectedReqName }); mismatches.push(mismatchObj); } /** * Collection stores empty description as null, while OpenAPI spec can have empty string as description. * Hence We need to treat null and empty string as match. So check first if both schema and collection description * are not empty. _.isEmpty() returns true for null/undefined/''(empty string) * i.e. collection desc = null and schema desc = '', for this case no mismatch will occurr */ if ((!_.isEmpty(actualReqDesc) || !_.isEmpty(expectedReqDesc)) && (actualReqDesc !== expectedReqDesc)) { mismatchObj = { property: 'REQUEST_DESCRIPTION', transactionJsonPath: transactionPathPrefix + '.request.description', schemaJsonPath: null, reasonCode: 'INVALID_VALUE', reason: 'The request description didn\'t match with specified schema' }; options.suggestAvailableFixes && (mismatchObj.suggestedFix = { key: 'description', actualValue: actualReqDesc || null, suggestedValue: expectedReqDesc }); mismatches.push(mismatchObj); } return callback(null, mismatches); }, checkQueryParams(requestUrl, transactionPathPrefix, schemaPath, components, options, schemaCache, callback) { let parsedUrl = require('url').parse(requestUrl), schemaParams = _.filter(schemaPath.parameters, (param) => { return param.in === 'query'; }), requestQueryArray = [], requestQueryParams = [], resolvedSchemaParams = [], mismatchProperty = 'QUERYPARAM', securityParams, urlMalformedError; if (options.validationPropertiesToIgnore.includes(mismatchProperty)) { return callback(null, []); } if (!parsedUrl.query) { // null query params should be treated as lack of any params parsedUrl.query = ''; } requestQueryArray = parsedUrl.query.split('&'); _.each(requestQueryArray, (rqp) => { let parts = rqp.split('='), qKey, qVal; try { qKey = decodeURIComponent(parts[0]); qVal = decodeURIComponent(parts.slice(1).join('=')); } catch (err) { return (urlMalformedError = err); } if (qKey.length > 0) { requestQueryParams.push({ key: qKey, value: qVal }); } }); if (urlMalformedError) { return callback(urlMalformedError); } // filter out query params added by security schemes securityParams = _.map(this.getSecurityParams(_.get(components, 'components'), 'query'), 'name'); requestQueryParams = _.filter(requestQueryParams, (pQuery) => { return !_.includes(securityParams, pQuery.key); }); // resolve schema params // below will make sure for exploded params actual schema of property present in collection is present _.forEach(schemaParams, (param) => { let pathPrefix = param.pathPrefix, paramSchema = deref.resolveRefs(_.cloneDeep(param.schema), PARAMETER_SOURCE.REQUEST, components, schemaCache), { style, explode } = this.getParamSerialisationInfo(param, PARAMETER_SOURCE.REQUEST, components, schemaCache), isPropSeparable = _.includes(['form', 'deepObject'], style); if (isPropSeparable && paramSchema.type === 'array' && explode) { /** * avoid validation of complex array type param as OAS doesn't define serialisation * of Array with deepObject style */ if (!_.includes(['array', 'object'], _.get(paramSchema, 'items.type'))) { // add schema of corresponding items instead array resolvedSchemaParams.push(_.assign({}, param, { schema: _.get(paramSchema, 'items'), isResolvedParam: true })); } } else if (isPropSeparable && paramSchema.type === 'object' && explode) { // resolve all child params of parent param with deepObject style if (style === 'deepObject') { resolvedSchemaParams = _.concat(resolvedSchemaParams, this.extractChildParamSchema(paramSchema, param.name, { required: _.get(param, 'required'), pathPrefix, description: _.get(param, 'description') })); } else { // add schema of all properties instead entire object _.forEach(_.get(paramSchema, 'properties', {}), (propSchema, propName) => { resolvedSchemaParams.push({ name: propName, schema: propSchema, required: _.get(param, 'required') || false, description: _.get(param, 'description'), isResolvedParam: true, pathPrefix }); }); } } else { resolvedSchemaParams.push(param); } }); return async.map(requestQueryParams, (pQuery, cb) => { let mismatches = [], index = _.findIndex(requestQueryParams, pQuery), resolvedParamValue = pQuery.value; const schemaParam = _.find(resolvedSchemaParams, (param) => { return param.name === pQuery.key; }); if (!schemaParam) { // skip validation of complex array params if (this.isParamComplexArray(pQuery.key)) { return cb(null, mismatches); } if (options.showMissingInSchemaErrors) { mismatches.push({ property: mismatchProperty, transactionJsonPath: transactionPathPrefix + `[${index}]`, schemaJsonPath: null, reasonCode: 'MISSING_IN_SCHEMA', reason: `The query parameter ${pQuery.key} was not found in the schema` }); } return cb(null, mismatches); } // assign parameter example(s) as schema examples; this.assignParameterExamples(schemaParam); if (!schemaParam.isResolvedParam) { resolvedParamValue = this.deserialiseParamValue(schemaParam, pQuery.value, PARAMETER_SOURCE.REQUEST, components, schemaCache); } // query found in spec. check query's schema setTimeout(() => { if (!schemaParam.schema) { // no errors to show if there's no schema present in the spec return cb(null, []); } this.checkValueAgainstSchema(mismatchProperty, transactionPathPrefix + `[${index}].value`, pQuery.key, resolvedParamValue, schemaParam.pathPrefix + '[?(@.name==\'' + schemaParam.name + '\')]', schemaParam.schema, PARAMETER_SOURCE.REQUEST, components, options, schemaCache, cb ); }, 0); }, (err, res) => { let mismatches = [], mismatchObj; _.each(_.filter(resolvedSchemaParams, (q) => { return q.required; }), (qp) => { if (!_.find(requestQueryParams, (param) => { return param.key === qp.name; })) { // assign parameter example(s) as schema examples; this.assignParameterExamples(qp); mismatchObj = { property: mismatchProperty, transactionJsonPath: transactionPathPrefix, schemaJsonPath: qp.pathPrefix + '[?(@.name==\'' + qp.name + '\')]', reasonCode: 'MISSING_IN_REQUEST', reason: `The required query parameter "${qp.name}" was not found in the transaction` }; if (options.suggestAvailableFixes) { mismatchObj.suggestedFix = { key: qp.name, actualValue: null, suggestedValue: { key: qp.name, value: safeSchemaFaker(qp.schema || {}, 'example', PROCESSING_TYPE.VALIDATION, PARAMETER_SOURCE.REQUEST, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache, options.stackLimit), description: this.getParameterDescription(qp) } }; } mismatches.push(mismatchObj); } }); return callback(null, _.concat(_.flatten(res), mismatches)); }); }, /** * Gives mismtach for content type header for request/response * * @param {Array} headers - Transaction Headers * @param {String} transactionPathPrefix - Transaction Path to headers * @param {String} schemaPathPrefix - Schema path to content object * @param {Object} contentObj - Corresponding Schema content object * @param {String} mismatchProperty - Mismatch property (HEADER / RESPONSE_HEADER) * @param {*} options - OAS options, check lib/options.js for more * @returns {Array} found mismatch objects */ checkContentTypeHeader: function (headers, transactionPathPrefix, schemaPathPrefix, contentObj, mismatchProperty, options) { let mediaTypes = [], contentHeader, contentHeaderIndex, contentHeaderMediaType, suggestedContentHeader, hasComputedType, humanPropName = mismatchProperty === 'HEADER' ? 'header' : 'response header', mismatches = []; // get all media types present in content object _.forEach(_.keys(contentObj), (contentType) => { let contentMediaType = this.parseMediaType(contentType); mediaTypes.push(contentMediaType.type + '/' + contentMediaType.subtype); }); // prefer JSON > XML > Other media types for suggested header. _.forEach(mediaTypes, (mediaType) => { let headerFamily = this.getHeaderFamily(mediaType); if (headerFamily !== HEADER_TYPE.INVALID) { suggestedContentHeader = mediaType; hasComputedType = true; if (headerFamily === HEADER_TYPE.JSON) { return false; } } }); // if no JSON or XML, take whatever we have if (!hasComputedType && mediaTypes.length > 0) { suggestedContentHeader = mediaTypes[0]; hasComputedType = true; } // get content-type header and info _.forEach(headers, (header, index) => { if (_.toLower(header.key) === 'content-type') { let mediaType = this.parseMediaType(header.value); contentHeader = header; contentHeaderIndex = index; contentHeaderMediaType = mediaType.type + '/' + mediaType.subtype; return false; } }); // Schema body content has no media type objects if (!_.isEmpty(contentHeader) && _.isEmpty(mediaTypes)) { // ignore mismatch for default header (text/plain) added by conversion if (options.showMissingInSchemaErrors && _.toLower(contentHeaderMediaType) !== TEXT_PLAIN) { mismatches.push({ property: mismatchProperty, transactionJsonPath: transactionPathPrefix + `[${contentHeaderIndex}]`, schemaJsonPath: null, reasonCode: 'MISSING_IN_SCHEMA', // Reason for missing in schema suggests that certain media type in req/res body is not present reason: `The ${mismatchProperty === 'HEADER' ? 'request' : 'response'} body should have media type` + ` "${contentHeaderMediaType}"` }); } } // No request/response content-type header else if (_.isEmpty(contentHeader) && !_.isEmpty(mediaTypes)) { let mismatchObj = { property: mismatchProperty, transactionJsonPath: transactionPathPrefix, schemaJsonPath: schemaPathPrefix, reasonCode: 'MISSING_IN_REQUEST', reason: `The ${humanPropName} "Content-Type" was not found in the transaction` }; if (options.suggestAvailableFixes) { mismatchObj.suggestedFix = { key: 'Content-Type', actualValue: null, suggestedValue: { key: 'Content-Type', value: suggestedContentHeader } }; } mismatches.push(mismatchObj); } // Invalid type of header found else if (!_.isEmpty(contentHeader) && !_.includes(mediaTypes, contentHeaderMediaType)) { let mismatchObj = { property: mismatchProperty, transactionJsonPath: transactionPathPrefix + `[${contentHeaderIndex}].value`, schemaJsonPath: schemaPathPrefix, reasonCode: 'INVALID_TYPE', reason: `The ${humanPropName} "Content-Type" needs to be "${suggestedContentHeader}",` + ` but we found "${contentHeaderMediaType}" instead` }; if (options.suggestAvailableFixes) { mismatchObj.suggestedFix = { key: 'Content-Type', actualValue: contentHeader.value, suggestedValue: suggestedContentHeader }; } mismatches.push(mismatchObj); } return mismatches; }, checkRequestHeaders: function (headers, transactionPathPrefix, schemaPathPrefix, schemaPath, components, options, schemaCache, callback) { let schemaHeaders = _.filter(schemaPath.parameters, (param) => { return param.in === 'header'; }), // key name of headers which are added by security schemes securityHeaders = _.map(this.getSecurityParams(_.get(components, 'components'), 'header'), 'name'), // filter out headers for following cases reqHeaders = _.filter(headers, (header) => { // 1. which need explicit handling according to schema (other than parameters object) // 2. which are added by security schemes return !_.includes(IMPLICIT_HEADERS, _.toLower(_.get(header, 'key'))) && !_.includes(securityHeaders, header.key); }), mismatchProperty = 'HEADER'; if (options.validationPropertiesToIgnore.includes(mismatchProperty)) { return callback(null, []); } // 1. for each header, find relevant schemaPath property return async.map(reqHeaders, (pHeader, cb) => { let mismatches = [], resolvedParamValue, index = _.findIndex(headers, pHeader); // find actual index from collection request headers const schemaHeader = _.find(schemaHeaders, (header) => { return header.name === pHeader.key; }); if (!schemaHeader) { // no schema header found if (options.showMissingInSchemaErrors) { mismatches.push({ property: mismatchProperty, transactionJsonPath: transactionPathPrefix + `[${index}]`, schemaJsonPath: null, reasonCode: 'MISSING_IN_SCHEMA', reason: `The header ${pHeader.key} was not found in the schema` }); } return cb(null, mismatches); } // assign parameter example(s) as schema examples; this.assignParameterExamples(schemaHeader); resolvedParamValue = this.deserialiseParamValue(schemaHeader, pHeader.value, PARAMETER_SOURCE.REQUEST, components, schemaCache); // header found in spec. check header's schema setTimeout(() => { if (!schemaHeader.schema) { // no errors to show if there's no schema present in the spec return cb(null, []); } this.checkValueAgainstSchema(mismatchProperty, transactionPathPrefix + `[${index}].value`, pHeader.key, resolvedParamValue, schemaHeader.pathPrefix + '[?(@.name==\'' + schemaHeader.name + '\')]', schemaHeader.schema, PARAMETER_SOURCE.REQUEST, components, options, schemaCache, cb ); }, 0); }, (err, res) => { let mismatches = [], mismatchObj, contentHeaderMismatches = this.checkContentTypeHeader(headers, transactionPathPrefix, schemaPathPrefix + '.requestBody.content', _.get(schemaPath, 'requestBody.content'), mismatchProperty, options); _.each(_.filter(schemaHeaders, (h) => { // exclude non-required and implicit header from further validation return h.required && !_.includes(IMPLICIT_HEADERS, _.toLower(h.name)); }), (header) => { if (!_.find(reqHeaders, (param) => { return param.key === header.name; })) { // assign parameter example(s) as schema examples; this.assignParameterExamples(header); mismatchObj = { property: mismatchProperty, transactionJsonPath: transactionPathPrefix, schemaJsonPath: header.pathPrefix + '[?(@.name==\'' + header.name + '\')]', reasonCode: 'MISSING_IN_REQUEST', reason: `The required header "${header.name}" was not found in the transaction` }; if (options.suggestAvailableFixes) { mismatchObj.suggestedFix = { key: header.name, actualValue: null, suggestedValue: { key: header.name, value: safeSchemaFaker(header.schema || {}, 'example', PROCESSING_TYPE.VALIDATION, PARAMETER_SOURCE.REQUEST, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache, options.stackLimit), description: this.getParameterDescription(header) } }; } mismatches.push(mismatchObj); } }); return callback(null, _.concat(contentHeaderMismatches, _.flatten(res), mismatches)); }); }, checkResponseHeaders: function (schemaResponse, headers, transactionPathPrefix, schemaPathPrefix, components, options, schemaCache, callback) { // 0. Need to find relevant response from schemaPath.responses let schemaHeaders, // filter out headers which need explicit handling according to schema (other than parameters object) resHeaders = _.filter(headers, (header) => { return !_.includes(IMPLICIT_HEADERS, _.toLower(_.get(header, 'key'))); }), mismatchProperty = 'RESPONSE_HEADER'; if (options.validationPropertiesToIgnore.includes(mismatchProperty)) { return callback(null, []); } if (!schemaResponse) { // no default response found, we can't call it a mismatch return callback(null, []); } schemaHeaders = schemaResponse.headers; return async.map(resHeaders, (pHeader, cb) => { let mismatches = [], index = _.findIndex(headers, pHeader); // find actual index from collection response headers const schemaHeader = _.get(schemaHeaders, pHeader.key); if (!schemaHeader) { // no schema header found if (options.showMissingInSchemaErrors) { mismatches.push({ property: mismatchProperty, transactionJsonPath: transactionPathPrefix + `[${index}]`, schemaJsonPath: schemaPathPrefix + '.headers', reasonCode: 'MISSING_IN_SCHEMA', reason: `The header ${pHeader.key} was not found in the schema` }); } return cb(null, mismatches); } // assign parameter example(s) as schema examples; this.assignParameterExamples(schemaHeader); // header found in spec. check header's schema setTimeout(() => { if (!schemaHeader.schema) { // no errors to show if there's no schema present in the spec return cb(null, []); } return this.checkValueAgainstSchema(mismatchProperty, transactionPathPrefix + `[${index}].value`, pHeader.key, pHeader.value, schemaPathPrefix + '.headers[' + pHeader.key + ']', schemaHeader.schema, PARAMETER_SOURCE.RESPONSE, components, options, schemaCache, cb ); }, 0); }, (err, res) => { let mismatches = [], mismatchObj, contentHeaderMismatches = this.checkContentTypeHeader(headers, transactionPathPrefix, schemaPathPrefix + '.content', _.get(schemaResponse, 'content'), mismatchProperty, options); _.each(_.filter(schemaHeaders, (h, hName) => { // exclude empty headers fron validation if (_.isEmpty(h)) { return false; } h.name = hName; // exclude non-required and implicit header from further validation return h.required && !_.includes(IMPLICIT_HEADERS, _.toLower(hName)); }), (header) => { if (!_.find(resHeaders, (param) => { return param.key === header.name; })) { // assign parameter example(s) as schema examples; this.assignParameterExamples(header); mismatchObj = { property: mismatchProperty, transactionJsonPath: transactionPathPrefix, schemaJsonPath: schemaPathPrefix + '.headers[\'' + header.name + '\']', reasonCode: 'MISSING_IN_REQUEST', reason: `The required response header "${header.name}" was not found in the transaction` }; if (options.suggestAvailableFixes) { mismatchObj.suggestedFix = { key: header.name, actualValue: null, suggestedValue: { key: header.name, value: safeSchemaFaker(header.schema || {}, 'example', PROCESSING_TYPE.VALIDATION, PARAMETER_SOURCE.REQUEST, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache, options.stackLimit), description: this.getParameterDescription(header) } }; } mismatches.push(mismatchObj); } }); callback(null, _.concat(contentHeaderMismatches, _.flatten(res), mismatches)); }); }, // Only application/json and application/x-www-form-urlencoded is validated for now checkRequestBody: function (requestBody, transactionPathPrefix, schemaPathPrefix, schemaPath, components, options, schemaCache, callback) { // check for body modes let jsonSchemaBody, jsonContentType, mismatchProperty = 'BODY'; if (options.validationPropertiesToIgnore.includes(mismatchProperty)) { return callback(null, []); } // resolve $ref in requestBody object if present if (!_.isEmpty(_.get(schemaPath, 'requestBody.$ref'))) { schemaPath.requestBody = this.getRefObject(schemaPath.requestBody.$ref, components, options); } // get valid json content type jsonContentType = this.getJsonContentType(_.get(schemaPath, 'requestBody.content', {})); jsonSchemaBody = _.get(schemaPath, ['requestBody', 'content', jsonContentType, 'schema']); if (requestBody && requestBody.mode === 'raw' && jsonSchemaBody) { setTimeout(() => { return this.checkValueAgainstSchema(mismatchProperty, transactionPathPrefix, null, // no param name for the request body requestBody.raw, schemaPathPrefix + '.requestBody.content[' + jsonContentType + '].schema', jsonSchemaBody, PARAMETER_SOURCE.REQUEST, components, _.extend({}, options, { shortValidationErrors: true }), schemaCache, callback ); }, 0); } else if (requestBody && requestBody.mode === 'urlencoded') { let urlencodedBodySchema = _.get(schemaPath, ['requestBody', 'content', URLENCODED, 'schema']), resolvedSchemaParams = [], pathPrefix = `${schemaPathPrefix}.requestBody.content[${URLENCODED}].schema`; urlencodedBodySchema = deref.resolveRefs(urlencodedBodySchema, PARAMETER_SOURCE.REQUEST, components, schemaCache.schemaResolutionCache, PROCESSING_TYPE.VALIDATION, 'example', 0, {}, options.stackLimit); // resolve each property as separate param similar to query parmas _.forEach(_.get(urlencodedBodySchema, 'properties'), (propSchema, propName) => { let resolvedProp = { name: propName, schema: propSchema, in: 'query', // serialization follows same behaviour as query params description: _.get(propSchema, 'description') || '' }, encodingValue = _.get(schemaPath, ['requestBody', 'content', URLENCODED, 'encoding', propName]), pSerialisationInfo, isPropSeparable; if (_.isObject(encodingValue)) { _.has(encodingValue, 'style') && (resolvedProp.style = encodingValue.style); _.has(encodingValue, 'explode') && (resolvedProp.explode = encodingValue.explode); } if (_.includes(_.get(urlencodedBodySchema, 'required'), propName)) { resolvedProp.required = true; } pSerialisationInfo = this.getParamSerialisationInfo(resolvedProp, PARAMETER_SOURCE.REQUEST, components, schemaCache); isPropSeparable = _.includes(['form', 'deepObject'], pSerialisationInfo.style); if (isPropSeparable && propSchema.type === 'array' && pSerialisationInfo.explode) { /** * avoid validation of complex array type param as OAS doesn't define serialisation * of Array with deepObject style */ if (!_.includes(['array', 'object'], _.get(propSchema, 'items.type'))) { // add schema of corresponding items instead array resolvedSchemaParams.push(_.assign({}, resolvedProp, { schema: _.get(propSchema, 'items'), isResolvedParam: true })); } } else if (isPropSeparable && propSchema.type === 'object' && pSerialisationInfo.explode) { // resolve all child params of parent param with deepObject style if (pSerialisationInfo.style === 'deepObject') { resolvedSchemaParams = _.concat(resolvedSchemaParams, this.extractChildParamSchema(propSchema, propName, { required: resolvedProp.required || false, description: resolvedProp.description })); } else { // add schema of all properties instead entire object _.forEach(_.get(propSchema, 'properties', {}), (value, key) => { resolvedSchemaParams.push({ name: key, schema: value, isResolvedParam: true, required: resolvedProp.required || false, description: resolvedProp.description }); }); } } else { resolvedSchemaParams.push(resolvedProp); } }); return async.map(requestBody.urlencoded, (uParam, cb) => { let mismatches = [], index = _.findIndex(requestBody.urlencoded, uParam), resolvedParamValue = uParam.value; const schemaParam = _.find(resolvedSchemaParams, (param) => { return param.name === uParam.key; }); if (!schemaParam) { // skip validation of complex array params if (this.isParamComplexArray(uParam.key)) { return cb(null, mismatches); } if (options.showMissingInSchemaErrors) { mismatches.push({ property: mismatchProperty, transactionJsonPath: transactionPathPrefix + `.urlencoded[${index}]`, schemaJsonPath: null, reasonCode: 'MISSING_IN_SCHEMA', reason: `The Url Encoded body param "${uParam.key}" was not found in the schema` }); } return cb(null, mismatches); } if (!schemaParam.isResolvedParam) { resolvedParamValue = this.deserialiseParamValue(schemaParam, uParam.value, PARAMETER_SOURCE.REQUEST, components, schemaCache); } // store value of transaction to use in mismatch object schemaParam.actualValue = uParam.value; // param found in spec. check param's schema setTimeout(() => { if (!schemaParam.schema) { // no errors to show if there's no schema present in the spec return cb(null, []); } this.checkValueAgainstSchema(mismatchProperty, transactionPathPrefix + `.urlencoded[${index}].value`, uParam.key, resolvedParamValue, pathPrefix + '.properties[' + schemaParam.name + ']', schemaParam.schema, PARAMETER_SOURCE.REQUEST, components, options, schemaCache, cb ); }, 0); }, (err, res) => { let mismatches = [], mismatchObj, // fetches property name from schem path getPropNameFromSchemPath = (schemaPath) => { let regex = /\.properties\[(.+)\]/gm; return _.last(regex.exec(schemaPath)); }; // update actual value and suggested value from JSON to serialized strings _.forEach(_.flatten(res), (mismatchObj) => { if (!_.isEmpty(mismatchObj)) { let propertyName = getPropNameFromSchemPath(mismatchObj.schemaJsonPath), schemaParam = _.find(resolvedSchemaParams, (param) => { return param.name === propertyName; }), serializedParamValue; if (schemaParam) { // serialize param value (to be used in suggested value) serializedParamValue = _.get(this.convertParamsWithStyle(schemaParam, _.get(mismatchObj, 'suggestedFix.suggestedValue'), PARAMETER_SOURCE.REQUEST, components, schemaCache, options), '[0].value'); _.set(mismatchObj, 'suggestedFix.actualValue', schemaParam.actualValue); _.set(mismatchObj, 'suggestedFix.suggestedValue', serializedParamValue); } } }); _.each(resolvedSchemaParams, (uParam) => { // report mismatches only for required properties if (!_.find(requestBody.urlencoded, (param) => { return param.key === uParam.name; }) && uParam.required) { mismatchObj = { property: mismatchProperty, transactionJsonPath: transactionPathPrefix + '.urlencoded', schemaJsonPath: pathPrefix + '.properties[' + uParam.name + ']', reasonCode: 'MISSING_IN_REQUEST', reason: `The Url Encoded body param "${uParam.name}" was not found in the transaction` }; if (options.suggestAvailableFixes) { mismatchObj.suggestedFix = { key: uParam.name, actualValue: null, suggestedValue: { key: uParam.name, value: safeSchemaFaker(uParam.schema || {}, 'example', PROCESSING_TYPE.VALIDATION, PARAMETER_SOURCE.REQUEST, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache, options.stackLimit), description: this.getParameterDescription(uParam) } }; } mismatches.push(mismatchObj); } }); return callback(null, _.concat(_.flatten(res), mismatches)); }); } else { return callback(null, []); } }, checkResponseBody: function (schemaResponse, body, transactionPathPrefix, schemaPathPrefix, components, options, schemaCache, callback) { let schemaContent, jsonContentType, mismatchProperty = 'RESPONSE_BODY'; if (options.validationPropertiesToIgnore.includes(mismatchProperty)) { return callback(null, []); } // get valid json content type jsonContentType = this.getJsonContentType(_.get(schemaResponse, 'content', {})); schemaContent = _.get(schemaResponse, ['content', jsonContentType, 'schema']); if (!schemaContent) { // no specific or default response with application/json // return callback(null, [{ // property: mismatchProperty, // transactionJsonPath: transactionPathPrefix, // schemaJsonPath: null, // reasonCode: 'BODY_SCHEMA_NOT_FOUND', // reason: 'No JSON schema found for this response' // }]); // cannot show mismatches if the schema didn't have any application/JSON response return callback(null, []); } setTimeout(() => { return this.checkValueAgainstSchema(mismatchProperty, transactionPathPrefix, null, // no param name for the response body body, schemaPathPrefix + '.content[' + jsonContentType + '].schema', schemaContent, PARAMETER_SOURCE.RESPONSE, components, _.extend({}, options, { shortValidationErrors: true }), schemaCache, callback ); }, 0); }, checkResponses: function (responses, transactionPathPrefix, schemaPathPrefix, schemaPath, components, options, schemaCache, cb) { // responses is an array of repsonses recd. for one Postman request // we've already determined the schemaPath against which all responses need to be validated // loop through all responses // for each response, find the appropriate response from schemaPath, and then validate response body and headers async.map(responses, (response, responseCallback) => { let thisResponseCode = response.code, thisSchemaResponse = _.get(schemaPath, ['responses', thisResponseCode]), responsePathPrefix = thisResponseCode; // find this code from the schemaPath if (!thisSchemaResponse) { // could not find an appropriate response for this code. check default? thisSchemaResponse = _.get(schemaPath, ['responses', 'default']); responsePathPrefix = 'default'; } // resolve $ref in response object if present if (!_.isEmpty(_.get(thisSchemaResponse, '$ref'))) { thisSchemaResponse = this.getRefObject(thisSchemaResponse.$ref, components, options); } // resolve $ref in all header objects if present _.forEach(_.get(thisSchemaResponse, 'headers'), (header) => { if (_.has(header, '$ref')) { _.assign(header, this.getRefObject(header.$ref, components, options)); _.unset(header, '$ref'); } }); if (!thisSchemaResponse) { // still didn't find a response responseCallback(null); } else { // check headers and body async.parallel({ headers: (cb) => { this.checkResponseHeaders(thisSchemaResponse, response.header, transactionPathPrefix + '[' + response.id + '].header', schemaPathPrefix + '.responses.' + responsePathPrefix, components, options, schemaCache, cb); }, body: (cb) => { // assume it's JSON at this point this.checkResponseBody(thisSchemaResponse, response.body, transactionPathPrefix + '[' + response.id + '].body', schemaPathPrefix + '.responses.' + responsePathPrefix, components, options, schemaCache, cb); } }, (err, result) => { return responseCallback(null, { id: response.id, matched: (result.body.length === 0 && result.headers.length === 0), mismatches: result.body.concat(result.headers) }); }); } }, (err, result) => { var retVal = _.keyBy(_.reject(result, (ai) => { return !ai; }), 'id'); return cb(null, retVal); }); }, /** * @param {string} postmanPath - parsed path (exclude host and params) from the Postman request * @param {string} schemaPath - schema path from the OAS spec (exclude servers object) * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. * @returns {*} score + match + pathVars - higher score - better match. null - no match */ getPostmanUrlSchemaMatchScore: function (postmanPath, schemaPath, options) { var postmanPathArr = _.reject(postmanPath.split('/'), (segment) => { return segment === ''; }), schemaPathArr = _.reject(schemaPath.split('/'), (segment) => { return segment === ''; }), matchedPathVars = null, maxScoreFound = -Infinity, anyMatchFound = false, fixedMatchedSegments, variableMatchedSegments, postmanPathSuffixes = []; // get array with all suffixes of postmanPath // if postmanPath = {{url}}/a/b, the suffix array is [ [{{url}}, a, b] , [a, b] , [b]] for (let i = postmanPathArr.length; i > 0; i--) { // i will be 3, 2, 1 postmanPathSuffixes.push(postmanPathArr.slice(-i)); break; // we only want one item in the suffixes array for now } // for each suffx, calculate score against the schemaPath // the schema<>postman score is the sum _.each(postmanPathSuffixes, (pps) => { let suffixMatchResult = this.getPostmanUrlSuffixSchemaScore(pps, schemaPathArr, options); if (suffixMatchResult.match && suffixMatchResult.score > maxScoreFound) { maxScoreFound = suffixMatchResult.score; matchedPathVars = suffixMatchResult.pathVars; // No. of fixed segment matches between schema and postman url path fixedMatchedSegments = suffixMatchResult.fixedMatchedSegments; // No. of variable segment matches between schema and postman url path variableMatchedSegments = suffixMatchResult.variableMatchedSegments; anyMatchFound = true; } }); // handle root path '/' if (postmanPath === '/' && schemaPath === '/') { anyMatchFound = true; maxScoreFound = 1; // assign max possible score matchedPathVars = []; // no path variables present fixedMatchedSegments = 0; variableMatchedSegments = 0; } if (anyMatchFound) { return { match: true, score: maxScoreFound, pathVars: matchedPathVars, fixedMatchedSegments, variableMatchedSegments }; } return { match: false }; }, /** * @param {*} pmSuffix - Collection request's path suffix array * @param {*} schemaPath - schema operation's path suffix array * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. * @returns {*} score - null of no match, int for match. higher value indicates better match * You get points for the number of URL segments that match * You are penalized for the number of schemaPath segments that you skipped */ getPostmanUrlSuffixSchemaScore: function (pmSuffix, schemaPath, options) { let mismatchFound = false, variables = [], minLength = Math.min(pmSuffix.length, schemaPath.length), sMax = schemaPath.length - 1, pMax = pmSuffix.length - 1, matchedSegments = 0, // No. of fixed segment matches between schema and postman url path fixedMatchedSegments = 0, // No. of variable segment matches between schema and postman url path variableMatchedSegments = 0, // checks if schema segment provided is path variable isSchemaSegmentPathVar = (segment) => { return segment.startsWith('{') && segment.endsWith('}') && // check that only one path variable is present as collection path variable can contain only one var segment.indexOf('}') === segment.lastIndexOf('}'); }; if (options.strictRequestMatching && pmSuffix.length !== schemaPath.length) { return { match: false, score: null, pathVars: [] }; } // start from the last segment of both // segments match if the schemaPath segment is {..} or the postmanPathStr is : or {{anything}} // for (let i = pmSuffix.length - 1; i >= 0; i--) { for (let i = 0; i < minLength; i++) { let schemaFixedParts = this.getFixedPartsFromPathSegment(schemaPath[sMax - i], 'schema'), collectionFixedParts = this.getFixedPartsFromPathSegment(pmSuffix[pMax - i], 'collection'); if ( (_.isEqual(schemaFixedParts, collectionFixedParts)) || // exact fixed parts match (isSchemaSegmentPathVar(schemaPath[sMax - i])) || // schema segment is a pathVar (pmSuffix[pMax - i].startsWith(':')) || // postman segment is a pathVar (this.isPmVariable(pmSuffix[pMax - i])) // postman segment is an env/collection var ) { // for variable match increase variable matched segments count (used for determining order for multiple matches) if ( (isSchemaSegmentPathVar(schemaPath[sMax - i])) && // schema segment is a pathVar ((pmSuffix[pMax - i].startsWith(':')) || // postman segment is a pathVar (this.isPmVariable(pmSuffix[pMax - i]))) // postman segment is an env/collection var ) { variableMatchedSegments++; } // for exact match increase fix matched segments count (used for determining order for multiple matches) else if (_.isEqual(schemaFixedParts, collectionFixedParts)) { fixedMatchedSegments++; } // add a matched path variable only if the schema one was a pathVar and only one path variable is in segment if (isSchemaSegmentPathVar(schemaPath[sMax - i])) { variables.push({ key: schemaPath[sMax - i].substring(1, schemaPath[sMax - i].length - 1), value: pmSuffix[pMax - i] }); } matchedSegments++; } else { // there was one segment for which there was no mismatch mismatchFound = true; break; } } if (!mismatchFound) { return { match: true, // schemaPath endsWith postman path suffix // score is length of the postman path array + schema array - length difference // the assumption is that a longer path matching a longer path is a higher score, with // penalty for any length difference // schemaPath will always be > postmanPathSuffix because SchemaPath ands with pps score: ((2 * matchedSegments) / (schemaPath.length + pmSuffix.length)), fixedMatchedSegments, variableMatchedSegments, pathVars: _.reverse(variables) // keep index in order of left to right }; } return { match: false, score: null, pathVars: [] }; }, /** * This function extracts suggested value from faked value at Ajv mismatch path (dataPath) * * @param {*} fakedValue Faked value by jsf * @param {*} actualValue Actual value in transaction * @param {*} ajvValidationErrorObj Ajv error for which fix is suggested * @returns {*} Suggested Value */ getSuggestedValue: function (fakedValue, actualValue, ajvValidationErrorObj) { var suggestedValue, tempSuggestedValue, dataPath = ajvValidationErrorObj.dataPath || '', targetActualValue, targetFakedValue; // discard the leading '.' if it exists if (dataPath[0] === '.') { dataPath = dataPath.slice(1); } targetActualValue = this.getPathValue(actualValue, dataPath, {}); targetFakedValue = this.getPathValue(fakedValue, dataPath, {}); switch (ajvValidationErrorObj.keyword) { // to do: check for minItems, maxItems case 'minProperties': suggestedValue = _.assign({}, targetActualValue, _.pick(targetFakedValue, _.difference(_.keys(targetFakedValue), _.keys(targetActualValue)))); break; case 'maxProperties': suggestedValue = _.pick(targetActualValue, _.intersection(_.keys(targetActualValue), _.keys(targetFakedValue))); break; case 'required': suggestedValue = _.assign({}, targetActualValue, _.pick(targetFakedValue, ajvValidationErrorObj.params.missingProperty)); break; case 'minItems': suggestedValue = _.concat(targetActualValue, _.slice(targetFakedValue, targetActualValue.length)); break; case 'maxItems': suggestedValue = _.slice(targetActualValue, 0, ajvValidationErrorObj.params.limit); break; case 'uniqueItems': tempSuggestedValue = _.cloneDeep(targetActualValue); tempSuggestedValue[ajvValidationErrorObj.params.j] = _.last(targetFakedValue); suggestedValue = tempSuggestedValue; break; // Keywords: minLength, maxLength, format, minimum, maximum, type, multipleOf, pattern default: suggestedValue = this.getPathValue(fakedValue, dataPath, null); break; } return suggestedValue; }, /** * @param {*} schema OpenAPI spec * @param {Array} matchedEndpoints - All matched endpoints * @param {object} components - components defined in the OAS spec. These are used to * 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 * @returns {Array} - Array of all MISSING_ENDPOINT objects */ getMissingSchemaEndpoints: function (schema, matchedEndpoints, components, options, schemaCache) { let endpoints = [], schemaPaths = schema.paths, rootCollectionVariables, schemaJsonPath; // collection variables generated for resolving for baseUrl and variables rootCollectionVariables = this.convertToPmCollectionVariables( schema.baseUrlVariables, 'baseUrl', schema.baseUrl ); _.forEach(schemaPaths, (schemaPathObj, schemaPath) => { _.forEach(_.keys(schemaPathObj), (pathKey) => { schemaJsonPath = `$.paths[${schemaPath}].${_.toLower(pathKey)}`; if (METHODS.includes(pathKey) && !matchedEndpoints.includes(schemaJsonPath)) { let mismatchObj = { property: 'ENDPOINT', transactionJsonPath: null, schemaJsonPath, reasonCode: 'MISSING_ENDPOINT', reason: `The endpoint "${_.toUpper(pathKey)} ${schemaPath}" is missing in collection`, endpoint: _.toUpper(pathKey) + ' ' + schemaPath }; if (options.suggestAvailableFixes) { let operationItem = _.get(schemaPathObj, pathKey) || {}, convertedRequest, variables = rootCollectionVariables, path = schemaPath, request; // add common parameters of path level operationItem.parameters = this.getRequestParams(operationItem.parameters, _.get(schemaPathObj, 'parameters'), components, options); // discard the leading slash, if it exists if (path[0] === '/') { path = path.substring(1); } // override root level collection variables (baseUrl and vars) with path level server url and vars if exists // storing common path/collection vars from the server object at the path item level if (!_.isEmpty(_.get(schemaPathObj, 'servers'))) { let pathLevelServers = schemaPathObj.servers; // add path level server object's URL as collection variable variables = this.convertToPmCollectionVariables( pathLevelServers[0].variables, // these are path variables in the server block this.fixPathVariableName(path), // the name of the variable this.fixPathVariablesInUrl(pathLevelServers[0].url) ); } request = { name: operationItem.summary || operationItem.description, method: pathKey, path: schemaPath[0] === '/' ? schemaPath.substring(1) : schemaPath, properties: operationItem, type: 'item', servers: _.isEmpty(_.get(schemaPathObj, 'servers')) }; // convert request to collection item and store collection variables convertedRequest = this.convertRequestToItem(schema, request, components, options, schemaCache, variables); mismatchObj.suggestedFix = { key: pathKey, actualValue: null, // Not adding colloection variables for now suggestedValue: { request: convertedRequest, variables: _.values(variables) } }; } endpoints.push(mismatchObj); } }); }); return endpoints; } };