diff --git a/CHANGELOG.md b/CHANGELOG.md index 56240ba3e..22169fd59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,14 @@ # OpenAPI-Postman Changelog +#### v1.1.20 (June 14, 2020) +* Added support for stricter request matching via option for validation. +* Added missing endpoints from collection info in result of validation. +* Suggest fixes in collection for violated properties in validation. +* Introduced option to validate metadata for validation. +* Use faked value instead of fallback to schema for parameter resolution when set to example. +* Use faked value instead of invalid schema defined example. +* Introduced option to ignore unresolved postman variable mismatches. +* Fixed invalid generated collection for body type formdata. + #### v1.1.19 (June 12, 2020) * Fix for [#232](https://github.com/postmanlabs/openapi-to-postman/issues/232) - Changes default auth of requests to null conforming to the JSON schema. diff --git a/assets/json-schema-faker.js b/assets/json-schema-faker.js index 726fdc5f6..03d281971 100644 --- a/assets/json-schema-faker.js +++ b/assets/json-schema-faker.js @@ -10,6 +10,8 @@ * Date: 2018-04-09 17:23:23.954Z */ +var validateSchema = require('../lib/ajvValidation').validateSchema; + (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : @@ -24430,6 +24432,15 @@ function extend() { return dateTimeGenerator().slice(11); } + /** + * Added few formats from latest json-schema-faker. see below for source + * https://github.com/json-schema-faker/json-schema-faker/blob/master/src/lib/generators/coreFormat.js + * + */ + const FRAGMENT = '[a-zA-Z][a-zA-Z0-9+-.]*'; + const URI_PATTERN = `https?://{hostname}(?:${FRAGMENT})+`; + const PARAM_PATTERN = '(?:\\?([a-z]{1,7}(=\\w{1,5})?&){0,3})?'; + /** * Predefined core formats * @type {[key: string]: string} @@ -24438,9 +24449,24 @@ function extend() { email: '[a-zA-Z\\d][a-zA-Z\\d-]{1,13}[a-zA-Z\\d]@{hostname}', hostname: '[a-zA-Z]{1,33}\\.[a-z]{2,4}', ipv6: '[a-f\\d]{4}(:[a-f\\d]{4}){7}', - uri: 'https?://[a-zA-Z][a-zA-Z0-9+-.]*', - 'uri-reference': '(https?://|#|/|)[a-zA-Z][a-zA-Z0-9+-.]*', + uri: URI_PATTERN, + slug: '[a-zA-Z\\d_-]+', + + // types from draft-0[67] (?) + 'uri-reference': `${URI_PATTERN}${PARAM_PATTERN}`, + 'uri-template': URI_PATTERN.replace('(?:', '(?:/\\{[a-z][:a-zA-Z0-9-]*\\}|'), + 'json-pointer': `(/(?:${FRAGMENT.replace(']*', '/]*')}|~[01]))+`, + + // some types from https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#data-types (?) + uuid: '^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$', }; + + regexps.iri = regexps['uri-reference']; + regexps['iri-reference'] = regexps['uri-reference']; + + regexps['idn-email'] = regexps.email; + regexps['idn-hostname'] = regexps.hostname; + /** * Generates randomized string basing on a built-in regex format * @@ -24476,6 +24502,14 @@ function extend() { case 'ipv6': case 'uri': case 'uri-reference': + case 'iri': + case 'iri-reference': + case 'idn-email': + case 'idn-hostname': + case 'json-pointer': + case 'slug': + case 'uri-template': + case 'uuid': return coreFormatGenerator(value.format); default: if (typeof callback === 'undefined') { @@ -24520,7 +24554,12 @@ function extend() { return; } if (optionAPI('useExamplesValue') && 'example' in schema) { - return schema.example; + var result = validateSchema(schema, schema.example); + + // Use example only if valid + if (result && result.length === 0) { + return schema.example; + } } if (optionAPI('useDefaultValue') && 'default' in schema) { return schema.default; diff --git a/lib/ajvValidation.js b/lib/ajvValidation.js new file mode 100644 index 000000000..6cf83b3ba --- /dev/null +++ b/lib/ajvValidation.js @@ -0,0 +1,89 @@ +var _ = require('lodash'), + Ajv = require('ajv'); + +// Following keyword are supoorted for Ajv but not by OAS +const IGNORED_KEYWORDS = ['propertyNames', 'const', 'additionalItems', 'dependencies']; + +/** + * Checks if value is postman variable or not + * + * @param {*} value - Value to check for + * @returns {Boolean} postman variable or not + */ +function isPmVariable (value) { + // collection/environment variables are in format - {{var}} + return _.isString(value) && _.startsWith(value, '{{') && _.endsWith(value, '}}'); +} + +/** + * Used to validate schema against a value. + * NOTE: Used in assets/json-schema-faker.js to validate schema example + * + * @param {*} schema - schema to validate + * @param {*} valueToUse - value to validate schema against + * @param {*} options - a standard list of options that's globally passed around. Check options.js for more. + * @returns {*} - Found Validation Errors + */ +function validateSchema (schema, valueToUse, options = {}) { + var ajv, + validate, + filteredValidationError; + + try { + // add Ajv options to support validation of OpenAPI schema. + // For more details see https://ajv.js.org/#options + ajv = new Ajv({ + // the unknown formats are ones that are allowed in OAS, but not JSON schema. + unknownFormats: ['int32', 'int64'], + + // check all rules collecting all errors. instead returning after the first error. + allErrors: true, + + // supports keyword "nullable" from Open API 3 specification. + nullable: true + }); + validate = ajv.compile(schema); + validate(valueToUse); + } + catch (e) { + // something went wrong validating the schema + // input was invalid. Don't throw mismatch + return filteredValidationError; + } + + // Filter validation errors for following cases + filteredValidationError = _.filter(_.get(validate, 'errors', []), (validationError) => { + let dataPath = _.get(validationError, 'dataPath', ''); + + // discard the leading '.' if it exists + if (dataPath[0] === '.') { + dataPath = dataPath.slice(1); + } + + // for invalid `propertyNames` two error are thrown by Ajv, which include error with `pattern` keyword + if (validationError.keyword === 'pattern') { + return !_.has(validationError, 'propertyName'); + } + + // As OAS only supports some of Json Schema keywords, and Ajv is supporting all keywords from Draft 7 + // Remove keyword currently not supported in OAS to make both compatible with each other + else if (_.includes(IGNORED_KEYWORDS, validationError.keyword)) { + return false; + } + + // Ignore unresolved variables from mismatch if option is set + else if (options.ignoreUnresolvedVariables && isPmVariable(_.get(valueToUse, dataPath))) { + return false; + } + return true; + }); + + // sort errors based on dataPath, as this will ensure no overriding later + filteredValidationError = _.sortBy(filteredValidationError, ['dataPath']); + + return filteredValidationError; +} + +module.exports = { + validateSchema +}; diff --git a/lib/ajvValidationError.js b/lib/ajvValidationError.js index 6b374c159..dbb9f28bb 100644 --- a/lib/ajvValidationError.js +++ b/lib/ajvValidationError.js @@ -9,12 +9,19 @@ var _ = require('lodash'); * @returns {Object} mismatchObj with reason and reasonCode properties */ function ajvValidationError(ajvValidationErrorObj, data) { - var mismatchPropName = _.get(ajvValidationErrorObj, 'dataPath', '').slice(1), - mismatchObj = { - reason: `The ${data.humanPropName} property "${mismatchPropName}" ` + - `${ajvValidationErrorObj.message}`, - reasonCode: 'INVALID_TYPE' - }; + var mismatchPropName = _.get(ajvValidationErrorObj, 'dataPath', ''), + mismatchObj; + + // discard the leading '.' if it exists + if (mismatchPropName[0] === '.') { + mismatchPropName = mismatchPropName.slice(1); + } + + mismatchObj = { + reason: `The ${data.humanPropName} property "${mismatchPropName}" ` + + `${ajvValidationErrorObj.message}`, + reasonCode: 'INVALID_TYPE' + }; switch (ajvValidationErrorObj.keyword) { @@ -22,11 +29,13 @@ function ajvValidationError(ajvValidationErrorObj, data) { mismatchObj.reasonCode = 'MISSING_IN_SCHEMA'; break; - case 'dependencies': - mismatchObj.reason = `The ${data.humanPropName} property "${mismatchPropName}" ` + - `should have property "${_.get(ajvValidationErrorObj, 'params.missingProperty')}" when property ` + - `"${_.get(ajvValidationErrorObj, 'params.property')}" is present`; - break; + // currently not supported as OAS doesn't support this keyword + // case 'dependencies': + // mismatchObj.reasonCode = 'MISSING_IN_REQUEST'; + // mismatchObj.reason = `The ${data.humanPropName} property "${mismatchPropName}" ` + + // `should have property "${_.get(ajvValidationErrorObj, 'params.missingProperty')}" when property ` + + // `"${_.get(ajvValidationErrorObj, 'params.property')}" is present`; + // break; case 'required': mismatchObj.reasonCode = 'MISSING_IN_REQUEST'; @@ -34,10 +43,11 @@ function ajvValidationError(ajvValidationErrorObj, data) { `property "${_.get(ajvValidationErrorObj, 'params.missingProperty')}"`; break; - case 'propertyNames': - mismatchObj.reason = `The ${data.humanPropName} property "${mismatchPropName}" contains invalid ` + - `property named "${_.get(ajvValidationErrorObj, 'params.propertyName')}"`; - break; + // currently not supported as OAS doesn't support this keyword + // case 'propertyNames': + // mismatchObj.reason = `The ${data.humanPropName} property "${mismatchPropName}" contains invalid ` + + // `property named "${_.get(ajvValidationErrorObj, 'params.propertyName')}"`; + // break;` default: break; diff --git a/lib/deref.js b/lib/deref.js index d2b62dab3..a6673f767 100644 --- a/lib/deref.js +++ b/lib/deref.js @@ -55,12 +55,14 @@ module.exports = { * @param {*} components components in openapi spec. * @param {object} schemaResolutionCache stores already resolved references * @param {*} resolveFor - resolve refs for validation/conversion (value to be one of VALIDATION/CONVERSION) + * @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 {*} stack counter which keeps a tab on nested schemas * @param {*} seenRef References that are repeated. Used to identify circular references. * @returns {*} schema - schema that adheres to all individual schemas in schemaArr */ resolveAllOf: function (schemaArr, parameterSourceOption, components, schemaResolutionCache, - resolveFor = 'CONVERSION', stack = 0, seenRef) { + resolveFor = 'CONVERSION', resolveTo = 'schema', stack = 0, seenRef) { if (!(schemaArr instanceof Array)) { return null; @@ -69,13 +71,13 @@ module.exports = { if (schemaArr.length === 1) { // for just one entry in allOf, don't need to enforce type: object restriction return this.resolveRefs(schemaArr[0], parameterSourceOption, components, schemaResolutionCache, resolveFor, - stack, seenRef); + resolveTo, stack, seenRef); } // generate one object for each schema let indivObjects = schemaArr.map((schema) => { return this.resolveRefs(schema, parameterSourceOption, components, schemaResolutionCache, resolveFor, - stack, seenRef); + resolveTo, stack, seenRef); }).filter((schema) => { return schema.type === 'object'; }), @@ -113,13 +115,15 @@ module.exports = { * @param {*} components components in openapi spec. * @param {object} schemaResolutionCache stores already resolved references * @param {*} resolveFor - resolve refs for validation/conversion (value to be one of VALIDATION/CONVERSION) + * @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 {*} stack counter which keeps a tab on nested schemas * @param {*} seenRef - References that are repeated. Used to identify circular references. * @returns {*} schema satisfying JSON-schema-faker. */ resolveRefs: function (schema, parameterSourceOption, components, schemaResolutionCache, - resolveFor = 'CONVERSION', stack = 0, seenRef = {}) { + resolveFor = 'CONVERSION', resolveTo = 'schema', stack = 0, seenRef = {}) { var resolvedSchema, prop, splitRef, ERR_TOO_MANY_LEVELS = ''; @@ -136,15 +140,15 @@ module.exports = { if (schema.anyOf) { return this.resolveRefs(schema.anyOf[0], parameterSourceOption, components, schemaResolutionCache, resolveFor, - stack, _.cloneDeep(seenRef)); + resolveTo, stack, _.cloneDeep(seenRef)); } if (schema.oneOf) { return this.resolveRefs(schema.oneOf[0], parameterSourceOption, components, schemaResolutionCache, resolveFor, - stack, _.cloneDeep(seenRef)); + resolveTo, stack, _.cloneDeep(seenRef)); } if (schema.allOf) { return this.resolveAllOf(schema.allOf, parameterSourceOption, components, schemaResolutionCache, resolveFor, - stack, _.cloneDeep(seenRef)); + resolveTo, stack, _.cloneDeep(seenRef)); } if (schema.$ref && _.isFunction(schema.$ref.split)) { let refKey = schema.$ref; @@ -186,7 +190,7 @@ module.exports = { if (resolvedSchema) { let refResolvedSchema = this.resolveRefs(resolvedSchema, parameterSourceOption, - components, schemaResolutionCache, resolveFor, stack, _.cloneDeep(seenRef)); + components, schemaResolutionCache, resolveFor, resolveTo, stack, _.cloneDeep(seenRef)); if (refResolvedSchema && refResolvedSchema.value !== ERR_TOO_MANY_LEVELS) { schemaResolutionCache[refKey] = refResolvedSchema; @@ -221,15 +225,19 @@ module.exports = { continue; } /* eslint-enable */ - tempSchema.properties[prop] = this.resolveRefs(property, - parameterSourceOption, components, schemaResolutionCache, resolveFor, stack, _.cloneDeep(seenRef)); + tempSchema.properties[prop] = this.resolveRefs(property, parameterSourceOption, components, + schemaResolutionCache, resolveFor, resolveTo, stack, _.cloneDeep(seenRef)); } } return tempSchema; } schema.type = 'string'; - schema.default = ''; + + // Override deefault value to appropriate type representation for parameter resolution to schema + if (resolveFor === 'CONVERSION' && resolveTo === 'schema') { + schema.default = ''; + } } else if (schema.type === 'array' && schema.items) { /* @@ -258,25 +266,28 @@ module.exports = { // without this, schemas with circular references aren't faked correctly let tempSchema = _.omit(schema, 'items'); tempSchema.items = this.resolveRefs(schema.items, parameterSourceOption, - components, schemaResolutionCache, resolveFor, stack, _.cloneDeep(seenRef)); + components, schemaResolutionCache, resolveFor, resolveTo, stack, _.cloneDeep(seenRef)); return tempSchema; } else if (!schema.hasOwnProperty('default')) { if (schema.hasOwnProperty('type')) { - if (!schema.hasOwnProperty('format')) { - schema.default = '<' + schema.type + '>'; - } - else if (type.hasOwnProperty(schema.type)) { - schema.default = type[schema.type][schema.format]; + // Override default value to schema for CONVERSION only for parmeter resolution set to schema + if (resolveFor === 'CONVERSION' && resolveTo === 'schema') { + if (!schema.hasOwnProperty('format')) { + schema.default = '<' + schema.type + '>'; + } + else if (type.hasOwnProperty(schema.type)) { + schema.default = type[schema.type][schema.format]; - // in case the format is a custom format (email, hostname etc.) - // https://swagger.io/docs/specification/data-models/data-types/#string - if (!schema.default && schema.format) { - schema.default = '<' + schema.format + '>'; + // in case the format is a custom format (email, hostname etc.) + // https://swagger.io/docs/specification/data-models/data-types/#string + if (!schema.default && schema.format) { + schema.default = '<' + schema.format + '>'; + } + } + else { + schema.default = '<' + schema.type + (schema.format ? ('-' + schema.format) : '') + '>'; } - } - else { - schema.default = '<' + schema.type + (schema.format ? ('-' + schema.format) : '') + '>'; } } else if (schema.enum && schema.enum.length > 0) { diff --git a/lib/options.js b/lib/options.js index 69317a2c3..3f195ce07 100644 --- a/lib/options.js +++ b/lib/options.js @@ -39,7 +39,7 @@ module.exports = { ' If “Fallback” is selected, the request will be named after one of the following schema' + ' values: `description`, `operationid`, `url`.', external: true, - usage: ['CONVERSION'] + usage: ['CONVERSION', 'VALIDATION'] }, { name: 'Set indent character', @@ -145,6 +145,43 @@ module.exports = { 'in the request/response body.', external: true, usage: ['VALIDATION'] + }, + { + name: 'Suggest fixes if available', + id: 'suggestAvailableFixes', + type: 'boolean', + default: false, + description: 'Whether to provide fixes for patching corresponding mismatches.', + external: true, + usage: ['VALIDATION'] + }, + { + name: 'Show Metadata validation messages', + id: 'validateMetadata', + type: 'boolean', + default: false, + description: 'Whether to show mismatches for incorrect name and description of request', + external: true, + usage: ['VALIDATION'] + }, + { + name: 'Ignore mismatch for unresolved postman variables', + id: 'ignoreUnresolvedVariables', + type: 'boolean', + default: false, + description: 'Whether to ignore mismatches resulting from unresolved variables in the Postman request', + external: true, + usage: ['VALIDATION'] + }, + { + name: 'Enable strict request matching', + id: 'strictRequestMatching', + type: 'boolean', + default: false, + description: 'Whether requests should be strictly matched with schema operations. Setting to true will not ' + + 'include any matches where the URL path segments don\'t match exactly.', + external: true, + usage: ['VALIDATION'] } ]; diff --git a/lib/schemaUtils.js b/lib/schemaUtils.js index 5b13e7369..f26e2f887 100644 --- a/lib/schemaUtils.js +++ b/lib/schemaUtils.js @@ -4,7 +4,6 @@ */ const async = require('async'), - Ajv = require('ajv'), sdk = require('postman-collection'), schemaFaker = require('../assets/json-schema-faker.js'), parse = require('./parse.js'), @@ -16,6 +15,7 @@ const async = require('async'), 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 @@ -114,6 +114,7 @@ function hash(input) { * @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 @@ -121,14 +122,14 @@ function hash(input) { * @param {object} schemaCache - object storing schemaFaker and schmeResolution caches * @returns {object} fakedObject */ -function safeSchemaFaker(oldSchema, resolveTo, parameterSourceOption, components, +function safeSchemaFaker(oldSchema, resolveTo, resolveFor, parameterSourceOption, components, schemaFormat, indentCharacter, schemaCache) { var prop, key, resolvedSchema, fakedSchema, schemaResolutionCache = _.get(schemaCache, 'schemaResolutionCache', {}), schemaFakerCache = _.get(schemaCache, 'schemaFakerCache', {}); resolvedSchema = deref.resolveRefs(oldSchema, parameterSourceOption, components, schemaResolutionCache, - PROCESSING_TYPE.CONVERSION); + resolveFor, resolveTo); key = JSON.stringify(resolvedSchema); if (resolveTo === 'schema') { @@ -144,6 +145,12 @@ function safeSchemaFaker(oldSchema, resolveTo, parameterSourceOption, components }); } + if (resolveFor === PROCESSING_TYPE.VALIDATION) { + schemaFaker.option({ + useDefaultValue: false + }); + } + key = hash(key); if (schemaFakerCache[key]) { return schemaFakerCache[key]; @@ -762,12 +769,13 @@ module.exports = { else { _.forEach(commonPathVars, (variable) => { let description = this.getParameterDescription(variable); + variables.push({ key: variable.name, // we only fake the schema for param-level pathVars value: options.schemaFaker ? - safeSchemaFaker(variable.schema || {}, 'schema', components, 'REQUEST', - SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache) : '', + safeSchemaFaker(variable.schema || {}, options.requestParametersResolution, PROCESSING_TYPE.CONVERSION, + PARAMETER_SOURCE.REQUEST, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache) : '', description: description }); }); @@ -1107,7 +1115,7 @@ module.exports = { if (this.getHeaderFamily(contentType) === HEADER_TYPE.XML) { schemaType = SCHEMA_FORMATS.XML; } - bodyData = safeSchemaFaker(bodyObj.schema || {}, resolveTo, parameterSourceOption, + bodyData = safeSchemaFaker(bodyObj.schema || {}, resolveTo, PROCESSING_TYPE.CONVERSION, parameterSourceOption, components, schemaType, indentCharacter, schemaCache); } else { @@ -1171,7 +1179,7 @@ module.exports = { if (param.hasOwnProperty('schema')) { // fake data generated paramValue = options.schemaFaker ? - safeSchemaFaker(param.schema, resolveTo, PARAMETER_SOURCE.REQUEST, + safeSchemaFaker(param.schema, resolveTo, PROCESSING_TYPE.CONVERSION, PARAMETER_SOURCE.REQUEST, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache) : ''; // paramType = param.schema.type; @@ -1378,7 +1386,7 @@ module.exports = { fakeData = ''; } else { - fakeData = safeSchemaFaker(header.schema || {}, resolveTo, parameterSource, + fakeData = safeSchemaFaker(header.schema || {}, resolveTo, PROCESSING_TYPE.CONVERSION, parameterSource, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache); // for schema.type=string or number, @@ -1533,7 +1541,8 @@ module.exports = { param = new sdk.FormParam({ key: key, - value: value + value: value, + type: 'text' }); param.description = description; paramArray.push(param); @@ -2088,7 +2097,7 @@ module.exports = { // 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) { + findMatchingRequestFromSchema: function (method, url, schema, options) { // first step - get array of requests from schema let parsedUrl = require('url').parse(url), retVal = [], @@ -2133,7 +2142,7 @@ module.exports = { } // check if path and pathToMatch match (non-null) - let schemaMatchResult = this.getPostmanUrlSchemaMatchScore(pathToMatch, path); + 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; @@ -2143,11 +2152,18 @@ module.exports = { path, pathItem: pathItemObject, matchScore: schemaMatchResult.score, - pathVars: schemaMatchResult.pathVars + 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 }); }); - _.each(filteredPathItemsArray, (fp) => { + // keep endpoints with more fix matched segments first in result + _.each(_.orderBy(filteredPathItemsArray, ['fixedMatchedSegments', 'variableMatchedSegments'], ['desc']), (fp) => { let path = fp.path, pathItemObject = fp.pathItem, score = fp.matchScore, @@ -2199,30 +2215,45 @@ module.exports = { }, /** + * Checks if value is postman variable or not * - * @param {*} property - one of QUERYPARAM, PATHVARIABLE, HEADER, BODY, RESPONSE_HEADER, RESPONSE_BODY - * @param {*} jsonPathPrefix - this will be prepended to all JSON schema paths on the request - * @param {*} txnParamName - Optional - The name of the param being validated (useful for query params, + * @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, '}}'); + }, + + /** + * + * @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 {*} schemaPathPrefix - this will be prepended to all JSON schema paths on the schema - * @param {*} schema - The schema against which to validate - * @param {*} components - Components in the spec that the schema might refer to - * @param {*} options - Global options - * @param {*} callback - For return - * @returns {*} array of mismatches + * @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, schema, - components, options, callback) { + 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, - ajv, validate, valueToUse = value, - res = true; + + // This is dereferenced schema (converted to JSON schema for validation) + schema = deref.resolveRefs(openApiSchemaObj, parameterSourceOption, components, + schemaCache.schemaResolutionCache, PROCESSING_TYPE.VALIDATION, 'example'); if (needJsonMatching) { try { @@ -2240,9 +2271,21 @@ module.exports = { // When processing a reference, schema.type could also be undefined if (schema && schema.type) { if (typeof schemaTypeToJsValidator[schema.type] === 'function') { - if (!schemaTypeToJsValidator[schema.type](valueToUse)) { + 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 = ''; + let reason = '', + mismatchObj; + if (property === 'RESPONSE_BODY' || property === 'BODY') { // we don't have names for the body, but there's only one reason = 'The ' + humanPropName; @@ -2272,59 +2315,98 @@ module.exports = { reason += `a ${typeof valueToUse} instead`; } - return callback(null, [{ + 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) + }; + } + + return callback(null, [mismatchObj]); } // only do AJV if type is array or object // simpler cases are handled by a type check if (schema.type === 'array' || schema.type === 'object') { - try { - // add Ajv options to support validation of OpenAPI schema. - // For more details see https://ajv.js.org/#options - ajv = new Ajv({ - // the unknown formats are ones that are allowed in OAS, but not JSON schema. - unknownFormats: ['int32', 'int64'], + let filteredValidationError = validateSchema(schema, valueToUse, options); - // check all rules collecting all errors. instead returning after the first error. - allErrors: true, + if (!_.isEmpty(filteredValidationError)) { + let mismatchObj, + suggestedValue, + fakedValue = safeSchemaFaker(openApiSchemaObj || {}, 'example', PROCESSING_TYPE.VALIDATION, + parameterSourceOption, components, SCHEMA_FORMATS.DEFAULT, options.indentCharacter, schemaCache); - // supports keyword "nullable" from Open API 3 specification. - nullable: true - }); - validate = ajv.compile(schema); - res = validate(valueToUse); - } - catch (e) { - // something went wrong validating the schema - // input was invalid. Don't throw mismatch - } - if (!res) { // Show detailed validation mismatches for only request/response body if (options.detailedBlobValidation && needJsonMatching) { - _.forEach(validate.errors, (ajvError) => { - mismatches.push(_.assign({ + _.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 + ajvError.schemaPath.replace(/\//g, '.').slice(1) - }, ajvValidationError(ajvError, { property, humanPropName }))); + schemaJsonPath: schemaPathPrefix + '.' + localSchemaPath + }, ajvValidationError(ajvError, { property, humanPropName })); + + if (options.suggestAvailableFixes) { + mismatchObj.suggestedFix = { + key: _.split(dataPath, '.').pop(), + actualValue: _.get(valueToUse, dataPath, null), + suggestedValue: this.getSuggestedValue(fakedValue, valueToUse, ajvError) + }; + } + mismatches.push(mismatchObj); }); } else { - let mismatchObj = { - reason: 'The property didn\'t match the specified schema', + mismatchObj = { + reason: `The ${humanPropName} didn\'t match the specified schema`, reasonCode: 'INVALID_TYPE' }; + // assign proper reason codes for invalid body if (property === 'BODY') { - mismatchObj.reason = 'The request body didn\'t match the specified schema'; 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); + } + _.set(suggestedValue, dataPath, this.getSuggestedValue(fakedValue, valueToUse, ajvError)); + }); + + mismatchObj.suggestedFix = { + key: property.toLowerCase(), + actualValue: valueToUse, + suggestedValue + }; + } mismatches.push(_.assign({ property: property, @@ -2338,7 +2420,23 @@ module.exports = { } // 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 @@ -2370,7 +2468,7 @@ module.exports = { * @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 {*} schemaResolutionCache cache used to store resolved schemas + * @param {*} schemaCache object storing schemaFaker and schmeResolution caches * @param {*} callback Callback * @returns {array} mismatches (in the callback) */ @@ -2380,7 +2478,7 @@ module.exports = { schemaPath, components, options, - schemaResolutionCache, + schemaCache, callback) { // schema path should have all parameters needed @@ -2399,7 +2497,8 @@ module.exports = { }); async.map(determinedPathVariables, (pathVar, cb) => { - let mismatches = []; + let mismatches = [], + index = _.findIndex(determinedPathVariables, pathVar); schemaPathVar = _.find(schemaPathVariables, (param) => { return param.name === pathVar.key; @@ -2411,10 +2510,10 @@ module.exports = { mismatches.push({ property: mismatchProperty, // not adding the pathVar name to the jsonPath because URL is just a string - transactionJsonPath: transactionPathPrefix, + transactionJsonPath: transactionPathPrefix + `[${index}]`, schemaJsonPath: null, reasonCode: 'MISSING_IN_SCHEMA', - reason: `The path variable ${pathVar.key} was not found in the schema` + reason: `The path variable "${pathVar.key}" was not found in the schema` }); } return cb(null, mismatches); @@ -2427,16 +2526,17 @@ module.exports = { } this.checkValueAgainstSchema(mismatchProperty, - transactionPathPrefix, + transactionPathPrefix + `[${index}].value`, pathVar.key, pathVar.value, schemaPathVar.pathPrefix + '[?(@.name==\'' + schemaPathVar.name + '\')]', - deref.resolveRefs(schemaPathVar.schema, 'request', components, schemaResolutionCache, - PROCESSING_TYPE.VALIDATION), - components, options, cb); + schemaPathVar.schema, + PARAMETER_SOURCE.REQUEST, + components, options, schemaCache, cb); }, 0); }, (err, res) => { - let mismatches = []; + let mismatches = [], + mismatchObj; if (err) { return callback(err); @@ -2445,13 +2545,26 @@ module.exports = { // 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; })) { - mismatches.push({ + mismatchObj = { property: mismatchProperty, - transactionJsonPath: null, + 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) + } + }; + } + mismatches.push(mismatchObj); } }); @@ -2460,8 +2573,102 @@ module.exports = { }); }, + /** + * + * @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); + } + + if (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, - schemaResolutionCache, callback) { + schemaCache, callback) { let parsedUrl = require('url').parse(requestUrl), schemaParams = _.filter(schemaPath.parameters, (param) => { return param.in === 'query'; }), requestQueryArray = [], @@ -2505,7 +2712,9 @@ module.exports = { } return async.map(requestQueryParams, (pQuery, cb) => { - let mismatches = []; + let mismatches = [], + index = _.findIndex(requestQueryParams, pQuery); + const schemaParam = _.find(schemaParams, (param) => { return param.name === pQuery.key; }); if (!schemaParam) { @@ -2513,7 +2722,7 @@ module.exports = { if (options.showMissingInSchemaErrors) { mismatches.push({ property: mismatchProperty, - transactionJsonPath: transactionPathPrefix + '[?(@.key==\'' + pQuery.key + '\')]', + transactionJsonPath: transactionPathPrefix + `[${index}]`, schemaJsonPath: null, reasonCode: 'MISSING_IN_SCHEMA', reason: `The query parameter ${pQuery.key} was not found in the schema` @@ -2529,27 +2738,41 @@ module.exports = { return cb(null, []); } this.checkValueAgainstSchema(mismatchProperty, - transactionPathPrefix + '[?(@.key==\'' + pQuery.key + '\')]', + transactionPathPrefix + `[${index}].value`, pQuery.key, pQuery.value, schemaParam.pathPrefix + '[?(@.name==\'' + schemaParam.name + '\')]', - deref.resolveRefs(schemaParam.schema, 'request', components, schemaResolutionCache, - PROCESSING_TYPE.VALIDATION), - components, options, - cb + schemaParam.schema, + PARAMETER_SOURCE.REQUEST, + components, options, schemaCache, cb ); }, 0); }, (err, res) => { - let mismatches = []; + let mismatches = [], + mismatchObj; + _.each(_.filter(schemaParams, (q) => { return q.required; }), (qp) => { if (!_.find(requestQueryParams, (param) => { return param.key === qp.name; })) { - mismatches.push({ + mismatchObj = { property: mismatchProperty, - transactionJsonPath: null, + 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) + } + }; + } + mismatches.push(mismatchObj); } }); return callback(null, _.concat(_.flatten(res), mismatches)); @@ -2557,7 +2780,7 @@ module.exports = { }, checkRequestHeaders: function (headers, transactionPathPrefix, schemaPath, components, options, - schemaResolutionCache, callback) { + schemaCache, callback) { let schemaHeaders = _.filter(schemaPath.parameters, (param) => { return param.in === 'header'; }), mismatchProperty = 'HEADER'; @@ -2567,7 +2790,9 @@ module.exports = { // 1. for each header, find relevant schemaPath property return async.map(headers, (pHeader, cb) => { - let mismatches = []; + let mismatches = [], + index = _.findIndex(headers, pHeader); + const schemaHeader = _.find(schemaHeaders, (header) => { return header.name === pHeader.key; }); if (!schemaHeader) { @@ -2575,7 +2800,7 @@ module.exports = { if (options.showMissingInSchemaErrors) { mismatches.push({ property: mismatchProperty, - transactionJsonPath: transactionPathPrefix + '[?(@.key==\'' + pHeader.key + '\')]', + transactionJsonPath: transactionPathPrefix + `[${index}]`, schemaJsonPath: null, reasonCode: 'MISSING_IN_SCHEMA', reason: `The header ${pHeader.key} was not found in the schema` @@ -2591,27 +2816,41 @@ module.exports = { return cb(null, []); } this.checkValueAgainstSchema(mismatchProperty, - transactionPathPrefix + '[?(@.key==\'' + pHeader.key + '\')]', + transactionPathPrefix + `[${index}].value`, pHeader.key, pHeader.value, schemaHeader.pathPrefix + '[?(@.name==\'' + schemaHeader.name + '\')]', - deref.resolveRefs(schemaHeader.schema, 'request', components, schemaResolutionCache, - PROCESSING_TYPE.VALIDATION), - components, options, - cb + schemaHeader.schema, + PARAMETER_SOURCE.REQUEST, + components, options, schemaCache, cb ); }, 0); }, (err, res) => { - let mismatches = []; + let mismatches = [], + mismatchObj; + _.each(_.filter(schemaHeaders, (h) => { return h.required; }), (header) => { if (!_.find(headers, (param) => { return param.key === header.name; })) { - mismatches.push({ + mismatchObj = { property: mismatchProperty, - transactionJsonPath: null, + 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) + } + }; + } + mismatches.push(mismatchObj); } }); return callback(null, _.concat(_.flatten(res), mismatches)); @@ -2619,7 +2858,7 @@ module.exports = { }, checkResponseHeaders: function (schemaResponse, headers, transactionPathPrefix, schemaPathPrefix, - components, options, schemaResolutionCache, callback) { + components, options, schemaCache, callback) { // 0. Need to find relevant response from schemaPath.responses let schemaHeaders, mismatchProperty = 'RESPONSE_HEADER'; @@ -2628,30 +2867,31 @@ module.exports = { return callback(null, []); } - if (!schemaResponse || !(schemaResponse.headers)) { - // no default response found, or no headers specified - // if there is no header key, we can't call it a mismatch + if (!schemaResponse) { + // no default response found, we can't call it a mismatch return callback(null, []); } schemaHeaders = schemaResponse.headers; return async.map(headers, (pHeader, cb) => { - let mismatches = []; - const schemaHeader = schemaHeaders[pHeader.key]; + let mismatches = [], + index = _.findIndex(headers, pHeader); + + const schemaHeader = _.get(schemaHeaders, pHeader.key); if (!schemaHeader) { // no schema header found if (options.showMissingInSchemaErrors) { mismatches.push({ property: mismatchProperty, - transactionJsonPath: transactionPathPrefix + '/' + pHeader.key, + transactionJsonPath: transactionPathPrefix + `[${index}]`, schemaJsonPath: schemaPathPrefix + '.headers', reasonCode: 'MISSING_IN_SCHEMA', reason: `The header ${pHeader.key} was not found in the schema` }); } - return cb(null, []); + return cb(null, mismatches); } // header found in spec. check header's schema @@ -2661,31 +2901,44 @@ module.exports = { return cb(null, []); } return this.checkValueAgainstSchema(mismatchProperty, - transactionPathPrefix + '/' + pHeader.key, + transactionPathPrefix + `[${index}].value`, pHeader.key, pHeader.value, schemaPathPrefix + '.headers[' + pHeader.key + ']', - deref.resolveRefs(schemaHeader.schema, 'response', components, schemaResolutionCache, - PROCESSING_TYPE.VALIDATION), - components, - options, - cb + schemaHeader.schema, + PARAMETER_SOURCE.RESPONSE, + components, options, schemaCache, cb ); }, 0); }, (err, res) => { - let mismatches = []; + let mismatches = [], + mismatchObj; + _.each(_.filter(schemaHeaders, (h, hName) => { h.name = hName; return h.required; }), (header) => { if (!_.find(headers, (param) => { return param.key === header.name; })) { - mismatches.push({ + mismatchObj = { property: mismatchProperty, - transactionJsonPath: null, + 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) + } + }; + } + mismatches.push(mismatchObj); } }); callback(null, _.concat(_.flatten(res), mismatches)); @@ -2694,7 +2947,7 @@ module.exports = { // Only application/json is validated for now checkRequestBody: function (requestBody, transactionPathPrefix, schemaPathPrefix, schemaPath, - components, options, schemaResolutionCache, callback) { + components, options, schemaCache, callback) { // check for body modes // TODO: The application/json can be anything that's application/*+json let jsonSchemaBody = _.get(schemaPath, ['requestBody', 'content', 'application/json', 'schema']), @@ -2712,10 +2965,11 @@ module.exports = { null, // no param name for the request body requestBody.raw, schemaPathPrefix + '.requestBody.content[application/json].schema', - deref.resolveRefs(jsonSchemaBody, 'request', components, schemaResolutionCache, - PROCESSING_TYPE.VALIDATION), + jsonSchemaBody, + PARAMETER_SOURCE.REQUEST, components, _.extend({}, options, { shortValidationErrors: true }), + schemaCache, callback ); }, 0); @@ -2726,7 +2980,7 @@ module.exports = { }, checkResponseBody: function (schemaResponse, body, transactionPathPrefix, schemaPathPrefix, - components, options, schemaResolutionCache, callback) { + components, options, schemaCache, callback) { let schemaContent = _.get(schemaResponse, ['content', 'application/json', 'schema']), mismatchProperty = 'RESPONSE_BODY'; @@ -2754,17 +3008,18 @@ module.exports = { null, // no param name for the response body body, schemaPathPrefix + '.content[application/json].schema', - deref.resolveRefs(schemaContent, 'response', components, schemaResolutionCache, - PROCESSING_TYPE.VALIDATION), + schemaContent, + PARAMETER_SOURCE.RESPONSE, components, _.extend({}, options, { shortValidationErrors: true }), + schemaCache, callback ); }, 0); }, checkResponses: function (responses, transactionPathPrefix, schemaPathPrefix, schemaPath, - components, options, schemaResolutionCache, cb) { + 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 @@ -2790,13 +3045,13 @@ module.exports = { headers: (cb) => { this.checkResponseHeaders(thisSchemaResponse, response.header, transactionPathPrefix + '[' + response.id + '].header', - schemaPathPrefix + '.responses.' + responsePathPrefix, components, options, schemaResolutionCache, cb); + 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, schemaResolutionCache, cb); + schemaPathPrefix + '.responses.' + responsePathPrefix, components, options, schemaCache, cb); } }, (err, result) => { return responseCallback(null, { @@ -2815,9 +3070,10 @@ module.exports = { /** * @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) { + getPostmanUrlSchemaMatchScore: function (postmanPath, schemaPath, options) { var postmanPathArr = _.reject(postmanPath.split('/'), (segment) => { return segment === ''; }), @@ -2827,6 +3083,8 @@ module.exports = { matchedPathVars = null, maxScoreFound = -Infinity, anyMatchFound = false, + fixedMatchedSegments, + variableMatchedSegments, postmanPathSuffixes = []; // get array with all suffixes of postmanPath @@ -2841,10 +3099,14 @@ module.exports = { // for each suffx, calculate score against the schemaPath // the schema<>postman score is the sum _.each(postmanPathSuffixes, (pps) => { - let suffixMatchResult = this.getPostmanUrlSuffixSchemaScore(pps, schemaPathArr); + 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; } }); @@ -2853,7 +3115,9 @@ module.exports = { return { match: true, score: maxScoreFound, - pathVars: matchedPathVars + pathVars: matchedPathVars, + fixedMatchedSegments, + variableMatchedSegments }; } return { @@ -2862,19 +3126,32 @@ module.exports = { }, /** - * @param {*} pmSuffix - * @param {*} schemaPath + * @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) { + 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; + 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; + + 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}} @@ -2884,8 +3161,22 @@ module.exports = { (schemaPath[sMax - i] === pmSuffix[pMax - i]) || // exact match (schemaPath[sMax - i].startsWith('{') && schemaPath[sMax - i].endsWith('}')) || // schema segment is a pathVar (pmSuffix[pMax - i].startsWith(':')) || // postman segment is a pathVar - (pmSuffix[pMax - i].startsWith('{{') && pmSuffix[pMax - i].endsWith('}}')) // postman segment is an env var + (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 ( + (schemaPath[sMax - i].startsWith('{') && schemaPath[sMax - i].endsWith('}')) && // 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 (schemaPath[sMax - i] === pmSuffix[pMax - i]) { + fixedMatchedSegments++; + } + // add a matched path variable only if the schema one was a pathVar if (schemaPath[sMax - i].startsWith('{') && schemaPath[sMax - i].endsWith('}')) { variables.push({ @@ -2911,6 +3202,8 @@ module.exports = { // 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: variables }; } @@ -2919,5 +3212,96 @@ module.exports = { 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 = _.get(actualValue, dataPath, {}), + targetFakedValue = _.get(fakedValue, dataPath, {}); + + // discard the leading '.' if it exists + if (dataPath[0] === '.') { + dataPath = dataPath.slice(1); + } + + 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 = _.get(fakedValue, dataPath, null); + break; + } + + return suggestedValue; + }, + + /** + * @param {Object} schemaPaths - OpenAPI Paths object + * @param {Array} matchedEndpoints - All matched endpoints + * @returns {Array} - Array of all MISSING_ENDPOINT objects + */ + getMissingSchemaEndpoints: function (schemaPaths, matchedEndpoints) { + let endpoints = [], + schemaJsonPath; + + _.forEach(schemaPaths, (schemaPathObj, schemaPath) => { + _.forEach(_.keys(schemaPathObj), (pathKey) => { + schemaJsonPath = `$.paths[${schemaPath}].${_.toLower(pathKey)}`; + if (METHODS.includes(pathKey) && !matchedEndpoints.includes(schemaJsonPath)) { + endpoints.push({ + property: 'ENDPOINT', + transactionJsonPath: null, + schemaJsonPath, + reasonCode: 'MISSING_ENDPOINT', + reason: `The endpoint "${_.toUpper(pathKey)} ${schemaPath}" is missing in collection`, + endpoint: { + method: _.toUpper(pathKey), + path: schemaPath + } + }); + } + }); + }); + return endpoints; } }; diff --git a/lib/schemapack.js b/lib/schemapack.js index ccee0bb32..fcc1a575d 100644 --- a/lib/schemapack.js +++ b/lib/schemapack.js @@ -324,7 +324,11 @@ class SchemaPack { let schema = this.openapi, componentsAndPaths, options = this.computedOptions, - schemaResolutionCache = this.schemaResolutionCache; + schemaCache = { + schemaResolutionCache: this.schemaResolutionCache, + schemaFakerCache: this.schemaFakerCache + }, + matchedEndpoints = []; if (!this.validated) { @@ -362,6 +366,13 @@ class SchemaPack { let requestUrl = transaction.request.url, matchedPaths; if (typeof requestUrl === 'object') { + + // SDK.Url.toString() resolves pathvar to empty string if value is empty + // so update path variable value to same as key in such cases + _.forEach(requestUrl.variable, (pathVar) => { + _.isEmpty(pathVar.value) && (pathVar.value = ':' + pathVar.key); + }); + // SDK URL object. Get raw string representation. requestUrl = (new sdk.Url(requestUrl)).toString(); } @@ -370,7 +381,8 @@ class SchemaPack { matchedPaths = schemaUtils.findMatchingRequestFromSchema( transaction.request.method, requestUrl, - schema + schema, + options ); if (!matchedPaths.length) { @@ -384,30 +396,35 @@ class SchemaPack { return setTimeout(() => { // 2. perform validation for each identified matchedPath (schema endpoint) return async.map(matchedPaths, (matchedPath, pathsCallback) => { + matchedEndpoints.push(matchedPath.jsonPath); // 3. validation involves checking these individual properties async.parallel({ + metadata: function(cb) { + schemaUtils.checkMetadata(transaction, '$', matchedPath.path, matchedPath.name, options, cb); + }, path: function(cb) { - schemaUtils.checkPathVariables(matchedPath.pathVariables, '$.request.url', matchedPath.path, - componentsAndPaths, options, schemaResolutionCache, cb); + schemaUtils.checkPathVariables(matchedPath.pathVariables, '$.request.url.variable', matchedPath.path, + componentsAndPaths, options, schemaCache, cb); }, queryparams: function(cb) { schemaUtils.checkQueryParams(requestUrl, '$.request.url.query', matchedPath.path, - componentsAndPaths, options, schemaResolutionCache, cb); + componentsAndPaths, options, schemaCache, cb); }, headers: function(cb) { schemaUtils.checkRequestHeaders(transaction.request.header, '$.request.header', matchedPath.path, - componentsAndPaths, options, schemaResolutionCache, cb); + componentsAndPaths, options, schemaCache, cb); }, requestBody: function(cb) { schemaUtils.checkRequestBody(transaction.request.body, '$.request.body', matchedPath.jsonPath, - matchedPath.path, componentsAndPaths, options, schemaResolutionCache, cb); + matchedPath.path, componentsAndPaths, options, schemaCache, cb); }, responses: function (cb) { schemaUtils.checkResponses(transaction.response, '$.responses', matchedPath.jsonPath, - matchedPath.path, componentsAndPaths, options, schemaResolutionCache, cb); + matchedPath.path, componentsAndPaths, options, schemaCache, cb); } }, (err, result) => { - let allMismatches = _.concat(result.queryparams, result.headers, result.path, result.requestBody), + let allMismatches = _.concat(result.metadata, result.queryparams, result.headers, result.path, + result.requestBody), responseMismatchesPresent = false, retVal; @@ -469,7 +486,8 @@ class SchemaPack { }); retVal = { - requests: _.keyBy(result, 'requestId') + requests: _.keyBy(result, 'requestId'), + missingEndpoints: schemaUtils.getMissingSchemaEndpoints(schema.paths, matchedEndpoints) }; callback(null, retVal); diff --git a/package.json b/package.json index 1a5ec271b..1addbf0ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openapi-to-postmanv2", - "version": "1.1.19", + "version": "1.1.20", "description": "Convert a given OpenAPI specification to Postman Collection v2.0", "homepage": "https://github.com/postmanlabs/openapi-to-postman", "bugs": "https://github.com/postmanlabs/openapi-to-postman/issues", diff --git a/test/data/valid_openapi/example_in_schema.json b/test/data/valid_openapi/example_in_schema.json index 158385001..2c892ea38 100644 --- a/test/data/valid_openapi/example_in_schema.json +++ b/test/data/valid_openapi/example_in_schema.json @@ -16,7 +16,7 @@ "schema": { "type": "integer", "format": "int32", - "example": "header example" + "example": 123 } }, { @@ -29,7 +29,7 @@ "items": { "type": "integer", "format": "int64", - "example": "queryParamExample" + "example": 123 } } }, @@ -43,7 +43,7 @@ "items": { "type": "integer", "format": "int64", - "example": "queryParamExample1" + "example": 456 } } } diff --git a/test/data/valid_openapi/multiple_refs.json b/test/data/valid_openapi/multiple_refs.json index 3550fd1b2..b8b53d145 100644 --- a/test/data/valid_openapi/multiple_refs.json +++ b/test/data/valid_openapi/multiple_refs.json @@ -81,7 +81,7 @@ "responseId": { "type": "integer", "format": "int64", - "example": "234" + "example": 234 }, "responseName": { "type":"string", diff --git a/test/system/structure.test.js b/test/system/structure.test.js index 6f4b1a1bc..81263ccce 100644 --- a/test/system/structure.test.js +++ b/test/system/structure.test.js @@ -11,7 +11,11 @@ const optionIds = [ 'shortValidationErrors', 'validationPropertiesToIgnore', 'showMissingInSchemaErrors', - 'detailedBlobValidation' + 'detailedBlobValidation', + 'suggestAvailableFixes', + 'validateMetadata', + 'ignoreUnresolvedVariables', + 'strictRequestMatching' ], expectedOptions = { collapseFolders: { @@ -92,6 +96,31 @@ const optionIds = [ default: false, description: 'Determines whether to show detailed mismatch information for application/json content ' + 'in the request/response body.' + }, + suggestAvailableFixes: { + name: 'Suggest fixes if available', + type: 'boolean', + default: false, + description: 'Whether to provide fixes for patching corresponding mismatches.' + }, + validateMetadata: { + name: 'Show Metadata validation messages', + type: 'boolean', + default: false, + description: 'Whether to show mismatches for incorrect name and description of request' + }, + ignoreUnresolvedVariables: { + name: 'Ignore mismatch for unresolved postman variables', + type: 'boolean', + default: false, + description: 'Whether to ignore mismatches resulting from unresolved variables in the Postman request' + }, + strictRequestMatching: { + name: 'Enable strict request matching', + type: 'boolean', + default: false, + description: 'Whether requests should be strictly matched with schema operations. Setting to true will not ' + + 'include any matches where the URL path segments don\'t match exactly.' } }; diff --git a/test/unit/base.test.js b/test/unit/base.test.js index b42e2f3fe..3184bffc4 100644 --- a/test/unit/base.test.js +++ b/test/unit/base.test.js @@ -284,8 +284,9 @@ describe('CONVERT FUNCTION TESTS ', function() { }); it('Should respects readOnly and writeOnly properties in requestBody or response schema' + readOnlySpec, function(done) { - var openapi = fs.readFileSync(readOnlySpec, 'utf8'); - Converter.convert({ type: 'string', data: openapi }, { schemaFaker: true }, (err, conversionResult) => { + var openapi = fs.readFileSync(readOnlySpec, 'utf8'), + options = { schemaFaker: true, exampleParametersResolution: 'schema' }; + Converter.convert({ type: 'string', data: openapi }, options, (err, conversionResult) => { let requestBody = conversionResult.output[0].data.item[0].item[1].request.body.raw, responseBody = conversionResult.output[0].data.item[0].item[0].response[0].body; expect(err).to.be.null; @@ -343,26 +344,30 @@ describe('CONVERT FUNCTION TESTS ', function() { .equal('{\n "a": "example-a",\n "b": "example-b"\n}'); // Request header expect(rootRequest.header[0].value).to.equal(''); - expect(exampleRequest.header[0].value).to.equal('header example'); + expect(exampleRequest.header[0].value).to.equal(123); // Request query parameters expect(rootRequest.url.query[0].value).to.equal(' '); expect(rootRequest.url.query[1].value).to.equal(' '); - expect(exampleRequest.url.query[0].value).to.equal('queryParamExample queryParamExample'); - expect(exampleRequest.url.query[1].value).to.equal('queryParamExample1 queryParamExample1'); + expect(exampleRequest.url.query[0].value).to.equal('123 123'); + expect(exampleRequest.url.query[1].value).to.equal('456 456'); done(); }); }); - it('Should fallback to schema if the example is not present in the spec and the option is set to example' + + it('Should fallback to faked value if the example is not present in the spec and the option is set to example' + schemaWithoutExampleSpec, function(done) { Converter.convert({ type: 'file', data: schemaWithoutExampleSpec }, { schemaFaker: true, requestParametersResolution: 'example', exampleParametersResolution: 'example' }, (err, conversionResult) => { - let rootRequest = conversionResult.output[0].data.item[0].request, - exampleRequest = conversionResult.output[0].data.item[0].response[0].originalRequest; - expect(exampleRequest.body.raw).to - .equal('{\n "a": "",\n "b": ""\n}'); - expect(rootRequest.body.raw).to - .equal('{\n "a": "",\n "b": ""\n}'); + let rootRequestBody = JSON.parse(conversionResult.output[0].data.item[0].request.body.raw), + exampleRequestBody = JSON.parse(conversionResult.output[0].data.item[0] + .response[0].originalRequest.body.raw); + + expect(rootRequestBody).to.have.all.keys(['a', 'b']); + expect(rootRequestBody.a).to.be.a('string'); + expect(rootRequestBody.b).to.be.a('string'); + expect(exampleRequestBody).to.have.all.keys(['a', 'b']); + expect(exampleRequestBody.a).to.be.a('string'); + expect(exampleRequestBody.b).to.be.a('string'); done(); }); }); @@ -627,11 +632,11 @@ describe('CONVERT FUNCTION TESTS ', function() { }); expect(responseBody).to.deep.equal({ key1: { - responseId: '234', + responseId: 234, responseName: '200 OK Response' }, key2: { - responseId: '234', + responseId: 234, responseName: '200 OK Response' } }); @@ -734,10 +739,12 @@ describe('CONVERT FUNCTION TESTS ', function() { describe('[Github #57] - folderStrategy option (value: Tags) ' + tagsFolderSpec, function() { async.series({ pathsOutput: (cb) => { - Converter.convert({ type: 'file', data: tagsFolderSpec }, { folderStrategy: 'Paths' }, cb); + Converter.convert({ type: 'file', data: tagsFolderSpec }, + { folderStrategy: 'Paths', exampleParametersResolution: 'schema' }, cb); }, tagsOutput: (cb) => { - Converter.convert({ type: 'file', data: tagsFolderSpec }, { folderStrategy: 'Tags' }, cb); + Converter.convert({ type: 'file', data: tagsFolderSpec }, + { folderStrategy: 'Tags', exampleParametersResolution: 'schema' }, cb); } }, (err, res) => { var collectionItems, @@ -909,6 +916,9 @@ describe('INTERFACE FUNCTION TESTS ', function () { var specPath = path.join(__dirname, pathPrefix, sample); it('Should generate collection conforming to schema for and fail if not valid ' + specPath, function(done) { // var openapi = fs.readFileSync(specPath, 'utf8'); + + // Increase timeout for larger schema + this.timeout(15000); var result = Converter.validate({ type: 'file', data: specPath }); expect(result.result).to.equal(true); Converter.convert({ type: 'file', data: specPath }, diff --git a/test/unit/deref.test.js b/test/unit/deref.test.js index 665ace6dd..31bb00a39 100644 --- a/test/unit/deref.test.js +++ b/test/unit/deref.test.js @@ -117,8 +117,7 @@ describe('DEREF FUNCTION TESTS ', function() { expect(output_validationTypeArray).to.deep.include({ type: 'array', items: { - type: 'string', - default: '' + type: 'string' }, minItems: 5, maxItems: 55 diff --git a/test/unit/util.test.js b/test/unit/util.test.js index 4b9f45df8..51931a839 100644 --- a/test/unit/util.test.js +++ b/test/unit/util.test.js @@ -66,9 +66,11 @@ describe('SCHEMA UTILITY FUNCTION TESTS ', function () { } }, parameterSource = 'REQUEST', - resolveTo = 'schema'; + resolveTo = 'schema', + resolveFor = 'CONVERSION'; - expect(SchemaUtils.safeSchemaFaker(schema, resolveTo, parameterSource, { components })).to.equal(''); + expect(SchemaUtils.safeSchemaFaker(schema, resolveTo, resolveFor, parameterSource, { components })) + .to.equal(''); done(); }); @@ -96,8 +98,9 @@ describe('SCHEMA UTILITY FUNCTION TESTS ', function () { }, parameterSource = 'REQUEST', resolveTo = 'schema', + resolveFor = 'CONVERSION', - result = SchemaUtils.safeSchemaFaker(schema, resolveTo, parameterSource, { components }), + result = SchemaUtils.safeSchemaFaker(schema, resolveTo, resolveFor, parameterSource, { components }), tooManyLevelsString = result[0].c.value; expect(result).to.not.equal(null); @@ -135,7 +138,8 @@ describe('SCHEMA UTILITY FUNCTION TESTS ', function () { }, parameterSource = 'REQUEST', resolveTo = 'schema', - fakedSchema = SchemaUtils.safeSchemaFaker(schema, resolveTo, parameterSource, { components }); + resolveFor = 'CONVERSION', + fakedSchema = SchemaUtils.safeSchemaFaker(schema, resolveTo, resolveFor, parameterSource, { components }); expect(fakedSchema.value).to.equal('reference #/components/schem2 not found in the OpenAPI spec'); done(); @@ -158,13 +162,14 @@ describe('SCHEMA UTILITY FUNCTION TESTS ', function () { }, parameterSource = 'REQUEST', resolveTo = 'schema', - resolvedSchema = deref.resolveRefs(schema, parameterSource, { components }, {}), + resolveFor = 'CONVERSION', + resolvedSchema = deref.resolveRefs(schema, parameterSource, { components }, {}, resolveFor, resolveTo), schemaCache = { schemaFakerCache: {}, schemaResolutionCache: {} }, key = hash('resolveToSchema ' + JSON.stringify(resolvedSchema)), - fakedSchema = SchemaUtils.safeSchemaFaker(schema, resolveTo, parameterSource, + fakedSchema = SchemaUtils.safeSchemaFaker(schema, resolveTo, resolveFor, parameterSource, { components }, 'default', ' ', schemaCache); expect(schemaCache.schemaFakerCache).to.have.property(key); @@ -194,13 +199,15 @@ describe('SCHEMA UTILITY FUNCTION TESTS ', function () { }, parameterSource = 'RESPONSE', resolveTo = 'example', + resolveFor = 'CONVERSION', schemaCache = { schemaFakerCache: {}, schemaResolutionCache: {} }, - resolvedSchema = deref.resolveRefs(schema, parameterSource, { components }, schemaCache.schemaResolutionCache), + resolvedSchema = deref.resolveRefs(schema, parameterSource, { components }, schemaCache.schemaResolutionCache, + resolveFor, resolveTo), key = hash('resolveToExample ' + JSON.stringify(resolvedSchema)), - fakedSchema = SchemaUtils.safeSchemaFaker(schema, resolveTo, parameterSource, + fakedSchema = SchemaUtils.safeSchemaFaker(schema, resolveTo, resolveFor, parameterSource, { components }, 'default', ' ', schemaCache); expect(schemaCache.schemaFakerCache).to.have.property(key); @@ -633,7 +640,7 @@ describe('SCHEMA UTILITY FUNCTION TESTS ', function () { expect(retVal).to.be.an('array'); expect(retVal[0].key).to.equal('varName'); expect(retVal[0].description).to.equal('varDesc'); - expect(retVal[0].value).to.equal(''); + expect(retVal[0].value).to.be.a('number'); }); }); @@ -2123,7 +2130,7 @@ describe('convertToPmQueryArray function', function() { items: { type: 'integer', format: 'int64', - example: 'queryParamExample' + example: 123 } } }, { name: 'variable3', in: 'query', @@ -2133,7 +2140,7 @@ describe('convertToPmQueryArray function', function() { items: { type: 'integer', format: 'int64', - example: 'queryParamExample1' + example: 456 } } }] }, requestType = 'EXAMPLE', result; @@ -2142,7 +2149,7 @@ describe('convertToPmQueryArray function', function() { exampleParametersResolution: 'example', requestParametersResolution: 'schema' }); - expect(result[0]).to.equal('variable2=queryParamExample queryParamExample'); - expect(result[1]).to.equal('variable3=queryParamExample1 queryParamExample1'); + expect(result[0]).to.equal('variable2=123 123'); + expect(result[1]).to.equal('variable3=456 456'); }); });