Merge develop into feature/meta-data-function

This commit is contained in:
umeshp7
2020-04-27 21:01:19 +05:30
20 changed files with 1151 additions and 192 deletions

View File

@@ -1,4 +1,11 @@
# OpenAPI-Postman Changelog # OpenAPI-Postman Changelog
#### v1.1.13 (April 21, 2020)
* Added support for detailed validation body mismatches with option detailedBlobValidation.
* Fix for [#8098](https://github.com/postmanlabs/postman-app-support/issues/8098) - Unable to validate schema with type array.
* Fixed URIError for invalid URI in transaction.
* Fix for [#152](https://github.com/postmanlabs/openapi-to-postman/issues/152) - Path references not resolved due to improver handling of special characters.
* Fix for [#160](https://github.com/postmanlabs/openapi-to-postman/issues/160) - Added handling for variables in local servers not a part of a URL segment. All path servers to be added as collection variables.
* Unresolved refs will not be stored in schemaResolutionCache.
#### v1.1.12 (Mar 26, 2020) #### v1.1.12 (Mar 26, 2020)
* Fix for https://github.com/postmanlabs/openapi-to-postman/issues/133 and https://github.com/postmanlabs/openapi-to-postman/issues/101 * Fix for https://github.com/postmanlabs/openapi-to-postman/issues/133 and https://github.com/postmanlabs/openapi-to-postman/issues/101

View File

@@ -28,8 +28,8 @@ module.exports = {
schema.mergeAndValidate(cb); schema.mergeAndValidate(cb);
}, },
getOptions: function() { getOptions: function(mode, criteria) {
return SchemaPack.getOptions(); return SchemaPack.getOptions(mode, criteria);
}, },
// new API // new API

49
lib/ajvValidationError.js Normal file
View File

@@ -0,0 +1,49 @@
var _ = require('lodash');
/**
* This function generates reason and reasonCodes (used in mismatch objects) using Ajv Validation Error
*
* @param {Object} ajvValidationErrorObj Ajv Validation Error Object (reference: https://ajv.js.org/#validation-errors)
* @param {Object} data data needed for generation of mismatch Object
*
* @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'
};
switch (ajvValidationErrorObj.keyword) {
case 'additionalProperties':
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;
case 'required':
mismatchObj.reasonCode = 'MISSING_IN_REQUEST';
mismatchObj.reason = `The ${data.humanPropName} property "${mismatchPropName}" should have required ` +
`property "${_.get(ajvValidationErrorObj, 'params.missingProperty')}"`;
break;
case 'propertyNames':
mismatchObj.reason = `The ${data.humanPropName} property "${mismatchPropName}" contains invalid ` +
`property named "${_.get(ajvValidationErrorObj, 'params.propertyName')}"`;
break;
default:
break;
}
return mismatchObj;
}
module.exports = ajvValidationError;

View File

@@ -54,23 +54,28 @@ module.exports = {
* @param {string} parameterSourceOption tells that the schema object is of request or response * @param {string} parameterSourceOption tells that the schema object is of request or response
* @param {*} components components in openapi spec. * @param {*} components components in openapi spec.
* @param {object} schemaResolutionCache stores already resolved references * @param {object} schemaResolutionCache stores already resolved references
* @param {*} resolveFor - resolve refs for validation/conversion (value to be one of VALIDATION/CONVERSION)
* @param {*} stack counter which keeps a tab on nested schemas * @param {*} stack counter which keeps a tab on nested schemas
* @param {*} seenRef References that are repeated. Used to identify circular references. * @param {*} seenRef References that are repeated. Used to identify circular references.
* @returns {*} schema - schema that adheres to all individual schemas in schemaArr * @returns {*} schema - schema that adheres to all individual schemas in schemaArr
*/ */
resolveAllOf: function (schemaArr, parameterSourceOption, components, schemaResolutionCache, stack = 0, seenRef) { resolveAllOf: function (schemaArr, parameterSourceOption, components, schemaResolutionCache,
resolveFor = 'CONVERSION', stack = 0, seenRef) {
if (!(schemaArr instanceof Array)) { if (!(schemaArr instanceof Array)) {
return null; return null;
} }
if (schemaArr.length === 1) { if (schemaArr.length === 1) {
// for just one entry in allOf, don't need to enforce type: object restriction // for just one entry in allOf, don't need to enforce type: object restriction
return this.resolveRefs(schemaArr[0], parameterSourceOption, components, schemaResolutionCache, stack, seenRef); return this.resolveRefs(schemaArr[0], parameterSourceOption, components, schemaResolutionCache, resolveFor,
stack, seenRef);
} }
// generate one object for each schema // generate one object for each schema
let indivObjects = schemaArr.map((schema) => { let indivObjects = schemaArr.map((schema) => {
return this.resolveRefs(schema, parameterSourceOption, components, schemaResolutionCache, stack, seenRef); return this.resolveRefs(schema, parameterSourceOption, components, schemaResolutionCache, resolveFor,
stack, seenRef);
}).filter((schema) => { }).filter((schema) => {
return schema.type === 'object'; return schema.type === 'object';
}), }),
@@ -107,16 +112,22 @@ module.exports = {
* @param {string} parameterSourceOption tells that the schema object is of request or response * @param {string} parameterSourceOption tells that the schema object is of request or response
* @param {*} components components in openapi spec. * @param {*} components components in openapi spec.
* @param {object} schemaResolutionCache stores already resolved references * @param {object} schemaResolutionCache stores already resolved references
* @param {*} resolveFor - resolve refs for validation/conversion (value to be one of VALIDATION/CONVERSION)
* @param {*} stack counter which keeps a tab on nested schemas * @param {*} stack counter which keeps a tab on nested schemas
* @param {*} seenRef - References that are repeated. Used to identify circular references. * @param {*} seenRef - References that are repeated. Used to identify circular references.
* @returns {*} schema satisfying JSON-schema-faker. * @returns {*} schema satisfying JSON-schema-faker.
*/ */
resolveRefs: function (schema, parameterSourceOption, components, schemaResolutionCache, stack = 0, seenRef = {}) {
var resolvedSchema, prop, splitRef; resolveRefs: function (schema, parameterSourceOption, components, schemaResolutionCache,
resolveFor = 'CONVERSION', stack = 0, seenRef = {}) {
var resolvedSchema, prop, splitRef,
ERR_TOO_MANY_LEVELS = '<Error: Too many levels of nesting to fake this schema>';
stack++; stack++;
schemaResolutionCache = schemaResolutionCache || {}; schemaResolutionCache = schemaResolutionCache || {};
if (stack > 20) { if (stack > 20) {
return { value: '<Error: Too many levels of nesting to fake this schema>' }; return { value: ERR_TOO_MANY_LEVELS };
} }
if (!schema) { if (!schema) {
@@ -124,16 +135,16 @@ module.exports = {
} }
if (schema.anyOf) { if (schema.anyOf) {
return this.resolveRefs(schema.anyOf[0], parameterSourceOption, components, schemaResolutionCache, stack, return this.resolveRefs(schema.anyOf[0], parameterSourceOption, components, schemaResolutionCache, resolveFor,
_.cloneDeep(seenRef)); stack, _.cloneDeep(seenRef));
} }
if (schema.oneOf) { if (schema.oneOf) {
return this.resolveRefs(schema.oneOf[0], parameterSourceOption, components, schemaResolutionCache, stack, return this.resolveRefs(schema.oneOf[0], parameterSourceOption, components, schemaResolutionCache, resolveFor,
_.cloneDeep(seenRef)); stack, _.cloneDeep(seenRef));
} }
if (schema.allOf) { if (schema.allOf) {
return this.resolveAllOf(schema.allOf, parameterSourceOption, components, schemaResolutionCache, stack, return this.resolveAllOf(schema.allOf, parameterSourceOption, components, schemaResolutionCache, resolveFor,
_.cloneDeep(seenRef)); stack, _.cloneDeep(seenRef));
} }
if (schema.$ref && _.isFunction(schema.$ref.split)) { if (schema.$ref && _.isFunction(schema.$ref.split)) {
let refKey = schema.$ref; let refKey = schema.$ref;
@@ -160,12 +171,27 @@ module.exports = {
// will be resolved - we don't care about anything after the components part // will be resolved - we don't care about anything after the components part
// splitRef.slice(1) will return ['components', 'schemas', 'PaginationEnvelope', 'properties', 'page'] // splitRef.slice(1) will return ['components', 'schemas', 'PaginationEnvelope', 'properties', 'page']
// not using _.get here because that fails if there's a . in the property name (Pagination.Envelope, for example) // not using _.get here because that fails if there's a . in the property name (Pagination.Envelope, for example)
resolvedSchema = this._getEscaped(components, splitRef.slice(1));
splitRef = splitRef.slice(1).map((elem) => {
// https://swagger.io/docs/specification/using-ref#escape
// since / is the default delimiter, slashes are escaped with ~1
return decodeURIComponent(
elem
.replace(/~1/g, '/')
.replace(/~0/g, '~')
);
});
resolvedSchema = this._getEscaped(components, splitRef);
if (resolvedSchema) { if (resolvedSchema) {
let refResolvedSchema = this.resolveRefs(resolvedSchema, parameterSourceOption, let refResolvedSchema = this.resolveRefs(resolvedSchema, parameterSourceOption,
components, schemaResolutionCache, stack, _.cloneDeep(seenRef)); components, schemaResolutionCache, resolveFor, stack, _.cloneDeep(seenRef));
if (refResolvedSchema && refResolvedSchema.value !== ERR_TOO_MANY_LEVELS) {
schemaResolutionCache[refKey] = refResolvedSchema; schemaResolutionCache[refKey] = refResolvedSchema;
}
return refResolvedSchema; return refResolvedSchema;
} }
return { value: 'reference ' + schema.$ref + ' not found in the OpenAPI spec' }; return { value: 'reference ' + schema.$ref + ' not found in the OpenAPI spec' };
@@ -196,7 +222,7 @@ module.exports = {
} }
/* eslint-enable */ /* eslint-enable */
tempSchema.properties[prop] = this.resolveRefs(property, tempSchema.properties[prop] = this.resolveRefs(property,
parameterSourceOption, components, schemaResolutionCache, stack, _.cloneDeep(seenRef)); parameterSourceOption, components, schemaResolutionCache, resolveFor, stack, _.cloneDeep(seenRef));
} }
} }
return tempSchema; return tempSchema;
@@ -207,15 +233,19 @@ module.exports = {
} }
else if (schema.type === 'array' && schema.items) { else if (schema.type === 'array' && schema.items) {
// This nonsense is needed because the schemaFaker doesn't respect options.maxItems/minItems // For VALIDATION - keep minItems and maxItems properties defined by user in schema as is
// FOR CONVERSION - need to set both properties to 2 for schema faking
if (resolveFor === 'CONVERSION') {
// This is needed because the schemaFaker doesn't respect options.maxItems/minItems
schema.maxItems = 2; schema.maxItems = 2;
schema.minItems = 2; schema.minItems = 2;
}
// have to create a shallow clone of schema object, // have to create a shallow clone of schema object,
// so that the original schema.items object will not change // so that the original schema.items object will not change
// without this, schemas with circular references aren't faked correctly // without this, schemas with circular references aren't faked correctly
let tempSchema = _.omit(schema, 'items'); let tempSchema = _.omit(schema, 'items');
tempSchema.items = this.resolveRefs(schema.items, parameterSourceOption, tempSchema.items = this.resolveRefs(schema.items, parameterSourceOption,
components, schemaResolutionCache, stack, _.cloneDeep(seenRef)); components, schemaResolutionCache, resolveFor, stack, _.cloneDeep(seenRef));
return tempSchema; return tempSchema;
} }
else if (!schema.hasOwnProperty('default')) { else if (!schema.hasOwnProperty('default')) {
@@ -251,8 +281,12 @@ module.exports = {
if (!schema.type) { if (!schema.type) {
schema.type = 'string'; schema.type = 'string';
} }
// For VALIDATION - Keep format as is to perform ajv validation on format
if (resolveFor !== 'VALIDATION') {
delete schema.format; delete schema.format;
} }
}
return schema; return schema;
} }

View File

@@ -12,74 +12,80 @@ module.exports = {
* availableOptions - allowed values (only for type=enum) * availableOptions - allowed values (only for type=enum)
* description - human-readable description of the item * description - human-readable description of the item
* external - whether the option is settable via the API * external - whether the option is settable via the API
* usage - array of supported types of usage (i.e. CONVERSION, VALIDATION)
* *
* @param {string} [mode='document'] Describes use-case. 'document' will return an array * @param {string} [mode='document'] Describes use-case. 'document' will return an array
* with all options being described. 'use' will return the default values of all options * with all options being described. 'use' will return the default values of all options
* @param {Object} criteria Decribes required criteria for options to be returned. can have properties
* external: <boolean>
* usage: <array> (Array of supported usage type - CONVERSION, VALIDATION)
* @returns {mixed} An array or object (depending on mode) that describes available options * @returns {mixed} An array or object (depending on mode) that describes available options
*/ */
getOptions: function(mode = 'document') { getOptions: function(mode = 'document', criteria = {}) {
// Override mode & criteria if first arg is criteria (objects)
if (typeof mode === 'object') {
criteria = mode;
mode = 'document';
}
let optsArray = [ let optsArray = [
{ {
name: 'Set request name source', name: 'Naming requests',
id: 'requestNameSource', id: 'requestNameSource',
type: 'enum', type: 'enum',
default: 'fallback', default: 'Fallback',
availableOptions: ['url', 'fallback'], availableOptions: ['Url', 'Fallback'],
description: 'Option for setting source for a request name', description: 'Determines how the requests inside the generated collection will be named.' +
external: true ' If “Fallback” is selected, the request will be named after one of the following schema' +
' values: `description`, `operationid`, `url`.',
external: true,
usage: ['CONVERSION']
}, },
{ {
name: 'Set indent character', name: 'Set indent character',
id: 'indentCharacter', id: 'indentCharacter',
type: 'enum', type: 'enum',
default: ' ', default: 'Space',
availableOptions: [' ', '\t'], availableOptions: ['Space', 'Tab'],
description: 'Option for setting indentation character', description: 'Option for setting indentation character',
external: true external: true,
usage: ['CONVERSION']
}, },
{ {
name: 'Toggle for collapsing folder for long routes', name: 'Collapse redundant folders',
id: 'collapseFolders', id: 'collapseFolders',
type: 'boolean', type: 'boolean',
default: true, default: true,
description: 'Determines whether the importer should attempt to collapse redundant folders into one.' + description: 'Importing will collapse all folders that have only one child element and lack ' +
'Folders are redundant if they have only one child element, and don\'t' + 'persistent folder-level data.',
'have any folder-level data to persist.', external: true,
external: true usage: ['CONVERSION']
}, },
{ {
name: 'Set root request parameters type', name: 'Request parameter generation',
id: 'requestParametersResolution', id: 'requestParametersResolution',
type: 'enum', type: 'enum',
default: 'schema', default: 'Schema',
availableOptions: ['example', 'schema'], availableOptions: ['Example', 'Schema'],
description: 'Determines how request parameters (query parameters, path parameters, headers,' + description: 'Select whether to generate the request parameters based on the' +
'or the request body) should be generated. Setting this to schema will cause the importer to' + ' [schema](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject) or the' +
'use the parameter\'s schema as an indicator; `example` will cause the example (if provided)' + ' [example](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#exampleObject)' +
'to be picked up.', ' in the schema.',
external: true external: true,
usage: ['CONVERSION']
}, },
{ {
name: 'Set example request and response parameters type', name: 'Response parameter generation',
id: 'exampleParametersResolution', id: 'exampleParametersResolution',
type: 'enum', type: 'enum',
default: 'example', default: 'Example',
availableOptions: ['example', 'schema'], availableOptions: ['Example', 'Schema'],
description: 'Determines how response parameters (query parameters, path parameters, headers,' + description: 'Select whether to generate the response parameters based on the' +
'or the request body) should be generated. Setting this to schema will cause the importer to' + ' [schema](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject) or the' +
'use the parameter\'s schema as an indicator; `example` will cause the example (if provided)' + ' [example](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#exampleObject)' +
'to be picked up.', ' in the schema.',
external: true external: true,
}, usage: ['CONVERSION']
{
name: 'Set folder strategy',
id: 'folderStrategy',
type: 'enum',
default: 'paths',
availableOptions: ['paths', 'tags'],
description: 'Determines whether the importer should attempt to create the folders according' +
'to paths or tags which are given in the spec.',
external: true
}, },
{ {
name: 'Enable Schema Faking', name: 'Enable Schema Faking',
@@ -87,15 +93,17 @@ module.exports = {
type: 'boolean', type: 'boolean',
default: true, default: true,
description: 'Whether or not schemas should be faked.', description: 'Whether or not schemas should be faked.',
external: false external: false,
usage: ['CONVERSION']
}, },
{ {
name: 'Short error messages during request <> schema validation', name: 'Short error messages during request <> schema validation',
id: 'shortValidationErrors', id: 'shortValidationErrors',
type: 'boolean', type: 'boolean',
default: false, default: false,
description: 'Whether detailed error messages are required for request <> schema validation operations', description: 'Whether detailed error messages are required for request <> schema validation operations.',
external: false external: true,
usage: ['VALIDATION']
}, },
{ {
name: 'Properties to ignore during validation', name: 'Properties to ignore during validation',
@@ -105,7 +113,8 @@ module.exports = {
description: 'Specific properties (parts of a request/response pair) to ignore during validation.' + description: 'Specific properties (parts of a request/response pair) to ignore during validation.' +
' Must be sent as an array of strings. Valid inputs in the array: PATHVARIABLE, QUERYPARAM,' + ' Must be sent as an array of strings. Valid inputs in the array: PATHVARIABLE, QUERYPARAM,' +
' HEADER, BODY, RESPONSE_HEADER, RESPONSE_BODY', ' HEADER, BODY, RESPONSE_HEADER, RESPONSE_BODY',
external: true external: true,
usage: ['VALIDATION']
}, },
{ {
name: 'Whether MISSING_IN_SCHEMA mismatches should be returned', name: 'Whether MISSING_IN_SCHEMA mismatches should be returned',
@@ -114,15 +123,48 @@ module.exports = {
default: false, default: false,
description: 'MISSING_IN_SCHEMA indicates that an extra parameter was included in the request. For most ' + description: 'MISSING_IN_SCHEMA indicates that an extra parameter was included in the request. For most ' +
'use cases, this need not be considered an error.', 'use cases, this need not be considered an error.',
external: true external: true,
usage: ['VALIDATION']
},
{
name: 'Show detailed body validation messages',
id: 'detailedBlobValidation',
type: 'boolean',
default: false,
description: 'Determines whether to show detailed mismatch information for application/json content ' +
'in the request/response body.',
external: true,
usage: ['VALIDATION']
} }
]; ];
// Filter options based on criteria
if (_.isObject(criteria)) {
typeof criteria.external === 'boolean' && (optsArray = _.filter(optsArray, { external: criteria.external }));
if (_.isArray(criteria.usage)) {
let tempOptsArray = [];
_.forEach(criteria.usage, (usageCriteria) => {
tempOptsArray = _.concat(tempOptsArray, _.filter(optsArray, (option) => {
return _.includes(option.usage, usageCriteria);
}));
});
optsArray = tempOptsArray;
}
}
if (mode === 'use') { if (mode === 'use') {
// options to be used as default kv-pairs // options to be used as default kv-pairs
let defOptions = {}; let defOptions = {};
_.each(optsArray, (opt) => { _.each(optsArray, (opt) => {
// special handling for indent character as in documentation it states `Tab` and `Space`
// but for the generation mode, we need actual values
if (opt.id === 'indentCharacter') {
defOptions[opt.id] = opt.default === 'tab' ? '\t' : ' ';
}
else {
defOptions[opt.id] = opt.default; defOptions[opt.id] = opt.default;
}
}); });
return defOptions; return defOptions;
} }

View File

@@ -12,6 +12,7 @@ const async = require('async'),
_ = require('lodash'), _ = require('lodash'),
xmlFaker = require('./xmlSchemaFaker.js'), xmlFaker = require('./xmlSchemaFaker.js'),
openApiErr = require('./error.js'), openApiErr = require('./error.js'),
ajvValidationError = require('./ajvValidationError'),
utils = require('./utils.js'), utils = require('./utils.js'),
defaultOptions = require('../lib/options.js').getOptions('use'), defaultOptions = require('../lib/options.js').getOptions('use'),
{ Node, Trie } = require('./trie.js'), { Node, Trie } = require('./trie.js'),
@@ -60,10 +61,15 @@ const async = require('async'),
QUERYPARAM: 'query parameter', QUERYPARAM: 'query parameter',
PATHVARIABLE: 'path variable', PATHVARIABLE: 'path variable',
HEADER: 'header', HEADER: 'header',
REQUEST_BODY: 'request body', BODY: 'request body',
RESPONSE_HEADER: 'response header', RESPONSE_HEADER: 'response header',
RESPONSE_BODY: 'response body' RESPONSE_BODY: 'response body'
}, },
// Specifies types of processing Refs
PROCESSING_TYPE = {
VALIDATION: 'VALIDATION',
CONVERSION: 'CONVERSION'
},
// These are the methods supported in the PathItem schema // These are the methods supported in the PathItem schema
// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#pathItemObject // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#pathItemObject
@@ -121,7 +127,8 @@ function safeSchemaFaker(oldSchema, resolveTo, parameterSourceOption, components
schemaResolutionCache = _.get(schemaCache, 'schemaResolutionCache', {}), schemaResolutionCache = _.get(schemaCache, 'schemaResolutionCache', {}),
schemaFakerCache = _.get(schemaCache, 'schemaFakerCache', {}); schemaFakerCache = _.get(schemaCache, 'schemaFakerCache', {});
resolvedSchema = deref.resolveRefs(oldSchema, parameterSourceOption, components, schemaResolutionCache); resolvedSchema = deref.resolveRefs(oldSchema, parameterSourceOption, components, schemaResolutionCache,
PROCESSING_TYPE.CONVERSION);
key = JSON.stringify(resolvedSchema); key = JSON.stringify(resolvedSchema);
if (resolveTo === 'schema') { if (resolveTo === 'schema') {
@@ -1522,7 +1529,7 @@ module.exports = {
return path.match(/(\{\{[^\/\{\}]+\}\})/g); return path.match(/(\{\{[^\/\{\}]+\}\})/g);
}, },
/** Separates outs collection and path variables from the reqUrl /** Separates out collection and path variables from the reqUrl
* *
* @param {string} reqUrl Request Url * @param {string} reqUrl Request Url
* @param {Array} pathVars Path variables * @param {Array} pathVars Path variables
@@ -1558,7 +1565,7 @@ module.exports = {
}); });
} }
return { reqUrl, pathVars, collectionVars }; return { url: reqUrl, pathVars, collectionVars };
}, },
/** /**
@@ -1605,7 +1612,7 @@ module.exports = {
sanitizeResult = this.sanitizeUrlPathParams(reqUrl, reqParams.path); sanitizeResult = this.sanitizeUrlPathParams(reqUrl, reqParams.path);
// Updated reqUrl // Updated reqUrl
reqUrl = sanitizeResult.reqUrl; reqUrl = sanitizeResult.url;
// Updated reqParams.path // Updated reqParams.path
reqParams.path = sanitizeResult.pathVars; reqParams.path = sanitizeResult.pathVars;
@@ -1625,24 +1632,59 @@ module.exports = {
if (Array.isArray(localServers) && localServers.length) { if (Array.isArray(localServers) && localServers.length) {
serverObj = operationItem.properties.servers[0]; serverObj = operationItem.properties.servers[0];
baseUrl = serverObj.url.replace(/{/g, ':').replace(/}/g, '');
baseUrl += reqUrl; // convert all {anything} to {{anything}}
baseUrl = this.fixPathVariablesInUrl(serverObj.url);
// add serverObj variables to pathVarArray
if (serverObj.variables) { if (serverObj.variables) {
pathVarArray = this.convertPathVariables('method', [], serverObj.variables, components, options, schemaCache); _.forOwn(serverObj.variables, (value, key) => {
pathVarArray.push({
name: key,
value: value.default || '',
description: this.getParameterDescription(value)
});
});
// use pathVarAray to sanitize url for path params and collection variables.
sanitizeResult = this.sanitizeUrlPathParams(baseUrl, pathVarArray);
// update the base url with update url
baseUrl = sanitizeResult.url;
// Add new collection variables to the variableStore
sanitizeResult.collectionVars.forEach((element) => {
if (!variableStore[element.name]) {
variableStore[element.name] = {
id: element.name,
value: element.value || '',
description: element.description,
type: 'collection'
};
} }
});
// remove all the collection variables from serverObj.variables
serverObj.pathVariables = {};
sanitizeResult.pathVars.forEach((element) => {
serverObj.pathVariables[element.name] = serverObj.variables[element.name];
});
// use this new filterd serverObj.pathVariables
// to generate pm path variables.
pathVarArray = this.convertPathVariables('method', [],
serverObj.pathVariables, components, options, schemaCache);
}
baseUrl += reqUrl;
} }
else { else {
// accounting for the overriding of the root level servers object if present at the path level // accounting for the overriding of the root level servers object if present at the path level
if (Array.isArray(globalServers) && globalServers.length) { if (Array.isArray(globalServers) && globalServers.length) {
if (operationItem.servers[0].hasOwnProperty('variables')) { // All the global servers present at the path level are taken care of in generateTrieFromPaths
serverObj = operationItem.servers[0]; // Just adding the same structure of the url as the display URL.
baseUrl = serverObj.url.replace(/{/g, ':').replace(/}/g, '');
pathVariables = serverObj.variables;
}
else {
displayUrl = '{{' + operationItem.path + 'Url}}' + reqUrl; displayUrl = '{{' + operationItem.path + 'Url}}' + reqUrl;
} }
} // In case there are no server available use the baseUrl
else { else {
baseUrl += reqUrl; baseUrl += reqUrl;
if (pathVariables) { if (pathVariables) {
@@ -1950,7 +1992,7 @@ module.exports = {
/** /**
* *
* @param {*} property - one of QUERYPARAM, PATHVARIABLE, HEADER, REQUEST_BODY, RESPONSE_HEADER, RESPONSE_BODY * @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 {*} 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 {*} txnParamName - Optional - The name of the param being validated (useful for query params,
* req headers, res headers) * req headers, res headers)
@@ -2035,6 +2077,7 @@ module.exports = {
// simpler cases are handled by a type check // simpler cases are handled by a type check
if (schema.type === 'array' || schema.type === 'object') { if (schema.type === 'array' || schema.type === 'object') {
try { try {
// the unknown formats are ones that are allowed in OAS, but not JSON schema
ajv = new Ajv({ unknownFormats: ['int32', 'int64'], allErrors: true }); ajv = new Ajv({ unknownFormats: ['int32', 'int64'], allErrors: true });
validate = ajv.compile(schema); validate = ajv.compile(schema);
res = validate(valueToUse); res = validate(valueToUse);
@@ -2044,13 +2087,33 @@ module.exports = {
// input was invalid. Don't throw mismatch // input was invalid. Don't throw mismatch
} }
if (!res) { if (!res) {
mismatches.push({ // Show detailed validation mismatches for only request/response body
if (options.detailedBlobValidation && needJsonMatching) {
_.forEach(validate.errors, (ajvError) => {
mismatches.push(_.assign({
property: property,
transactionJsonPath: jsonPathPrefix + ajvError.dataPath,
schemaJsonPath: schemaPathPrefix + ajvError.schemaPath.replace(/\//g, '.').slice(1)
}, ajvValidationError(ajvError, { property, humanPropName })));
});
}
else {
let mismatchObj = {
reason: 'The property didn\'t match the specified schema',
reasonCode: 'INVALID_TYPE'
};
if (property === 'BODY') {
mismatchObj.reason = 'The request body didn\'t match the specified schema';
mismatchObj.reasonCode = 'INVALID_BODY';
}
mismatches.push(_.assign({
property: property, property: property,
transactionJsonPath: jsonPathPrefix, transactionJsonPath: jsonPathPrefix,
schemaJsonPath: schemaPathPrefix, schemaJsonPath: schemaPathPrefix
reasonCode: 'INVALID_TYPE', }, mismatchObj));
reason: 'The property didn\'t match the specified schema' }
});
// only return AJV mismatches // only return AJV mismatches
return callback(null, mismatches); return callback(null, mismatches);
@@ -2150,7 +2213,8 @@ module.exports = {
pathVar.key, pathVar.key,
pathVar.value, pathVar.value,
schemaPathVar.pathPrefix + '[?(@.name==\'' + schemaPathVar.name + '\')]', schemaPathVar.pathPrefix + '[?(@.name==\'' + schemaPathVar.name + '\')]',
deref.resolveRefs(schemaPathVar.schema, 'request', components, schemaResolutionCache), deref.resolveRefs(schemaPathVar.schema, 'request', components, schemaResolutionCache,
PROCESSING_TYPE.VALIDATION),
components, options, cb); components, options, cb);
}, 0); }, 0);
}, (err, res) => { }, (err, res) => {
@@ -2251,7 +2315,8 @@ module.exports = {
pQuery.key, pQuery.key,
pQuery.value, pQuery.value,
schemaParam.pathPrefix + '[?(@.name==\'' + schemaParam.name + '\')]', schemaParam.pathPrefix + '[?(@.name==\'' + schemaParam.name + '\')]',
deref.resolveRefs(schemaParam.schema, 'request', components, schemaResolutionCache), deref.resolveRefs(schemaParam.schema, 'request', components, schemaResolutionCache,
PROCESSING_TYPE.VALIDATION),
components, options, components, options,
cb cb
); );
@@ -2312,7 +2377,8 @@ module.exports = {
pHeader.key, pHeader.key,
pHeader.value, pHeader.value,
schemaHeader.pathPrefix + '[?(@.name==\'' + schemaHeader.name + '\')]', schemaHeader.pathPrefix + '[?(@.name==\'' + schemaHeader.name + '\')]',
deref.resolveRefs(schemaHeader.schema, 'request', components, schemaResolutionCache), deref.resolveRefs(schemaHeader.schema, 'request', components, schemaResolutionCache,
PROCESSING_TYPE.VALIDATION),
components, options, components, options,
cb cb
); );
@@ -2362,7 +2428,7 @@ module.exports = {
mismatches.push({ mismatches.push({
property: mismatchProperty, property: mismatchProperty,
transactionJsonPath: transactionPathPrefix + '/' + pHeader.key, transactionJsonPath: transactionPathPrefix + '/' + pHeader.key,
schemaJsonPath: schemaPathPrefix + '/headers', schemaJsonPath: schemaPathPrefix + '.headers',
reasonCode: 'MISSING_IN_SCHEMA', reasonCode: 'MISSING_IN_SCHEMA',
reason: `The header ${pHeader.key} was not found in the schema` reason: `The header ${pHeader.key} was not found in the schema`
}); });
@@ -2381,7 +2447,8 @@ module.exports = {
pHeader.key, pHeader.key,
pHeader.value, pHeader.value,
schemaPathPrefix + '.headers[' + pHeader.key + ']', schemaPathPrefix + '.headers[' + pHeader.key + ']',
deref.resolveRefs(schemaHeader.schema, 'response', components, schemaResolutionCache), deref.resolveRefs(schemaHeader.schema, 'response', components, schemaResolutionCache,
PROCESSING_TYPE.VALIDATION),
components, components,
options, options,
cb cb
@@ -2413,54 +2480,31 @@ module.exports = {
// check for body modes // check for body modes
// TODO: The application/json can be anything that's application/*+json // TODO: The application/json can be anything that's application/*+json
let jsonSchemaBody = _.get(schemaPath, ['requestBody', 'content', 'application/json', 'schema']), let jsonSchemaBody = _.get(schemaPath, ['requestBody', 'content', 'application/json', 'schema']),
mismatches = [],
mismatchProperty = 'BODY'; mismatchProperty = 'BODY';
if (options.validationPropertiesToIgnore.includes(mismatchProperty)) { if (options.validationPropertiesToIgnore.includes(mismatchProperty)) {
return callback(null, []); return callback(null, []);
} }
if (requestBody && requestBody.mode === 'raw' && jsonSchemaBody) {
// only raw for now // only raw for now
// the unknown formats are ones that are allowed in OAS, but not JSON schema if (requestBody && requestBody.mode === 'raw' && jsonSchemaBody) {
let ajv, setTimeout(() => {
validate, return this.checkValueAgainstSchema(mismatchProperty,
res = true; transactionPathPrefix,
null, // no param name for the request body
try { requestBody.raw,
ajv = new Ajv({ unknownFormats: ['int32', 'int64'], allErrors: true }); schemaPathPrefix + '.requestBody.content[application/json].schema',
validate = ajv.compile(deref.resolveRefs(jsonSchemaBody, 'request', components, schemaResolutionCache)); deref.resolveRefs(jsonSchemaBody, 'request', components, schemaResolutionCache,
res = validate(JSON.parse(requestBody.raw)); PROCESSING_TYPE.VALIDATION),
components,
_.extend({}, options, { shortValidationErrors: true }),
callback
);
}, 0);
} }
catch (e) { else {
// something went wrong validating the schema
// input was invalid. Don't throw mismatch
}
if (!res) {
mismatches.push({
property: mismatchProperty,
transactionJsonPath: transactionPathPrefix,
schemaJsonPath: schemaPathPrefix + 'requestBody.content.application.json.schema',
reasonCode: 'INVALID_BODY',
reason: 'The request body didn\'t match the specified schema'
});
// Not validating parts of the body for now
// _.each(validate.errors, (error) => {
// // error.keyword can be https://ajv.js.org/keywords.html
// mismatches.push({
// property: 'REQUEST_BODY',
// transactionJsonPath: transactionPathPrefix + error.dataPath,
// schemaJsonPath: schemaPathPrefix + 'requestBody.content.application.json.schema.' + error.schemaPath,
// reasonCode: error.keyword.toUpperCase(),
// reason: error.message
// });
// });
return callback(null, mismatches);
}
}
return callback(null, []); return callback(null, []);
}
}, },
checkResponseBody: function (schemaResponse, body, transactionPathPrefix, schemaPathPrefix, checkResponseBody: function (schemaResponse, body, transactionPathPrefix, schemaPathPrefix,
@@ -2489,10 +2533,11 @@ module.exports = {
setTimeout(() => { setTimeout(() => {
return this.checkValueAgainstSchema(mismatchProperty, return this.checkValueAgainstSchema(mismatchProperty,
transactionPathPrefix, transactionPathPrefix,
null, // no param name for the request body null, // no param name for the response body
body, body,
schemaPathPrefix + '.content[application/json].schema', schemaPathPrefix + '.content[application/json].schema',
deref.resolveRefs(schemaContent, 'response', components, schemaResolutionCache), deref.resolveRefs(schemaContent, 'response', components, schemaResolutionCache,
PROCESSING_TYPE.VALIDATION),
components, components,
_.extend({}, options, { shortValidationErrors: true }), _.extend({}, options, { shortValidationErrors: true }),
callback callback
@@ -2526,13 +2571,13 @@ module.exports = {
async.parallel({ async.parallel({
headers: (cb) => { headers: (cb) => {
this.checkResponseHeaders(thisSchemaResponse, response.header, this.checkResponseHeaders(thisSchemaResponse, response.header,
transactionPathPrefix + '[' + response.id + ']header', transactionPathPrefix + '[' + response.id + '].header',
schemaPathPrefix + '.responses.' + responsePathPrefix, components, options, schemaResolutionCache, cb); schemaPathPrefix + '.responses.' + responsePathPrefix, components, options, schemaResolutionCache, cb);
}, },
body: (cb) => { body: (cb) => {
// assume it's JSON at this point // assume it's JSON at this point
this.checkResponseBody(thisSchemaResponse, response.body, this.checkResponseBody(thisSchemaResponse, response.body,
transactionPathPrefix + '[' + response.id + ']body', transactionPathPrefix + '[' + response.id + '].body',
schemaPathPrefix + '.responses.' + responsePathPrefix, components, options, schemaResolutionCache, cb); schemaPathPrefix + '.responses.' + responsePathPrefix, components, options, schemaResolutionCache, cb);
} }
}, (err, result) => { }, (err, result) => {
@@ -2658,4 +2703,3 @@ module.exports = {
}; };
} }
}; };

View File

@@ -81,7 +81,8 @@ class SchemaPack {
); );
// hardcoding this option - not exposed to users yet // hardcoding this option - not exposed to users yet
this.computedOptions.schemaFaker = true; this.computedOptions.schemaFaker = true;
this.metaData = null; let indentCharacter = this.computedOptions.indentCharacter;
this.computedOptions.indentCharacter = indentCharacter === 'tab' ? '\t' : ' ';
this.validate(); this.validate();
} }
@@ -515,8 +516,8 @@ class SchemaPack {
}, 0); }, 0);
} }
static getOptions() { static getOptions(mode, criteria) {
return getOptions(); return getOptions(mode, criteria);
} }
} }

View File

@@ -1,3 +1,5 @@
const _ = require('lodash');
// this will have non-OAS-related utils // this will have non-OAS-related utils
module.exports = { module.exports = {
@@ -10,6 +12,11 @@ module.exports = {
// set the default value to that option if the user has not defined // set the default value to that option if the user has not defined
if (userOptions[id] === undefined) { if (userOptions[id] === undefined) {
retVal[id] = defaultOptions[id].default; retVal[id] = defaultOptions[id].default;
// ignore case-sensitivity for enum option with type string
if (defaultOptions[id].type === 'enum' && _.isString(retVal[id])) {
retVal[id] = _.toLower(defaultOptions[id].default);
}
continue; continue;
} }
@@ -24,12 +31,19 @@ module.exports = {
} }
break; break;
case 'enum': case 'enum':
if (defaultOptions[id].availableOptions.includes(userOptions[id])) { // ignore case-sensitivity for string options
if ((defaultOptions[id].availableOptions.includes(userOptions[id])) ||
(_.isString(userOptions[id]) &&
_.map(defaultOptions[id].availableOptions, _.toLower).includes(_.toLower(userOptions[id])))) {
retVal[id] = userOptions[id]; retVal[id] = userOptions[id];
} }
else { else {
retVal[id] = defaultOptions[id].default; retVal[id] = defaultOptions[id].default;
} }
// ignore case-sensitivity for string options
_.isString(retVal[id]) && (retVal[id] = _.toLower(retVal[id]));
break; break;
case 'array': case 'array':
// user input needs to be parsed // user input needs to be parsed

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "openapi-to-postmanv2", "name": "openapi-to-postmanv2",
"version": "1.1.12", "version": "1.1.13",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "openapi-to-postmanv2", "name": "openapi-to-postmanv2",
"version": "1.1.12", "version": "1.1.13",
"description": "Convert a given OpenAPI specification to Postman Collection v2.0", "description": "Convert a given OpenAPI specification to Postman Collection v2.0",
"homepage": "https://github.com/postmanlabs/openapi-to-postman", "homepage": "https://github.com/postmanlabs/openapi-to-postman",
"bugs": "https://github.com/postmanlabs/openapi-to-postman/issues", "bugs": "https://github.com/postmanlabs/openapi-to-postman/issues",

View File

@@ -0,0 +1,152 @@
{
"openapi": "3.0.0",
"info": {
"version": "1.0.0",
"title": "#160",
"license": {
"name": "MIT"
}
},
"servers": [
{
"url": "http://petstore.swagger.io/v1"
}
],
"paths": {
"/pets": {
"servers": [
{
"url": "http://petstore.swagger.io:{port}/{basePath}",
"variables": {
"port": {
"enum": [
"8443",
"443"
],
"default": "8443"
},
"basePath": {
"default": "v2"
}
}
}
],
"get": {
"summary": "List all pets",
"operationId": "listPets",
"tags": [
"pets"
],
"parameters": [
{
"name": "limit",
"in": "query",
"description": "How many items to return at one time (max 100)",
"required": false,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "A paged array of pets",
"headers": {
"x-next": {
"description": "A link to the next page of responses",
"schema": {
"type": "string"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Pets"
}
}
}
},
"default": {
"description": "unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
},
"post": {
"summary": "Create a pet",
"operationId": "createPets",
"tags": [
"pets"
],
"responses": {
"201": {
"description": "Null response"
},
"default": {
"description": "unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Pet": {
"type": "object",
"required": [
"id",
"name"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"tag": {
"type": "string"
}
}
},
"Pets": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Pet"
}
},
"Error": {
"type": "object",
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
}
}
}
}
}
}

View File

@@ -0,0 +1,113 @@
openapi: 3.0.0
info:
version: 1.0.0
title: Swagger Petstore
license:
name: MIT
servers:
- url: http://petstore.swagger.io/v1
paths:
/pets:
get:
summary: List all pets
operationId: listPets
tags:
- pets
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
format: int32
responses:
"200":
description: A paged array of pets
headers:
x-next:
description: A link to the next page of responses
schema:
type: string
content:
application/json:
schema:
properties:
newprop:
type: array
items:
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
default:
description: unexpected error
content:
application/json:
schema:
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
post:
summary: Create a pet
operationId: createPets
tags:
- pets
responses:
"201":
description: Null response
default:
description: unexpected error
content:
application/json:
schema:
properties:
data:
type: array
items:
oneOf:
- $ref: "#/paths/~1pets/get/responses/200/content/applicati\
on~1json/schema/properties/newprop"
- $ref: "#/paths/~1pets/get/responses/default/content/appli\
cation~1json/schema"
components:
schemas:
Pet:
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
Pets:
type: array
items:
$ref: "#/components/schemas/Pet"
Error:
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string

View File

@@ -22,7 +22,19 @@
"summary": "Should use the other domain", "summary": "Should use the other domain",
"servers": [ "servers": [
{ {
"url": "https://other-api.example.com" "url": "http://petstore.swagger.io:{port}/{basePath}",
"variables": {
"port": {
"enum": [
"8443",
"443"
],
"default": "8443"
},
"basePath": {
"default": "v2"
}
}
} }
] ]
} }

View File

@@ -0,0 +1,180 @@
{
"openapi": "3.0.0",
"info": {
"description": "Too Many Refs",
"version": "v1",
"title": "Too Many Refs",
"termsOfService": "http://www.example.com/termsOfService",
"contact": {
"name": "Sample",
"url": "http://sample.com/",
"email": "sample@sample.com"
}
},
"tags": [
{
"name": "Folder tree"
}
],
"paths": {
"/path": {
"get": {
"tags": [
"Some path"
],
"summary": "Too many refs path",
"description": "Returns a list of folders in a nested, tree structure.",
"operationId": "listFolderTree",
"parameters": [],
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/b"
}
}
}
}
}
}
},
"/path2": {
"get": {
"tags": [
"Some path"
],
"summary": "Too many refs path",
"description": "Returns a list of folders in a nested, tree structure.",
"operationId": "listFolderTree",
"parameters": [],
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/k"
}
}
}
}
}
}
}
},
"servers": [
{
"url": "https://{host}/sample/api",
"variables": {
"host": {
"default": "unknown"
}
}
}
],
"components": {
"schemas": {
"b": {
"type": "object",
"properties": {
"c": {
"$ref": "#/components/schemas/c"
}
}
},
"c": {
"type": "object",
"properties": {
"d": {
"$ref": "#/components/schemas/d"
}
}
},
"d": {
"type": "object",
"properties": {
"e": {
"$ref": "#/components/schemas/e"
}
}
},
"e": {
"type": "object",
"properties": {
"f": {
"$ref": "#/components/schemas/f"
}
}
},
"f": {
"type": "object",
"properties": {
"g": {
"$ref": "#/components/schemas/g"
}
}
},
"g": {
"type": "object",
"properties": {
"h": {
"$ref": "#/components/schemas/h"
}
}
},
"h": {
"type": "object",
"properties": {
"i": {
"$ref": "#/components/schemas/i"
}
}
},
"i": {
"type": "object",
"properties": {
"j": {
"$ref": "#/components/schemas/j"
}
}
},
"j": {
"type": "object",
"properties": {
"k": {
"$ref": "#/components/schemas/k"
}
}
},
"k": {
"type": "object",
"properties": {
"l": {
"$ref": "#/components/schemas/a"
}
}
},
"a": {
"required": [
"id",
"name"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"tag": {
"type": "string"
}
}
}
}
}
}

View File

@@ -0,0 +1,85 @@
{
"info": {
"_postman_id": "7a1d1b7f-f6c3-4ce1-8c1f-1e93fc4a470d",
"name": "lets see",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"id": "req",
"name": "Sample endpoint: Returns details about a particular user",
"request": {
"auth": {
"type": "noauth"
},
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/user?id=1",
"host": [
"{{baseUrl}}"
],
"path": [
"user"
],
"query": [
{
"key": "id",
"value": "1",
"description": "(Required) ID of the user"
}
]
},
"body": {
"mode": "raw",
"raw": "{\n \"data\": [\n {\n \"id\": \"5e5792b234d88e12b8511b92\",\n \"accountNumber\": \"1YNykgIi3T2NDeElON0IqcPOpPI\",\n \"entityName\": \"Farmer Freddy's Veg\",\n \"entityPhone\": \"+4420832132132\",\n \"incType\": \"sole\",\n \"companyNumber\": 10000,\n \"needThisNot\": \"hello\",\n \"website\": \"https://farmer-freddy.null\",\n \"turnover\": 10000,\n \"description\": \"def\",\n \"status\": \"tradingAccepted\",\n \"offers\": [\n \"organic-vegetables\",\n \"organic-fruits\",\n \"101\"\n ],\n \"wants\": [\n \"carpentry\",\n \"beer\",\n \"beer\"\n ],\n \"isFavorite\": true\n }\n ],\n \"meta\": {\n \"notNeeded\": 1,\n \"numberOfResults\": 1,\n \"totalPages\": 1\n }\n}"
}
},
"response": [
{
"id": "res",
"name": "OK",
"originalRequest": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/user?id=1",
"host": [
"{{baseUrl}}"
],
"path": [
"user"
],
"query": [
{
"key": "id",
"value": "1"
}
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"cookie": [],
"body": "{\n \"data\": [\n {\n \"id\": \"5e5792b234d88e12b8511b92\",\n \"accountNumber\": \"1YNykgIi3T2NDeElON0IqcPOpPI\",\n \"entityName\": \"Farmer Freddy's Veg\",\n \"entityPhone\": \"+4420832132132\",\n \"incType\": \"sole\",\n \"companyNumber\": \"none\",\n \"website\": \"https://farmer-freddy.null\",\n \"turnover\": 10000,\n \"description\": \"High quality carpentry at a reasonable price.\",\n \"status\": \"wrongEnum\",\n \"offers\": [\n \"organic-vegetables\",\n \"organic-fruits\"\n ],\n \"wants\": [\n \"carpentry\",\n \"beer\"\n ],\n \"isFavorite\": true\n }\n ],\n \"meta\": {\n \"numberOfResults\": 1,\n \"totalPages\": 1\n }\n}"
}
]
}
],
"variable": [
{
"id": "baseUrl",
"key": "baseUrl",
"value": "http://localhost:3000",
"type": "string"
}
],
"protocolProfileBehavior": {}
}

View File

@@ -0,0 +1,127 @@
openapi: 3.0.0
info:
version: 1
title: IMPORT-202
servers:
- url: 'http://localhost:3000'
paths:
/user:
get:
summary: 'Sample endpoint: Returns details about a particular user'
operationId: listUser
tags:
- user
parameters:
- name: id
in: query
description: ID of the user
required: true
schema:
type: integer
format: int32
requestBody:
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Entity'
meta:
type: object
properties:
numberOfResults:
type: number
format: int32
totalPages:
type: number
format: int32
responses:
200:
description: OK
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Entity'
meta:
type: object
maxProperties: 1
additionalProperties: false
propertyNames:
pattern: '^_'
properties:
numberOfResults:
type: number
format: int32
totalPages:
type: number
format: int32
components:
schemas:
Entity:
type: object
title: Entity
description: A single and unique entity linked to a user
minProperties: 50
required: [id, needThis]
dependencies:
id: [needThis, accountNumber]
properties:
id:
type: string
maxLength: 5
accountNumber:
type: string
minLength: 50
entityName:
type: string
format: ipv4
entityPhone:
type: string
incType:
type: string
const: 'Postman'
companyNumber:
type: number
exclusiveMinimum: 10000
website:
type: number
turnover:
type: integer
format: int32
multipleOf: 7
description:
type: string
pattern: '[abc]+'
status:
type: string
enum:
- pending
- accepted
- rejected
- tradingPending
- tradingAccepted
- tradingRejected
offers:
type: array
items: [
type: string,
type: string
]
additionalItems: false
wants:
type: array
uniqueItems: true
items:
type: string
isFavorite:
type: boolean
needThis:
type: string

View File

@@ -5,62 +5,62 @@ const optionIds = [
'collapseFolders', 'collapseFolders',
'requestParametersResolution', 'requestParametersResolution',
'exampleParametersResolution', 'exampleParametersResolution',
'folderStrategy',
'indentCharacter', 'indentCharacter',
'requestNameSource', 'requestNameSource',
'shortValidationErrors',
'validationPropertiesToIgnore', 'validationPropertiesToIgnore',
'showMissingInSchemaErrors' 'showMissingInSchemaErrors',
'detailedBlobValidation'
], ],
expectedOptions = { expectedOptions = {
collapseFolders: { collapseFolders: {
name: 'Toggle for collapsing folder for long routes', name: 'Collapse redundant folders',
type: 'boolean', type: 'boolean',
default: true, default: true,
description: 'Determines whether the importer should attempt to collapse redundant folders into one.' + description: 'Importing will collapse all folders that have only one child element and lack ' +
'Folders are redundant if they have only one child element, and don\'t' + 'persistent folder-level data.'
'have any folder-level data to persist.'
}, },
requestParametersResolution: { requestParametersResolution: {
name: 'Set root request parameters type', name: 'Request parameter generation',
type: 'enum', type: 'enum',
default: 'schema', default: 'Schema',
availableOptions: ['example', 'schema'], availableOptions: ['Example', 'Schema'],
description: 'Determines how request parameters (query parameters, path parameters, headers,' + description: 'Select whether to generate the request parameters based on the' +
'or the request body) should be generated. Setting this to schema will cause the importer to' + ' [schema](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject) or the' +
'use the parameter\'s schema as an indicator; `example` will cause the example (if provided)' + ' [example](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#exampleObject)' +
'to be picked up.' ' in the schema.'
}, },
exampleParametersResolution: { exampleParametersResolution: {
name: 'Set example request and response parameters type', name: 'Response parameter generation',
type: 'enum', type: 'enum',
default: 'example', default: 'Example',
availableOptions: ['example', 'schema'], availableOptions: ['Example', 'Schema'],
description: 'Determines how response parameters (query parameters, path parameters, headers,' + description: 'Select whether to generate the response parameters based on the' +
'or the request body) should be generated. Setting this to schema will cause the importer to' + ' [schema](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject) or the' +
'use the parameter\'s schema as an indicator; `example` will cause the example (if provided)' + ' [example](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#exampleObject)' +
'to be picked up.' ' in the schema.'
},
folderStrategy: {
name: 'Set folder strategy',
type: 'enum',
default: 'paths',
availableOptions: ['paths', 'tags'],
description: 'Determines whether the importer should attempt to create the folders according' +
'to paths or tags which are given in the spec.'
}, },
indentCharacter: { indentCharacter: {
name: 'Set indent character', name: 'Set indent character',
type: 'enum', type: 'enum',
default: ' ', default: 'Space',
availableOptions: [' ', '\t'], availableOptions: ['Space', 'Tab'],
description: 'Option for setting indentation character' description: 'Option for setting indentation character'
}, },
requestNameSource: { requestNameSource: {
name: 'Set request name source', name: 'Naming requests',
type: 'enum', type: 'enum',
default: 'fallback', default: 'Fallback',
availableOptions: ['url', 'uKnown', 'fallback'], availableOptions: ['Url', 'Fallback'],
description: 'Option for setting source for a request name' description: 'Determines how the requests inside the generated collection will be named.' +
' If “Fallback” is selected, the request will be named after one of the following schema' +
' values: `description`, `operationid`, `url`.'
},
shortValidationErrors: {
name: 'Short error messages during request <> schema validation',
type: 'boolean',
default: false,
description: 'Whether detailed error messages are required for request <> schema validation operations.'
}, },
validationPropertiesToIgnore: { validationPropertiesToIgnore: {
name: 'Properties to ignore during validation', name: 'Properties to ignore during validation',
@@ -76,6 +76,14 @@ const optionIds = [
default: false, default: false,
description: 'MISSING_IN_SCHEMA indicates that an extra parameter was included in the request. For most ' + description: 'MISSING_IN_SCHEMA indicates that an extra parameter was included in the request. For most ' +
'use cases, this need not be considered an error.' 'use cases, this need not be considered an error.'
},
detailedBlobValidation: {
name: 'Show detailed body validation messages',
id: 'detailedBlobValidation',
type: 'boolean',
default: false,
description: 'Determines whether to show detailed mismatch information for application/json content ' +
'in the request/response body.'
} }
}; };
@@ -111,5 +119,18 @@ describe('getOptions', function() {
} }
}); });
}); });
it('must return all valid options based on criteria', function () {
getOptions({ usage: ['CONVERSION'] }).forEach((option) => {
expect(option.id).to.be.oneOf(optionIds);
expect(option.usage).to.include('CONVERSION');
});
getOptions({ external: true, usage: ['VALIDATION'] }).forEach((option) => {
expect(option.id).to.be.oneOf(optionIds);
expect(option.external).to.eql(true);
expect(option.usage).to.include('VALIDATION');
});
});
}); });

View File

@@ -12,6 +12,7 @@ describe('CONVERT FUNCTION TESTS ', function() {
var testSpec = path.join(__dirname, VALID_OPENAPI_PATH + '/test.json'), var testSpec = path.join(__dirname, VALID_OPENAPI_PATH + '/test.json'),
testSpec1 = path.join(__dirname, VALID_OPENAPI_PATH + '/test1.json'), testSpec1 = path.join(__dirname, VALID_OPENAPI_PATH + '/test1.json'),
issue133 = path.join(__dirname, VALID_OPENAPI_PATH + '/issue#133.json'), issue133 = path.join(__dirname, VALID_OPENAPI_PATH + '/issue#133.json'),
issue160 = path.join(__dirname, VALID_OPENAPI_PATH, '/issue#160.json'),
unique_items_schema = path.join(__dirname, VALID_OPENAPI_PATH + '/unique_items_schema.json'), unique_items_schema = path.join(__dirname, VALID_OPENAPI_PATH + '/unique_items_schema.json'),
serverOverRidingSpec = path.join(__dirname, VALID_OPENAPI_PATH + '/server_overriding.json'), serverOverRidingSpec = path.join(__dirname, VALID_OPENAPI_PATH + '/server_overriding.json'),
infoHavingContactOnlySpec = path.join(__dirname, VALID_OPENAPI_PATH + '/info_having_contact_only.json'), infoHavingContactOnlySpec = path.join(__dirname, VALID_OPENAPI_PATH + '/info_having_contact_only.json'),
@@ -30,7 +31,44 @@ describe('CONVERT FUNCTION TESTS ', function() {
requiredInParams = path.join(__dirname, VALID_OPENAPI_PATH, '/required_in_parameters.json'), requiredInParams = path.join(__dirname, VALID_OPENAPI_PATH, '/required_in_parameters.json'),
multipleRefs = path.join(__dirname, VALID_OPENAPI_PATH, '/multiple_refs.json'), multipleRefs = path.join(__dirname, VALID_OPENAPI_PATH, '/multiple_refs.json'),
issue150 = path.join(__dirname, VALID_OPENAPI_PATH + '/issue#150.yml'), issue150 = path.join(__dirname, VALID_OPENAPI_PATH + '/issue#150.yml'),
issue173 = path.join(__dirname, VALID_OPENAPI_PATH, '/issue#173.yml'); issue173 = path.join(__dirname, VALID_OPENAPI_PATH, '/issue#173.yml'),
issue152 = path.join(__dirname, VALID_OPENAPI_PATH, '/path-refs-error.yaml'),
tooManyRefs = path.join(__dirname, VALID_OPENAPI_PATH, '/too-many-refs.json');
it('Should generate collection conforming to schema for and fail if not valid ' +
tooManyRefs, function(done) {
var openapi = fs.readFileSync(tooManyRefs, 'utf8'),
body;
Converter.convert({ type: 'string', data: openapi }, { schemaFaker: true }, (err, conversionResult) => {
expect(err).to.be.null;
expect(conversionResult.result).to.equal(true);
expect(conversionResult.output.length).to.equal(1);
expect(conversionResult.output[0].type).to.equal('collection');
expect(conversionResult.output[0].data).to.have.property('info');
expect(conversionResult.output[0].data).to.have.property('item');
body = conversionResult.output[0].data.item[1].response[0].body;
expect(body).to.not.contain('<Error: Too many levels of nesting to fake this schema>');
done();
});
});
it('Should generate collection conforming to schema for and fail if not valid ' +
issue152, function(done) {
var openapi = fs.readFileSync(issue152, 'utf8'),
refNotFound = 'reference #/paths/~1pets/get/responses/200/content/application~1json/schema/properties/newprop' +
' not found in the OpenAPI spec';
Converter.convert({ type: 'string', data: openapi }, { schemaFaker: true }, (err, conversionResult) => {
expect(err).to.be.null;
expect(conversionResult.result).to.equal(true);
expect(conversionResult.output.length).to.equal(1);
expect(conversionResult.output[0].type).to.equal('collection');
expect(conversionResult.output[0].data).to.have.property('info');
expect(conversionResult.output[0].data).to.have.property('item');
expect(conversionResult.output[0].data.item[0].item[1].response[1].body).to.not.contain(refNotFound);
done();
});
});
it('Should generate collection conforming to schema for and fail if not valid ' + it('Should generate collection conforming to schema for and fail if not valid ' +
testSpec, function(done) { testSpec, function(done) {
@@ -79,6 +117,21 @@ describe('CONVERT FUNCTION TESTS ', function() {
}); });
}); });
it('#GITHUB-160 should generate correct display url for path containing servers' +
issue160, function(done) {
var openapi = fs.readFileSync(issue160, 'utf8');
Converter.convert({ type: 'string', data: openapi }, {}, (err, conversionResult) => {
expect(err).to.be.null;
expect(conversionResult.result).to.equal(true);
expect(conversionResult.output.length).to.equal(1);
expect(conversionResult.output[0].type).to.equal('collection');
expect(conversionResult.output[0].data).to.have.property('info');
expect(conversionResult.output[0].data).to.have.property('item');
expect(conversionResult.output[0].data.item[0].item[0].request.url.host[0]).to.equal('{{petsUrl}}');
done();
});
});
it('Should not get stuck while resolving circular references' + it('Should not get stuck while resolving circular references' +
unique_items_schema, function(done) { unique_items_schema, function(done) {
Converter.convert({ type: 'file', data: Converter.convert({ type: 'file', data:
@@ -163,12 +216,13 @@ describe('CONVERT FUNCTION TESTS ', function() {
let request = conversionResult.output[0].data.item[1].request, let request = conversionResult.output[0].data.item[1].request,
protocol = request.url.protocol, protocol = request.url.protocol,
host = request.url.host.join('.'), host = request.url.host.join('.'),
port = request.url.port,
path = request.url.path.join('/'), path = request.url.path.join('/'),
endPoint = protocol + '://' + host + '/' + path, endPoint = protocol + '://' + host + ':' + port + '/' + path,
host1 = conversionResult.output[0].data.variable[0].value, host1 = conversionResult.output[0].data.variable[0].value,
path1 = conversionResult.output[0].data.item[0].request.url.path.join('/'), path1 = conversionResult.output[0].data.item[0].request.url.path.join('/'),
endPoint1 = host1 + '/' + path1; endPoint1 = host1 + '/' + path1;
expect(endPoint).to.equal('https://other-api.example.com/secondary-domain/fails'); expect(endPoint).to.equal('http://petstore.swagger.io:{{port}}/:basePath/secondary-domain/fails');
expect(endPoint1).to.equal('https://api.example.com/primary-domain/works'); expect(endPoint1).to.equal('https://api.example.com/primary-domain/works');
done(); done();
}); });

View File

@@ -24,6 +24,9 @@ describe('DEREF FUNCTION TESTS ', function() {
} }
}] }]
}, },
schemaWithTypeArray = {
$ref: '#/components/schemas/schemaTypeArray'
},
componentsAndPaths = { componentsAndPaths = {
components: { components: {
schemas: { schemas: {
@@ -69,6 +72,14 @@ describe('DEREF FUNCTION TESTS ', function() {
format: 'email' format: 'email'
} }
} }
},
schemaTypeArray: {
type: 'array',
items: {
type: 'string'
},
minItems: 5,
maxItems: 55
} }
} }
} }
@@ -77,7 +88,9 @@ describe('DEREF FUNCTION TESTS ', function() {
output = deref.resolveRefs(schema, parameterSource, componentsAndPaths), output = deref.resolveRefs(schema, parameterSource, componentsAndPaths),
output_withdot = deref.resolveRefs(schemaWithDotInKey, parameterSource, componentsAndPaths), output_withdot = deref.resolveRefs(schemaWithDotInKey, parameterSource, componentsAndPaths),
output_customFormat = deref.resolveRefs(schemaWithCustomFormat, parameterSource, componentsAndPaths), output_customFormat = deref.resolveRefs(schemaWithCustomFormat, parameterSource, componentsAndPaths),
output_withAllOf = deref.resolveRefs(schemaWithAllOf, parameterSource, componentsAndPaths); output_withAllOf = deref.resolveRefs(schemaWithAllOf, parameterSource, componentsAndPaths),
output_validationTypeArray = deref.resolveRefs(schemaWithTypeArray, parameterSource, componentsAndPaths,
{}, 'VALIDATION');
expect(output).to.deep.include({ type: 'object', expect(output).to.deep.include({ type: 'object',
@@ -100,6 +113,17 @@ describe('DEREF FUNCTION TESTS ', function() {
} }
}); });
// deref.resolveRef() should not change minItems and maxItems for VALIDATION
expect(output_validationTypeArray).to.deep.include({
type: 'array',
items: {
type: 'string',
default: '<string>'
},
minItems: 5,
maxItems: 55
});
done(); done();
}); });

View File

@@ -2070,8 +2070,8 @@ describe('SCHEMA UTILITY FUNCTION TESTS ', function () {
resultObj = SchemaUtils.sanitizeUrlPathParams('/anotherpath/{{path}}/{{new-path-variable}}.{{onemore}}', resultObj = SchemaUtils.sanitizeUrlPathParams('/anotherpath/{{path}}/{{new-path-variable}}.{{onemore}}',
pathParams); pathParams);
expect(resultObj).to.have.property('reqUrl'); expect(resultObj).to.have.property('url');
expect(resultObj.reqUrl).to.equal('/anotherpath/:path/{{new-path-variable}}.{{onemore}}'); expect(resultObj.url).to.equal('/anotherpath/:path/{{new-path-variable}}.{{onemore}}');
expect(resultObj).to.have.property('pathVars'); expect(resultObj).to.have.property('pathVars');
expect(resultObj).to.have.property('collectionVars'); expect(resultObj).to.have.property('collectionVars');