UNPKG

openapi-to-postmanv2

Version:

Convert a given OpenAPI specification to Postman Collection v2.0

1,354 lines (1,211 loc) 105 kB
/* eslint-disable require-jsdoc */ // TODO: REMOVE THIS ☝🏻 const _ = require('lodash'), { Header } = require('postman-collection/lib/collection/header'), { QueryParam } = require('postman-collection/lib/collection/query-param'), { Url } = require('postman-collection/lib/collection/url'), { Variable } = require('postman-collection/lib/collection/variable'), async = require('async'), crypto = require('crypto'), schemaFaker = require('../assets/json-schema-faker.js'), xmlFaker = require('./xmlSchemaFaker.js'), utils = require('./utils'), { resolveSchema, resolveRefFromSchema, resolvePostmanRequest, resolveResponseForPostmanRequest } = require('./schemaUtils'), concreteUtils = require('../lib/30XUtils/schemaUtils30X'), ajvValidationError = require('../lib/ajValidation/ajvValidationError'), { validateSchema } = require('../lib/ajValidation/ajvValidation'), { formatDataPath, checkIsCorrectType, isKnownType, getServersPathVars } = require('../lib/common/schemaUtilsCommon.js'), { findMatchingRequestFromSchema, isPmVariable } = require('./requestMatchingUtils'), // common global constants 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', TEXT_PLAIN = 'text/plain', PARAMETER_SOURCE = { REQUEST: 'REQUEST', RESPONSE: 'RESPONSE' }, HEADER_TYPE = { JSON: 'json', XML: 'xml', INVALID: 'invalid' }, 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' ], OAS_NOT_SUPPORTED = '<Error: Not supported in OAS>', /** * @sujay: this needs to be a better global level setting * before we start using the v2 validations everywhere. */ VALIDATE_OPTIONAL_PARAMS = true; // 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: array) useDefaultValue: true, ignoreMissingRefs: true, avoidExampleItemsLength: true, // option to avoid validating type array schema example's minItems and maxItems props. failOnInvalidFormat: false }); /** * * @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'); } /** * Provides context that's needed for V2 resolveSchema() interface * * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. * @param {object} components - components defined in the OAS spec. These are used to * resolve references while generating params * @returns {object} - Provides default context */ function getDefaultContext (options, components = {}) { return { concreteUtils, schemaCache: {}, computedOptions: options, schemaValidationCache: new Map(), specComponents: { components: components.components } }; } /** * Verifies if the deprecated operations should be added * * @param {object} operation - openAPI operation object * @param {object} options - a standard list of options that's globally passed around. Check options.js for more. * @returns {boolean} whether to add or not the deprecated operation */ function shouldAddDeprecatedOperation (operation, options) { if (typeof operation === 'object') { return !operation.deprecated || (operation.deprecated === true && options.includeDeprecated === true); } return false; } /** * Safe wrapper for schemaFaker that resolves references and * removes things that might make schemaFaker crash * @param {Object} context - Required context from related SchemaPack function * @param {*} oldSchema the schema to fake * 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 {object} schemaCache - object storing schemaFaker and schemaResolution caches * @returns {object} fakedObject */ function safeSchemaFaker (context, oldSchema, resolveFor, parameterSourceOption, components, schemaFormat, schemaCache) { let prop, key, resolvedSchema, fakedSchema, schemaFakerCache = _.get(schemaCache, 'schemaFakerCache', {}), concreteUtils = context.concreteUtils; const options = context.computedOptions, resolveTo = _.get(options, 'parametersResolution', 'example'), indentCharacter = options.indentCharacter; /** * Schema is cloned here as resolveSchema() when called for CONVERSION use cases, will mutate schema in certain way. * i.e. For array it'll add maxItems = 2. This should be avoided as we'll again be needing non-mutated schema * in further VALIDATION use cases as needed. */ resolvedSchema = resolveSchema(context, _.cloneDeep(oldSchema), { resolveFor: _.toLower(PROCESSING_TYPE.CONVERSION), isResponseSchema: parameterSourceOption === PARAMETER_SOURCE.RESPONSE }); resolvedSchema = concreteUtils.fixExamplesByVersion(resolvedSchema); key = JSON.stringify(resolvedSchema); if (resolveTo === 'schema') { key = 'resolveToSchema ' + key; schemaFaker.option({ useExamplesValue: false, useDefaultValue: true }); } else if (resolveTo === 'example') { key = 'resolveToExample ' + key; schemaFaker.option({ useExamplesValue: true }); } if (resolveFor === PROCESSING_TYPE.VALIDATION) { schemaFaker.option({ avoidExampleItemsLength: false }); } if (schemaFormat === 'xml') { key += ' schemaFormatXML'; } else { key += ' schemaFormatDEFAULT'; } 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, null, _.get(schemaCache, 'schemaValidationCache')); schemaFakerCache[key] = fakedSchema; return fakedSchema; } catch (e) { console.warn( 'Error faking a schema. Not faking this schema. Schema:', resolvedSchema, 'Error', e ); return null; } } /** 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. */ function sanitizeUrlPathParams (reqUrl, pathVars) { var matches, collectionVars = []; // converts all the of the following: // /{{path}}/{{file}}.{{format}}/{{hello}} => /:path/{{file}}.{{format}}/:hello matches = utils.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 = utils.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 }; } /** * * @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) */ function checkMetadata (transaction, transactionPathPrefix, schemaPath, pathRoute, options, callback) { let expectedReqName, reqNameMismatch, actualReqName = _.get(transaction, 'name'), trimmedReqName, mismatches = [], mismatchObj, reqUrl; if (!options.validateMetadata) { return callback(null, []); } // only validate string upto 255 character as longer name results in issues while updation trimmedReqName = utils.trimRequestName(actualReqName); // handling path templating in request url if any // convert all {anything} to {{anything}} reqUrl = utils.fixPathVariablesInUrl(pathRoute.slice(pathRoute.indexOf('/'))); // convert all /{{one}}/{{two}} to /:one/:two // Doesn't touch /{{file}}.{{format}} reqUrl = sanitizeUrlPathParams(reqUrl, []).url; switch (options.requestNameSource) { case 'fallback' : { // operationId is usually camelcase or snake case expectedReqName = schemaPath.summary || utils.insertSpacesInName(schemaPath.operationId) || schemaPath.description || reqUrl; expectedReqName = utils.trimRequestName(expectedReqName); reqNameMismatch = (trimmedReqName !== 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; expectedReqName = utils.trimRequestName(expectedReqName); reqNameMismatch = !_.endsWith(actualReqName, reqUrl); break; } default : { expectedReqName = schemaPath[options.requestNameSource]; expectedReqName = utils.trimRequestName(expectedReqName); reqNameMismatch = (trimmedReqName !== 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); } /** * Note: Request Description validation/syncing is removed with v2 interface */ return callback(null, mismatches); } /** * Given parameter objects, it assigns example/examples of parameter object as schema example. * * @param {Object} parameter - parameter object * @returns {null} - null */ function assignParameterExamples (parameter) { let example = _.get(parameter, 'example'), examples = _.values(_.get(parameter, 'examples')); if (example !== undefined) { _.set(parameter, 'schema.example', example); } else if (examples) { let exampleToUse = _.get(examples, '[0].value'); !_.isUndefined(exampleToUse) && (_.set(parameter, 'schema.example', exampleToUse)); } } /** * 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 */ function getParameterDescription (parameter) { if (!_.isObject(parameter)) { return ''; } return (parameter.required ? '(Required) ' : '') + (parameter.description || '') + (parameter.enum ? ' (This can only be one of ' + parameter.enum + ')' : ''); } /** * 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} options - a standard list of options that's globally passed around. Check options.js for more. * @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) * } */ function getParamSerialisationInfo (param, parameterSource, components, options) { var paramName = _.get(param, 'name'), paramSchema = resolveSchema(getDefaultContext(options, components), _.cloneDeep(param.schema), { resolveFor: PROCESSING_TYPE.VALIDATION, isResponseSchema: parameterSource === PARAMETER_SOURCE.RESPONSE }), 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 divided 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 divided into different key-value pairs explode = true; break; default: break; } return { style, explode, startValue, propSeparator, keyValueSeparator, isExplodable }; } /** * This function 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} options - a standard list of options that's globally passed around. Check options.js for more. * @param {Object} schemaCache - object storing schemaFaker and schemaResolution caches * @returns {*} - deserialises parameter value */ function deserialiseParamValue (param, paramValue, parameterSource, components, options) { var constructedValue, paramSchema = resolveSchema(getDefaultContext(options, components), _.cloneDeep(param.schema), { resolveFor: PROCESSING_TYPE.VALIDATION, isResponseSchema: parameterSource === PARAMETER_SOURCE.RESPONSE }), 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 } = getParamSerialisationInfo(param, parameterSource, components, options); // 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; } /** * 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 */ function getPathValue (sourceValue, dataPath, fallback) { return (dataPath === '' ? sourceValue : _.get(sourceValue, dataPath, fallback)); } /** * 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 */ function getSuggestedValue (fakedValue, actualValue, ajvValidationErrorObj) { var suggestedValue, tempSuggestedValue, dataPath = formatDataPath(ajvValidationErrorObj.instancePath || ''), targetActualValue, targetFakedValue; // discard the leading '.' if it exists if (dataPath[0] === '.') { dataPath = dataPath.slice(1); } targetActualValue = getPathValue(actualValue, dataPath, {}); targetFakedValue = 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 = getPathValue(fakedValue, dataPath, null); break; } return suggestedValue; } /** * Tests whether given parameter is of complex array type from param key * * @param {*} paramKey - Parmaeter key that is to be tested * @returns {Boolean} - result */ function isParamComplexArray (paramKey) { // this checks if parameter key numbered element (i.e. itemArray[1] is complex array param) let regex = /\[[\d]+\]/gm; return regex.test(paramKey); } /** * 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 */ function parseMediaType (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 }; } /** * Get the format of content type header * @param {string} cTypeHeader - the content type header string * @returns {string} type of content type header */ function getHeaderFamily (cTypeHeader) { let mediaType = 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; } /** * Finds valid JSON media type object from content object * * @param {*} contentObj - Content Object from schema * @returns {*} - valid JSON media type if exists */ function getJsonContentType (contentObj) { let jsonContentType = _.find(_.keys(contentObj), (contentType) => { let mediaType = parseMediaType(contentType); return mediaType.type === 'application' && ( mediaType.subtype === 'json' || _.endsWith(mediaType.subtype, '+json') ); }); return jsonContentType; } /** * 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 */ function checkContentTypeHeader (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 = parseMediaType(contentType); mediaTypes.push({ type: contentMediaType.type, subtype: contentMediaType.subtype, contentType: contentMediaType.type + '/' + contentMediaType.subtype }); }); // prefer JSON > XML > Other media types for suggested header. _.forEach(mediaTypes, (mediaType) => { let headerFamily = getHeaderFamily(mediaType.contentType); if (headerFamily !== HEADER_TYPE.INVALID) { suggestedContentHeader = mediaType.contentType; 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].contentType; hasComputedType = true; } // get content-type header and info _.forEach(headers, (header, index) => { if (_.toLower(header.key) === 'content-type') { let mediaType = 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)) { let mismatchObj, matched = false; // wildcard header matching _.forEach(mediaTypes, (mediaType) => { let transactionHeader = _.split(contentHeaderMediaType, '/'), headerTypeMatched = (mediaType.type === '*' || mediaType.type === transactionHeader[0]), headerSubtypeMatched = (mediaType.subtype === '*' || mediaType.subtype === transactionHeader[1]); if (headerTypeMatched && headerSubtypeMatched) { matched = true; } }); if (!matched) { 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; } /** * 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 */ function generateSdkParam (param, location) { const sdkElementMap = { 'query': QueryParam, 'header': Header, 'path': 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; } /** * Recursively 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 */ function extractDeepObjectParams (deepObject, objectKey) { let extractedParams = []; Object.keys(deepObject).forEach((key) => { let value = deepObject[key]; if (value && typeof value === 'object') { extractedParams = _.concat(extractedParams, 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 schemaResolution 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 */ function convertParamsWithStyle (param, paramValue, parameterSource, components, schemaCache, options) { var paramName = _.get(param, 'name'), pmParams = [], serialisedValue = '', description = getParameterDescription(param), disabled = false; // for invalid param object return null if (!_.isObject(param)) { return null; } let { style, explode, startValue, propSeparator, keyValueSeparator, isExplodable } = getParamSerialisationInfo(param, parameterSource, components, options); if (options && !options.enableOptionalParameters) { 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(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 = extractDeepObjectParams(paramValue, paramName); _.forEach(extractedParams, (extractedParam) => { pmParams.push(generateSdkParam({ key: extractedParam.key, value: extractedParam.value || '', description, disabled }, _.get(param, 'in'))); }); return pmParams; } break; 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(generateSdkParam({ key: paramName, value: serialisedValue, description, disabled }, _.get(param, 'in'))); return pmParams; } /** * Converts the necessary 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 */ function convertToPmCollectionVariables (serverVariables, keyName, serverUrl = '') { var variables = []; if (serverVariables) { _.forOwn(serverVariables, (value, key) => { let description = getParameterDescription(value); variables.push(new Variable({ key: key, value: value.default || '', description: description })); }); } if (keyName) { variables.push(new Variable({ key: keyName, value: serverUrl, type: 'string' })); } return variables; } /** * 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 {Object} context - Required context from related SchemaPack function * @param {array} operationParam operation (Postman request)-level params. * @param {array} pathParam are path parent-level params. * @returns {*} combined requestParams from operation and path params. */ function getRequestParams (context, operationParam, pathParam) { if (!Array.isArray(operationParam)) { operationParam = []; } if (!Array.isArray(pathParam)) { pathParam = []; } pathParam.forEach((param, index, arr) => { if (_.has(param, '$ref')) { arr[index] = resolveRefFromSchema(context, param.$ref); } }); operationParam.forEach((param, index, arr) => { if (_.has(param, '$ref')) { arr[index] = resolveRefFromSchema(context, param.$ref); } }); 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; } // TODO: document / comment properly all cases /** * Resolves schema for form params such that each individual request body param can be validated * to corresponding resolved schema params * * @param {*} schema - Schema object for corresponding form params * @param {*} schemaKey - Key for corresponding Schema object to be resolved * @param {*} encodingObj - OAS Encoding object * @param {*} requestParams - collection request parameters * @param {*} metaInfo - meta information of param (i.e. required) * @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 {Boolean} shouldIterateChildren - Defines whether to iterate over children further for type object children * @return {Array} Resolved form schema params */ function resolveFormParamSchema (schema, schemaKey, encodingObj, requestParams, metaInfo, components, options, shouldIterateChildren) { let resolvedSchemaParams = [], resolvedProp, encodingValue, pSerialisationInfo, isPropSeparable; if (_.isArray(schema.anyOf) || _.isArray(schema.oneOf)) { _.forEach(schema.anyOf || schema.oneOf, (schemaElement) => { // As for such schemas there can be multiple choices, keep them as non required resolvedSchemaParams = _.concat(resolvedSchemaParams, resolveFormParamSchema(schemaElement, schemaKey, encodingObj, requestParams, _.assign(metaInfo, { required: false, isComposite: true }), components, options, shouldIterateChildren)); }); return resolvedSchemaParams; } resolvedProp = { name: schemaKey, schema: schema, required: _.get(metaInfo, 'required'), in: 'query', // serialization follows same behaviour as query params description: _.get(schema, 'description', _.get(metaInfo, 'description', '')), pathPrefix: _.get(metaInfo, 'pathPrefix'), isComposite: _.get(metaInfo, 'isComposite', false), deprecated: _.get(schema, 'deprecated', _.get(metaInfo, 'deprecated')) }; encodingValue = _.get(encodingObj, schemaKey); if (_.isObject(encodingValue)) { _.has(encodingValue, 'style') && (resolvedProp.style = encodingValue.style); _.has(encodingValue, 'explode') && (resolvedProp.explode = encodingValue.explode); } pSerialisationInfo = getParamSerialisationInfo(resolvedProp, PARAMETER_SOURCE.REQUEST, components, options); isPropSeparable = _.includes(['form', 'deepObject'], pSerialisationInfo.style); /** * schema apart from type object should be only resolved if corresponding schema key is present. * i.e. As URL encoded body requires key-value pair, non key schemas should not be resolved. */ if (!isPropSeparable || (!_.includes(['object', 'array'], _.get(schema, 'type')) && !_.isEmpty(schemaKey)) || (_.isEmpty(schemaKey) && _.get(schema, 'type') === 'array') || !pSerialisationInfo.explode) { resolvedSchemaParams.push(resolvedProp); } else if (_.get(schema, 'type') === 'array' && pSerialisationInfo.style === 'form' && pSerialisationInfo.explode) { resolvedProp.schema = _.get(schema, 'items', {}); resolvedSchemaParams.push(resolvedProp); } else { // resolve each property as separate param similar to query params _.forEach(_.get(schema, 'properties'), (propSchema, propName) => { let resolvedPropName = _.isEmpty(schemaKey) ? propName : `${schemaKey}[${propName}]`, resolvedProp = { name: resolvedPropName, schema: propSchema, in: 'query', // serialization follows same behaviour as query params description: _.get(propSchema, 'description') || _.get(metaInfo, 'description') || '', required: _.get(metaInfo, 'required'), isComposite: _.get(metaInfo, 'isComposite', false), deprecated: _.get(propSchema, 'deprecated') || _.get(metaInfo, 'deprecated') }, parentPropName = resolvedPropName.indexOf('[') === -1 ? resolvedPropName : resolvedPropName.slice(0, resolvedPropName.indexOf('[')), encodingValue = _.get(encodingObj, parentPropName), pSerialisationInfo, isPropSeparable; if (_.isObject(encodingValue)) { _.has(encodingValue, 'style') && (resolvedProp.style = encodingValue.style); _.has(encodingValue, 'explode') && (resolvedProp.explode = encodingValue.explode); } if (_.isUndefined(metaInfo.required) && _.includes(_.get(schema, 'required'), propName)) { resolvedProp.required = true; } pSerialisationInfo = getParamSerialisationInfo(resolvedProp, PARAMETER_SOURCE.REQUEST, components, options); isPropSeparable = _.includes(['form', 'deepObject'], pSerialisationInfo.style); if (_.isArray(propSchema.anyOf) || _.isArray(propSchema.oneOf)) { _.forEach(propSchema.anyOf || propSchema.oneOf, (schemaElement) => { let nextSchemaKey = _.isEmpty(schemaKey) ? propName : `${schemaKey}[${propName}]`; resolvedSchemaParams = _.concat(resolvedSchemaParams, resolveFormParamSchema(schemaElement, nextSchemaKey, encodingObj, requestParams, _.assign(metaInfo, { required: false, isComposite: true }), components, options, pSerialisationInfo.style === 'deepObject')); }); return resolvedSchemaParams; } 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 (pSerialisationInfo.style !== 'deepObject' && !_.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) { let localMetaInfo = _.isEmpty(metaInfo) ? (metaInfo = { required: resolvedProp.required, description: resolvedProp.description, deprecated: _.get(resolvedProp, 'deprecated') }) : metaInfo, nextSchemaKey = _.isEmpty(schemaKey) ? propName : `${schemaKey}[${propName}]`; // resolve all child params of parent param with deepObject style if (pSerialisationInfo.style === 'deepObject') { resolvedSchemaParams = _.concat(resolvedSchemaParams, resolveFormParamSchema(propSchema, nextSchemaKey, encodingObj, requestParams, localMetaInfo, components, options, true)); } 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, description: resolvedProp.description, isComposite: _.get(metaInfo, 'isComposite', false), deprecated: _.get(resolvedProp, 'deprecated') || _.get(metaInfo, 'deprecated') }); }); } } else { resolvedSchemaParams.push(resolvedProp); } }); // Resolve additionalProperties via first finding additionalProper if (_.isObject(_.get(schema, 'additionalProperties'))) { const additionalPropSchema = _.get(schema, 'additionalProperties'), matchingRequestParamKeys = []; /** * Find matching keys from request param as additional props can be unknown keys. * and these unknown key names are not mentioned in schema */ _.forEach(requestParams, ({ key }) => { if (_.isString(key) && schemaKey === '') { const isParamResolved = _.some(resolvedSchemaParams, (param) => { return key === param.key; }); !isParamResolved && (matchingRequestParamKeys.push(key)); } else if (_.isString(key) && _.startsWith(key, schemaKey + '[') && _.endsWith(key, ']')) { const childKey = key.substring(key.indexOf(schemaKey + '[') + schemaKey.length + 1, key.length - 1); if (!_.includes(childKey, '[')) { matchingRequestParamKeys.push(key); } else { matchingRequestParamKeys.push(schemaKey + '[' + childKey.slice(0, childKey.indexOf('['))); } } }); // For every matched request param key add a child param schema that can be validated further _.forEach(matchingRequestParamKeys, (matchedRequestParamKey) => { if (_.get(additionalPropSchema, 'type') === 'object' && shouldIterateChildren) { resolvedSchemaParams = _.concat(resolvedSchemaParams, resolveFormParamSchema(additionalPropSchema, matchedRequestParamKey, encodingObj, requestParams, metaInfo, components, options, shouldIterateChildren)); } // Avoid adding invalid array child params, As deepObject style should only contain object or simple types else if (_.get(additionalPropSchema, 'type') !== 'array') { resolvedSchemaParams.push({ name: matchedRequestParamKey, schema: additionalPropSchema, description: _.get(additionalPropSchema, 'description') || _.get(metaInfo, 'description') || '', required: false, isResolvedParam: true, isComposite: true, deprecated: _.get(additionalPropSchema, 'deprecated') || _.get(metaInfo, 'deprecated') }); } }); } } return resolvedSchemaParams; } /** * * @param {Object} context - Required context from related SchemaPack function * @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 schemaResolution caches * @param {string} jsonSchemaDialect The schema dialect defined in the OAS object * @param {Function} callback - For return * @returns {Array} array of mismatches */ function checkValueAgainstSchema (context, property, jsonPathPrefix, txnParamName, value, schemaPathPrefix, openApiSchemaObj, parameterSourceOption, components, options, schemaCache, jsonSchemaDialect, callback) { let mismatches = [], jsonValue, humanPropName = propNames[property], needJsonMatching = (property === 'BODY' || property === 'RESPONSE_BODY'), invalidJson = false, valueToUse = value, schema = resolveSchema(context, openApiSchemaObj, { resolveFor: PROCESSING_TYPE.VALIDATION, isResponseSchema: parameterSourceOption === PARAMETER_SOURCE.RESPONSE }), compositeSchema = schema.oneOf || schema.anyOf, compareTypes = _.get(context, 'concreteUtils.compareTypes') || concreteUtils.compareTypes; if (needJsonMatching) { try { j