Merge pull request #482 from postmanlabs/feature/validateTransaction-fixes

Fixed issue where newly converted collection had mismatches from validateTransaction() API.
This commit is contained in:
Vishal Shingala
2022-03-01 14:38:26 +05:30
committed by GitHub
5 changed files with 88 additions and 179 deletions

View File

@@ -16,7 +16,17 @@ const _ = require('lodash'),
'date-time': '<dateTime>',
password: '<password>'
},
boolean: '<boolean>'
boolean: '<boolean>',
array: '<array>',
object: '<object>'
},
SCHEMA_TYPES = {
array: 'array',
boolean: 'boolean',
integer: 'integer',
number: 'number',
object: 'object',
string: 'string'
},
PARAMETER_SOURCE = {
REQUEST: 'REQUEST',
@@ -141,14 +151,6 @@ module.exports = {
return { value: ERR_TOO_MANY_LEVELS };
}
// Update max stack reached for all current refs that's being resolved
if (!_.isEmpty(this._currentRefStack)) {
_.forEach(this._currentRefStack, (refKey) => {
_.set(schemaResolutionCache, [refKey, 'maxStack'],
Math.max(_.get(schemaResolutionCache, [refKey, 'maxStack'], 0), stack));
});
}
if (!schema) {
return { value: '<Error: Schema not found>' };
}
@@ -177,10 +179,6 @@ module.exports = {
return this.resolveAllOf(schema.allOf, parameterSourceOption, components, schemaResolutionCache, resolveFor,
resolveTo, stack, _.cloneDeep(seenRef), stackLimit);
}
if (schema.additionalProperties && !_.isBoolean(schema.additionalProperties)) {
schema.additionalProperties = this.resolveRefs(schema.additionalProperties, parameterSourceOption,
components, schemaResolutionCache, resolveFor, resolveTo, stack, _.cloneDeep(seenRef), stackLimit);
}
if (schema.$ref && _.isFunction(schema.$ref.split)) {
let refKey = schema.$ref,
outerProperties = concreteUtils.getOuterPropsIfIsSupported(schema);
@@ -201,20 +199,6 @@ module.exports = {
return { value: 'reference ' + schema.$ref + ' not found in the OpenAPI spec' };
}
if (_.get(schemaResolutionCache, [refKey, 'schema'])) {
// maxStack for cached schema is how deep of nesting level we reached while resolving that schema
let maxStack = _.get(schemaResolutionCache, [refKey, 'maxStack'], 0),
// resLevel of perticuler cached schema is nesting level at which schema was resolved
resLevel = _.get(schemaResolutionCache, [refKey, 'resLevel'], stackLimit);
/**
* use cached schema if it was resolved at level lower or equal then at current stack level or
* if cached schema is resolved fully (it does not contain ERR_TOO_MANY_LEVELS value in sub schema)
*/
if (resLevel <= stack || maxStack < stackLimit) {
return schemaResolutionCache[refKey].schema;
}
}
// something like #/components/schemas/PaginationEnvelope/properties/page
// will be resolved - we don't care about anything after the components part
// splitRef.slice(1) will return ['components', 'schemas', 'PaginationEnvelope', 'properties', 'page']
@@ -235,32 +219,33 @@ module.exports = {
resolvedSchema = concreteUtils.addOuterPropsToRefSchemaIfIsSupported(resolvedSchema, outerProperties);
}
if (resolvedSchema) {
// add current ref that's being resolved in ref stack
!_.isArray(this._currentRefStack) && (this._currentRefStack = []);
this._currentRefStack.push(refKey);
let refResolvedSchema = this.resolveRefs(resolvedSchema, parameterSourceOption,
components, schemaResolutionCache, resolveFor, resolveTo, stack, _.cloneDeep(seenRef), stackLimit);
// remove current ref that's being resolved from stack as soon as resolved
_.isArray(this._currentRefStack) && (this._currentRefStack.pop());
if (refResolvedSchema && refResolvedSchema.value !== ERR_TOO_MANY_LEVELS) {
_.set(schemaResolutionCache, [refKey, 'resLevel'], stack);
_.set(schemaResolutionCache, [refKey, 'schema'], refResolvedSchema);
}
return refResolvedSchema;
}
return { value: 'reference ' + schema.$ref + ' not found in the OpenAPI spec' };
}
if (concreteUtils.compareTypes(schema.type, 'objects') || schema.hasOwnProperty('properties')) {
if (concreteUtils.compareTypes(schema.type, SCHEMA_TYPES.object) || schema.hasOwnProperty('properties') ||
schema.hasOwnProperty('additionalProperties')) {
// go through all props
schema.type = 'object';
if (schema.hasOwnProperty('properties')) {
schema.type = SCHEMA_TYPES.object;
if (_.has(schema, 'properties') || _.has(schema, 'additionalProperties')) {
// shallow cloning schema object except properties object
let tempSchema = _.omit(schema, 'properties');
tempSchema.properties = {};
let tempSchema = _.omit(schema, ['properties', 'additionalProperties']);
if (_.has(schema, 'additionalProperties')) {
// don't resolve boolean values
if (_.isBoolean(schema.additionalProperties)) {
tempSchema.additionalProperties = schema.additionalProperties;
}
else {
tempSchema.additionalProperties = this.resolveRefs(schema.additionalProperties, parameterSourceOption,
components, schemaResolutionCache, resolveFor, resolveTo, stack, _.cloneDeep(seenRef), stackLimit);
}
}
!_.isEmpty(schema.properties) && (tempSchema.properties = {});
for (prop in schema.properties) {
if (schema.properties.hasOwnProperty(prop)) {
/* eslint-disable max-depth */
@@ -288,10 +273,10 @@ module.exports = {
// Override deefault value to appropriate type representation for parameter resolution to schema
if (resolveFor === 'CONVERSION' && resolveTo === 'schema') {
schema.default = '<object>';
schema.default = type.object;
}
}
else if (concreteUtils.compareTypes(schema.type, 'array') && schema.items) {
else if (concreteUtils.compareTypes(schema.type, SCHEMA_TYPES.array) && schema.items) {
/*
For VALIDATION - keep minItems and maxItems properties defined by user in schema as is
FOR CONVERSION -
@@ -350,11 +335,11 @@ module.exports = {
}
else {
return {
type: 'object'
type: SCHEMA_TYPES.object
};
}
if (!schema.type) {
schema.type = 'string';
schema.type = SCHEMA_TYPES.string;
}
// Discard format if not supported by both json-schema-faker and ajv or pattern is also defined

View File

@@ -915,7 +915,7 @@ module.exports = {
convertPathVariables: function(type, providedPathVars, commonPathVars, components, options, schemaCache) {
options = _.merge({}, defaultOptions, options);
var variables = providedPathVars;
var variables = [];
// converting the base uri path variables, if any
// commonPathVars is an object for type = root/method
// array otherwise
@@ -948,7 +948,8 @@ module.exports = {
});
}
return variables;
// keep already provided varables (server variables) at last
return _.concat(variables, providedPathVars);
},
/**
@@ -1429,8 +1430,13 @@ module.exports = {
else if (bodyObj.schema) {
if (bodyObj.schema.hasOwnProperty('$ref')) {
let outerProps = concreteUtils.getOuterPropsIfIsSupported(bodyObj.schema),
resolvedSchema;
// skip beforehand resolution for OAS 3.0
if (outerProps) {
resolvedSchema = this.getRefObject(bodyObj.schema.$ref, components, options);
bodyObj.schema = concreteUtils.addOuterPropsToRefSchemaIfIsSupported(resolvedSchema, outerProps);
bodyObj.schema = concreteUtils.addOuterPropsToRefSchemaIfIsSupported(resolvedSchema, outerProps);
}
}
if (options.schemaFaker) {
if (this.getHeaderFamily(contentType) === HEADER_TYPE.XML) {
@@ -1438,7 +1444,7 @@ module.exports = {
}
// Do not fake schemas if the complexity score is 10
if (options.complexityScore === 10) {
schemaType = bodyObj.schema.type;
schemaType = _.get(this.getRefObject(bodyObj.schema.$ref, components, options), 'type');
if (schemaType === 'object') {
return {
value: '<Error: Spec size too large, skipping faking of schemas>'
@@ -1760,12 +1766,14 @@ module.exports = {
// handling for the urlencoded media type
if (contentObj.hasOwnProperty(URLENCODED)) {
rDataMode = 'urlencoded';
if (contentObj[URLENCODED].hasOwnProperty('schema') && contentObj[URLENCODED].schema.hasOwnProperty('$ref')) {
contentObj[URLENCODED].schema = this.getRefObject(contentObj[URLENCODED].schema.$ref, components, options);
}
bodyData = this.convertToPmBodyData(contentObj[URLENCODED], requestType, URLENCODED,
PARAMETER_SOURCE.REQUEST, options.indentCharacter, components, options, schemaCache);
encoding = contentObj[URLENCODED].encoding ? contentObj[URLENCODED].encoding : {};
if (contentObj[URLENCODED].hasOwnProperty('schema') && contentObj[URLENCODED].schema.hasOwnProperty('$ref')) {
contentObj[URLENCODED].schema = this.getRefObject(contentObj[URLENCODED].schema.$ref, components, options);
}
// create query parameters and add it to the request body object
_.forOwn(bodyData, (value, key) => {
@@ -2054,6 +2062,10 @@ module.exports = {
options = _.merge({}, defaultOptions, options);
var refObj, savedSchema;
if (typeof $ref !== 'string') {
return { value: `Invalid $ref: ${$ref} was found` };
}
savedSchema = $ref.split('/').slice(1).map((elem) => {
// https://swagger.io/docs/specification/using-ref#escape
// since / is the default delimiter, slashes are escaped with ~1

View File

@@ -424,9 +424,10 @@ paths:
application/json:
schema:
type: object
additionalProperties:
type: integer
format: int32
properties:
prop123:
type: integer
format: int32
security:
- api_key: []
/store/order:

View File

@@ -34,7 +34,20 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Pet"
"properties": {
"id": {
"type": "integer",
"format": "int64",
"readOnly": true
},
"name": {
"type": "string"
},
"tag": {
"type": "string",
"writeOnly": true
}
}
}
}
}

View File

@@ -100,6 +100,14 @@ describe('DEREF FUNCTION TESTS ', function() {
name: {
type: 'string'
}
},
additionalProperties: {
type: 'object',
properties: {
hello: {
type: 'string'
}
}
}
}
}
@@ -119,7 +127,8 @@ describe('DEREF FUNCTION TESTS ', function() {
_.cloneDeep(componentsAndPaths), {}, 'VALIDATION'),
output_emptyObject = deref.resolveRefs(schemaWithEmptyObject, parameterSource, _.cloneDeep(componentsAndPaths)),
output_additionalProps = deref.resolveRefs(schemaWithAdditionPropRef, parameterSource,
_.cloneDeep(componentsAndPaths), {}, 'VALIDATION');
_.cloneDeep(componentsAndPaths), {}, 'VALIDATION'),
output_additionalPropsOverride;
expect(output).to.deep.include({ type: 'object',
required: ['id'],
@@ -170,42 +179,15 @@ describe('DEREF FUNCTION TESTS ', function() {
// additionalProperties $ref should be resolved
expect(output_additionalProps).to.deep.include(componentsAndPaths.components.schemas.schemaAdditionalProps);
done();
});
// add default to above resolved schema
output_additionalProps.additionalProperties.properties.hello.default = '<string>';
it('should populate schemaResolutionCache having key as the ref provided', function (done) {
var schema = {
$ref: '#/components/schema/request'
},
componentsAndPaths = {
components: {
schema: {
request: {
properties: {
name: {
type: 'string',
example: 'example name'
}
}
}
}
},
concreteUtils: schemaUtils30X
},
parameterSource = 'REQUEST',
schemaResolutionCache = {},
resolvedSchema = deref.resolveRefs(schema, parameterSource, componentsAndPaths, schemaResolutionCache);
expect(_.get(schemaResolutionCache, ['#/components/schema/request', 'schema'])).to.deep.equal(resolvedSchema);
expect(resolvedSchema).to.deep.equal({
type: 'object',
properties: {
name: {
type: 'string',
example: 'example name',
default: '<string>'
}
}
});
output_additionalPropsOverride = deref.resolveRefs(schemaWithAdditionPropRef, parameterSource,
_.cloneDeep(componentsAndPaths), {}, 'VALIDATION');
// override should not affect newly resolved schema
expect(output_additionalPropsOverride).to.deep.include(
componentsAndPaths.components.schemas.schemaAdditionalProps);
done();
});
@@ -273,90 +255,6 @@ describe('DEREF FUNCTION TESTS ', function() {
expect(output.pattern).to.eql(schema.pattern);
done();
});
it('should correctly resolve schema from schemaResoltionCache based on schema resolution level', function (done) {
let schema = {
$ref: '#/components/schemas/schemaUsed'
},
consumerSchema = {
type: 'object',
properties: { level2: {
type: 'object',
properties: { level3: {
type: 'object',
properties: { level4: {
type: 'object',
properties: { level5: {
type: 'object',
properties: { level6: {
type: 'object',
properties: { level7: {
type: 'object',
properties: { level8: {
type: 'object',
properties: { level9: { $ref: '#/components/schemas/schemaUsed' } }
} }
} }
} }
} }
} }
} }
} }
},
componentsAndPaths = {
components: {
schemas: {
schemaUsed: {
'type': 'object',
'required': [
'id',
'name'
],
'properties': {
'id': {
'type': 'integer',
'format': 'int64'
},
'name': {
'type': 'string'
},
'tag': {
'type': 'string'
}
}
}
}
},
concreteUtils: schemaUtils30X
},
parameterSource = 'REQUEST',
schemaResoltionCache = {},
resolvedConsumerSchema,
resolvedSchema;
resolvedConsumerSchema = deref.resolveRefs(consumerSchema, parameterSource, componentsAndPaths,
schemaResoltionCache);
// Consumer schema contains schema at nesting level 9, which results in impartial resolution of schema
expect(_.get(schemaResoltionCache, ['#/components/schemas/schemaUsed', 'resLevel'])).to.eql(9);
expect(_.get(resolvedConsumerSchema, _.join(_.map(_.range(1, 10), (ele) => {
return `properties.level${ele}`;
}), '.'))).to.not.deep.equal(componentsAndPaths.components.schemas.schemaUsed);
expect(_.get(schemaResoltionCache, ['#/components/schemas/schemaUsed', 'schema'])).to.not.deep
.equal(componentsAndPaths.components.schemas.schemaUsed);
resolvedSchema = deref.resolveRefs(schema, parameterSource, componentsAndPaths, schemaResoltionCache);
// Restoring the original format as it is deleted if not supported by json-schema-faker and ajv
resolvedSchema.properties.id.format = 'int64';
/**
* Even though schema cache contains schemaUsed as impartially cached,resolution were it's used again will
* depend on ongoing resolution level and schema is cached again if it's updated.
*/
expect(resolvedSchema).to.deep.equal(componentsAndPaths.components.schemas.schemaUsed);
expect(_.get(schemaResoltionCache, ['#/components/schemas/schemaUsed', 'schema'])).to.deep
.equal(componentsAndPaths.components.schemas.schemaUsed);
done();
});
});
describe('resolveAllOf Function', function () {