Merge pull request #386 from postmanlabs/feature/fix-deepobject-conversion

Fixed issue where params with style deepObject were converted to only one level of key-value pair.
This commit is contained in:
Vishal Shingala
2021-07-16 14:03:39 +05:30
committed by GitHub
7 changed files with 502 additions and 38 deletions

View File

@@ -1515,6 +1515,28 @@ module.exports = {
return pmParams; return pmParams;
}, },
/**
* Recursicely extracts key-value pair from deep objects.
*
* @param {*} deepObject - Deep object
* @param {*} objectKey - key associated with deep object
* @returns {Array} array of param key-value pairs
*/
extractDeepObjectParams: function (deepObject, objectKey) {
let extractedParams = [];
// console.log(deepObject);
_.forEach(deepObject, (value, key) => {
if (typeof value === 'object') {
extractedParams = _.concat(extractedParams, this.extractDeepObjectParams(value, objectKey + '[' + key + ']'));
}
else {
extractedParams.push({ key: objectKey + '[' + key + ']', value });
}
});
return extractedParams;
},
/** /**
* Returns an array of parameters * Returns an array of parameters
* Handles array/object/string param types * Handles array/object/string param types
@@ -1573,10 +1595,12 @@ module.exports = {
break; break;
case 'deepObject': case 'deepObject':
if (_.isObject(paramValue)) { if (_.isObject(paramValue)) {
_.forOwn(paramValue, (value, key) => { let extractedParams = this.extractDeepObjectParams(paramValue, paramName);
_.forEach(extractedParams, (extractedParam) => {
pmParams.push(this.generateSdkParam({ pmParams.push(this.generateSdkParam({
key: param.name + '[' + key + ']', key: extractedParam.key,
value: (value === undefined ? '' : value), value: extractedParam.value || '',
description, description,
disabled disabled
}, _.get(param, 'in'))); }, _.get(param, 'in')));
@@ -2811,6 +2835,50 @@ module.exports = {
return { type, subtype }; return { type, subtype };
}, },
/**
* Extracts all child parameters from explodable param
*
* @param {*} schema - Corresponding schema object of parent parameter to be devided into child params
* @param {*} paramKey - Parameter name of parent param object
* @param {*} metaInfo - meta information of param (i.e. required)
* @returns {Array} - Extracted child parameters
*/
extractChildParamSchema: function (schema, paramKey, metaInfo) {
let childParamSchemas = [];
_.forEach(_.get(schema, 'properties', {}), (value, key) => {
if (_.get(value, 'type') === 'object') {
childParamSchemas = _.concat(childParamSchemas, this.extractChildParamSchema(value,
`${paramKey}[${key}]`, metaInfo));
}
else {
let required = _.get(metaInfo, 'required') || false,
pathPrefix = _.get(metaInfo, 'pathPrefix');
childParamSchemas.push({
name: `${paramKey}[${key}]`,
schema: value,
required,
isResolvedParam: true,
pathPrefix
});
}
});
return childParamSchemas;
},
/**
* Tests whether given parameter is of complex array type from param key
*
* @param {*} paramKey - Parmaeter key that is to be tested
* @returns {Boolean} - result
*/
isParamComplexArray: function (paramKey) {
// this checks if parameter key numbered element (i.e. itemArray[1] is complex array param)
let regex = /\[[\d]+\]/gm;
return regex.test(paramKey);
},
/** /**
* Finds valid JSON media type object from content object * Finds valid JSON media type object from content object
* *
@@ -3369,22 +3437,36 @@ module.exports = {
isPropSeparable = _.includes(['form', 'deepObject'], style); isPropSeparable = _.includes(['form', 'deepObject'], style);
if (isPropSeparable && paramSchema.type === 'array' && explode) { if (isPropSeparable && paramSchema.type === 'array' && explode) {
// add schema of items and instead array /**
resolvedSchemaParams.push(_.assign({}, param, { * avoid validation of complex array type param as OAS doesn't define serialisation
schema: _.get(paramSchema, 'items'), * of Array with deepObject style
isResolvedParam: true */
})); if (!_.includes(['array', 'object'], _.get(paramSchema, 'items.type'))) {
// add schema of corresponding items instead array
resolvedSchemaParams.push(_.assign({}, param, {
schema: _.get(paramSchema, 'items'),
isResolvedParam: true
}));
}
} }
else if (isPropSeparable && paramSchema.type === 'object' && explode) { else if (isPropSeparable && paramSchema.type === 'object' && explode) {
// add schema of all properties instead entire object // resolve all child params of parent param with deepObject style
_.forEach(_.get(paramSchema, 'properties', {}), (propSchema, propName) => { if (style === 'deepObject') {
resolvedSchemaParams.push({ resolvedSchemaParams = _.concat(resolvedSchemaParams, this.extractChildParamSchema(paramSchema,
name: propName, param.name, { required: _.get(param, 'required'), pathPrefix }));
schema: propSchema, }
isResolvedParam: true, else {
pathPrefix // add schema of all properties instead entire object
_.forEach(_.get(paramSchema, 'properties', {}), (propSchema, propName) => {
resolvedSchemaParams.push({
name: propName,
schema: propSchema,
required: _.get(param, 'required') || false,
isResolvedParam: true,
pathPrefix
});
}); });
}); }
} }
else { else {
resolvedSchemaParams.push(param); resolvedSchemaParams.push(param);
@@ -3399,7 +3481,10 @@ module.exports = {
const schemaParam = _.find(resolvedSchemaParams, (param) => { return param.name === pQuery.key; }); const schemaParam = _.find(resolvedSchemaParams, (param) => { return param.name === pQuery.key; });
if (!schemaParam) { if (!schemaParam) {
// no schema param found // skip validation of complex array params
if (this.isParamComplexArray(pQuery.key)) {
return cb(null, mismatches);
}
if (options.showMissingInSchemaErrors) { if (options.showMissingInSchemaErrors) {
mismatches.push({ mismatches.push({
property: mismatchProperty, property: mismatchProperty,
@@ -3440,7 +3525,7 @@ module.exports = {
let mismatches = [], let mismatches = [],
mismatchObj; mismatchObj;
_.each(_.filter(schemaParams, (q) => { return q.required; }), (qp) => { _.each(_.filter(resolvedSchemaParams, (q) => { return q.required; }), (qp) => {
if (!_.find(requestQueryParams, (param) => { return param.key === qp.name; })) { if (!_.find(requestQueryParams, (param) => { return param.key === qp.name; })) {
// assign parameter example(s) as schema examples; // assign parameter example(s) as schema examples;
@@ -3807,7 +3892,7 @@ module.exports = {
}); });
}, },
// Only application/json is validated for now // Only application/json and application/x-www-form-urlencoded is validated for now
checkRequestBody: function (requestBody, transactionPathPrefix, schemaPathPrefix, schemaPath, checkRequestBody: function (requestBody, transactionPathPrefix, schemaPathPrefix, schemaPath,
components, options, schemaCache, callback) { components, options, schemaCache, callback) {
// check for body modes // check for body modes
@@ -3828,7 +3913,6 @@ module.exports = {
jsonContentType = this.getJsonContentType(_.get(schemaPath, 'requestBody.content', {})); jsonContentType = this.getJsonContentType(_.get(schemaPath, 'requestBody.content', {}));
jsonSchemaBody = _.get(schemaPath, ['requestBody', 'content', jsonContentType, 'schema']); jsonSchemaBody = _.get(schemaPath, ['requestBody', 'content', jsonContentType, 'schema']);
// only raw for now
if (requestBody && requestBody.mode === 'raw' && jsonSchemaBody) { if (requestBody && requestBody.mode === 'raw' && jsonSchemaBody) {
setTimeout(() => { setTimeout(() => {
return this.checkValueAgainstSchema(mismatchProperty, return this.checkValueAgainstSchema(mismatchProperty,
@@ -3878,21 +3962,35 @@ module.exports = {
isPropSeparable = _.includes(['form', 'deepObject'], pSerialisationInfo.style); isPropSeparable = _.includes(['form', 'deepObject'], pSerialisationInfo.style);
if (isPropSeparable && propSchema.type === 'array' && pSerialisationInfo.explode) { if (isPropSeparable && propSchema.type === 'array' && pSerialisationInfo.explode) {
// add schema of items and instead array /**
resolvedSchemaParams.push(_.assign({}, resolvedProp, { * avoid validation of complex array type param as OAS doesn't define serialisation
schema: _.get(propSchema, 'items'), * of Array with deepObject style
isResolvedParam: true */
})); if (!_.includes(['array', 'object'], _.get(propSchema, 'items.type'))) {
// add schema of corresponding items instead array
resolvedSchemaParams.push(_.assign({}, resolvedProp, {
schema: _.get(propSchema, 'items'),
isResolvedParam: true
}));
}
} }
else if (isPropSeparable && propSchema.type === 'object' && pSerialisationInfo.explode) { else if (isPropSeparable && propSchema.type === 'object' && pSerialisationInfo.explode) {
// add schema of all properties instead entire object // resolve all child params of parent param with deepObject style
_.forEach(_.get(propSchema, 'properties', {}), (value, key) => { if (pSerialisationInfo.style === 'deepObject') {
resolvedSchemaParams.push({ resolvedSchemaParams = _.concat(resolvedSchemaParams, this.extractChildParamSchema(propSchema,
name: key, propName, { required: resolvedProp.required || false }));
schema: value, }
isResolvedParam: true else {
// add schema of all properties instead entire object
_.forEach(_.get(propSchema, 'properties', {}), (value, key) => {
resolvedSchemaParams.push({
name: key,
schema: value,
isResolvedParam: true,
required: resolvedProp.required || false
});
}); });
}); }
} }
else { else {
resolvedSchemaParams.push(resolvedProp); resolvedSchemaParams.push(resolvedProp);
@@ -3907,7 +4005,10 @@ module.exports = {
const schemaParam = _.find(resolvedSchemaParams, (param) => { return param.name === uParam.key; }); const schemaParam = _.find(resolvedSchemaParams, (param) => { return param.name === uParam.key; });
if (!schemaParam) { if (!schemaParam) {
// no schema param found // skip validation of complex array params
if (this.isParamComplexArray(uParam.key)) {
return cb(null, mismatches);
}
if (options.showMissingInSchemaErrors) { if (options.showMissingInSchemaErrors) {
mismatches.push({ mismatches.push({
property: mismatchProperty, property: mismatchProperty,
@@ -3971,7 +4072,7 @@ module.exports = {
}); });
_.each(resolvedSchemaParams, (uParam) => { _.each(resolvedSchemaParams, (uParam) => {
// report mismatches only for reuired properties // report mismatches only for required properties
if (!_.find(requestBody.urlencoded, (param) => { return param.key === uParam.name; }) && uParam.required) { if (!_.find(requestBody.urlencoded, (param) => { return param.key === uParam.name; }) && uParam.required) {
mismatchObj = { mismatchObj = {
property: mismatchProperty, property: mismatchProperty,

View File

@@ -0,0 +1,160 @@
{
"item": [
{
"id": "9861bc73-aafe-4c44-8c91-f42e4d820ae6",
"name": "pet",
"description": {
"content": "",
"type": "text/plain"
},
"item": [
{
"id": "23d37c51-7a63-412e-b15a-10c336d49615",
"name": "Updates a pet in the store with form data",
"request": {
"name": "Updates a pet in the store with form data",
"description": {},
"url": {
"path": [
"pets"
],
"host": [
"{{baseUrl}}"
],
"query": [
{
"disabled": false,
"key": "user[id]",
"value": "notAnInteger",
"description": "(Required) info about user"
},
{
"disabled": false,
"key": "user[name]",
"value": "John Johanson",
"description": "(Required) info about user"
},
{
"disabled": false,
"key": "user[address][city]",
"value": "Delhi",
"description": "(Required) info about user"
},
{
"disabled": false,
"key": "propArrayComplex[0][prop1ArrayComp]",
"value": "notAnInteger",
"description": "(Required) deepObject with complex array structure"
},
{
"disabled": false,
"key": "propArrayComplex[0][prop2ArrayComp]",
"value": "qui anim",
"description": "(Required) deepObject with complex array structure"
},
{
"disabled": false,
"key": "propArrayComplex[1][prop1ArrayComp]",
"value": "87313126",
"description": "(Required) deepObject with complex array structure"
},
{
"disabled": false,
"key": "propArrayComplex[1][prop2ArrayComp]",
"value": "reprehenderit",
"description": "(Required) deepObject with complex array structure"
}
],
"variable": []
},
"method": "GET",
"auth": null
},
"response": [
{
"id": "76137f7a-7d78-4f8b-837f-d678124c0b8e",
"name": "Pet updated.",
"originalRequest": {
"url": {
"path": [
"pets"
],
"host": [
"{{baseUrl}}"
],
"query": [
{
"key": "user[id]",
"value": "123"
},
{
"key": "user[name]",
"value": "John Johanson"
},
{
"key": "user[address][city]",
"value": "Delhi"
},
{
"key": "user[address][country]",
"value": "India"
},
{
"key": "propArrayComplex[0][prop1ArrayComp]",
"value": "70013937"
},
{
"key": "propArrayComplex[0][prop2ArrayComp]",
"value": "pariatur sit consectetur minim"
},
{
"key": "propArrayComplex[1][prop1ArrayComp]",
"value": "-77852940"
},
{
"key": "propArrayComplex[1][prop2ArrayComp]",
"value": "amet"
}
],
"variable": []
},
"method": "GET",
"body": {}
},
"status": "OK",
"code": 200,
"header": [
{
"key": "Content-Type",
"value": "text/plain"
}
],
"body": "",
"cookie": [],
"_postman_previewlanguage": "text"
}
],
"event": []
}
],
"event": []
}
],
"event": [],
"variable": [
{
"type": "string",
"value": "http://petstore.swagger.io/v1",
"key": "baseUrl"
}
],
"info": {
"_postman_id": "c3eff23a-fbd9-40b7-9029-7e9699d9bb1b",
"name": "Swagger Petstore",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"description": {
"content": "",
"type": "text/plain"
}
}
}

View File

@@ -0,0 +1,56 @@
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:
tags:
- pet
summary: Updates a pet in the store with form data
operationId: updatePetWithForm
parameters:
- name: user
in: query
description: info about user
required: true
style: deepObject
schema:
type: object
properties:
id:
type: integer
example: 123
name:
type: string
example: John Johanson
address:
type: object
properties:
city:
type: string
example: Delhi
country:
type: string
example: India
- name: propArrayComplex
in: query
description: deepObject with complex array structure
required: true
style: deepObject
schema:
type: array
items:
type: object
properties:
prop1ArrayComp:
type: integer
prop2ArrayComp:
type: string
responses:
'200':
description: Pet updated.

View File

@@ -66,6 +66,46 @@
{ {
"key": "propSimple", "key": "propSimple",
"value": "123" "value": "123"
},
{
"disabled": false,
"key": "propDeepObject[id]",
"value": "123"
},
{
"disabled": false,
"key": "propDeepObject[name]",
"value": "John Johanson"
},
{
"disabled": false,
"key": "propDeepObject[address][city]",
"value": "123"
},
{
"disabled": false,
"key": "propDeepObject[address][country]",
"value": "India"
},
{
"disabled": false,
"key": "propArrayComplex[0][prop1ArrayComp]",
"value": "notAnInteger"
},
{
"disabled": false,
"key": "propArrayComplex[0][prop2ArrayComp]",
"value": "irure labore Lorem consequat l"
},
{
"disabled": false,
"key": "propArrayComplex[1][prop1ArrayComp]",
"value": "-14216671"
},
{
"disabled": false,
"key": "propArrayComplex[1][prop2ArrayComp]",
"value": "officia"
} }
] ]
} }

View File

@@ -54,6 +54,33 @@ paths:
propSimple: propSimple:
type: integer type: integer
example: 123 example: 123
propDeepObject:
type: object
properties:
id:
type: integer
example: 123
name:
type: string
example: John Johanson
address:
type: object
properties:
city:
type: string
example: Delhi
country:
type: string
example: India
propArrayComplex:
type: array
items:
type: object
properties:
prop1ArrayComp:
type: integer
prop2ArrayComp:
type: string
required: required:
- status - status
encoding: encoding:
@@ -63,6 +90,12 @@ paths:
propObjectNonExplodable: propObjectNonExplodable:
style: form style: form
explode: false explode: false
propDeepObject:
style: deepObject
explode: true
propArrayComplex:
style: deepObject
explode: true
responses: responses:
'200': '200':
description: Pet updated. description: Pet updated.

View File

@@ -1427,18 +1427,34 @@ describe('SCHEMA UTILITY FUNCTION TESTS ', function () {
}, },
name: { name: {
type: 'string' type: 'string'
},
address: {
type: 'object',
properties: {
city: { type: 'string' },
state: { type: 'string' },
country: { type: 'string' }
}
} }
} }
} }
}; };
it('schemaFaker = true', function (done) { it('schemaFaker = true', function (done) {
let pmParam = SchemaUtils.convertToPmQueryParameters(param); let pmParam = SchemaUtils.convertToPmQueryParameters(param);
expect(pmParam).to.have.lengthOf(5);
expect(pmParam[0].key).to.equal(param.name + '[id]'); expect(pmParam[0].key).to.equal(param.name + '[id]');
expect(pmParam[1].key).to.equal(param.name + '[name]'); expect(pmParam[1].key).to.equal(param.name + '[name]');
expect(pmParam[0].description).to.equal(param.description); expect(pmParam[2].key).to.equal(param.name + '[address][city]');
expect(pmParam[1].description).to.equal(param.description); expect(pmParam[3].key).to.equal(param.name + '[address][state]');
expect(pmParam[4].key).to.equal(param.name + '[address][country]');
_.map(pmParam, (val, ind) => { expect(pmParam[ind].description).to.equal(param.description); });
expect(pmParam[0].value).to.equal('<long>'); expect(pmParam[0].value).to.equal('<long>');
expect(pmParam[1].value).to.equal('<string>'); expect(pmParam[1].value).to.equal('<string>');
expect(pmParam[2].value).to.equal('<string>');
expect(pmParam[3].value).to.equal('<string>');
expect(pmParam[4].value).to.equal('<string>');
done(); done();
}); });
it('schemaFaker = false', function (done) { it('schemaFaker = false', function (done) {

View File

@@ -640,6 +640,51 @@ describe('VALIDATE FUNCTION TESTS ', function () {
done(); done();
}); });
}); });
it('Should be able to validate schema with deepObject style query params against corresponding ' +
'transactions', function (done) {
let queryParamDeepObjectSpec = fs.readFileSync(path.join(__dirname, VALIDATION_DATA_FOLDER_PATH +
'/queryParamDeepObjectSpec.yaml'), 'utf-8'),
queryParamDeepObjectCollection = fs.readFileSync(path.join(__dirname, VALIDATION_DATA_FOLDER_PATH +
'/queryParamDeepObjectCollection.json'), 'utf-8'),
resultObj,
historyRequest = [],
schemaPack = new Converter.SchemaPack({ type: 'string', data: queryParamDeepObjectSpec },
{ suggestAvailableFixes: true, showMissingInSchemaErrors: true });
getAllTransactions(JSON.parse(queryParamDeepObjectCollection), historyRequest);
schemaPack.validateTransaction(historyRequest, (err, result) => {
expect(err).to.be.null;
expect(result).to.be.an('object');
resultObj = result.requests[historyRequest[0].id].endpoints[0];
expect(resultObj.mismatches).to.have.lengthOf(2);
/**
* no mismatches should be found for complex array type params as validation is skipped for them,
* even though corresponding value is of incorrect type
*/
_.forEach(resultObj.mismatches, (mismatch) => {
expect(mismatch.suggestedFix.key).to.not.eql('propArrayComplex[0][prop1ArrayComp]');
});
// for deepObject param "user", child param "user[id]" is of incorrect type
expect(resultObj.mismatches[0].reasonCode).to.eql('INVALID_TYPE');
expect(resultObj.mismatches[0].transactionJsonPath).to.eql('$.request.url.query[0].value');
expect(resultObj.mismatches[0].suggestedFix.actualValue).to.eql('notAnInteger');
expect(resultObj.mismatches[0].suggestedFix.suggestedValue).to.eql(123);
// for deepObject param "user", child param "user[address][country]" is missing in transaction
expect(resultObj.mismatches[1].reasonCode).to.eql('MISSING_IN_REQUEST');
expect(resultObj.mismatches[1].suggestedFix.key).to.eql('user[address][country]');
expect(resultObj.mismatches[1].suggestedFix.actualValue).to.be.null;
expect(resultObj.mismatches[1].suggestedFix.suggestedValue).to.eql({
key: 'user[address][country]',
value: 'India'
});
done();
});
});
}); });
describe('getPostmanUrlSuffixSchemaScore function', function () { describe('getPostmanUrlSuffixSchemaScore function', function () {
@@ -668,7 +713,7 @@ describe('VALIDATE FUNCTION TESTS ', function () {
resultObj, resultObj,
historyRequest = [], historyRequest = [],
schemaPack = new Converter.SchemaPack({ type: 'string', data: urlencodedBodySpec }, schemaPack = new Converter.SchemaPack({ type: 'string', data: urlencodedBodySpec },
{ suggestAvailableFixes: true }); { suggestAvailableFixes: true, showMissingInSchemaErrors: true });
getAllTransactions(JSON.parse(urlencodedBodyCollection), historyRequest); getAllTransactions(JSON.parse(urlencodedBodyCollection), historyRequest);
@@ -676,7 +721,15 @@ describe('VALIDATE FUNCTION TESTS ', function () {
expect(err).to.be.null; expect(err).to.be.null;
expect(result).to.be.an('object'); expect(result).to.be.an('object');
resultObj = result.requests[historyRequest[0].id].endpoints[0]; resultObj = result.requests[historyRequest[0].id].endpoints[0];
expect(resultObj.mismatches).to.have.lengthOf(3); expect(resultObj.mismatches).to.have.lengthOf(4);
/**
* no mismatches should be found for complex array type params as validation is skipped for them,
* even though corresponding value is of incorrect type
*/
_.forEach(resultObj.mismatches, (mismatch) => {
expect(mismatch.suggestedFix.key).to.not.eql('propArrayComplex[0][prop1ArrayComp]');
});
// for explodable property of type object named "propObjectExplodable", // for explodable property of type object named "propObjectExplodable",
// second property named "prop2" is incorrect, while property "prop1" is correct // second property named "prop2" is incorrect, while property "prop1" is correct
@@ -693,6 +746,11 @@ describe('VALIDATE FUNCTION TESTS ', function () {
expect(resultObj.mismatches[2].transactionJsonPath).to.eql('$.request.body.urlencoded[4].value'); expect(resultObj.mismatches[2].transactionJsonPath).to.eql('$.request.body.urlencoded[4].value');
expect(resultObj.mismatches[2].suggestedFix.actualValue).to.eql('999'); expect(resultObj.mismatches[2].suggestedFix.actualValue).to.eql('999');
expect(resultObj.mismatches[2].suggestedFix.suggestedValue).to.eql('exampleString'); expect(resultObj.mismatches[2].suggestedFix.suggestedValue).to.eql('exampleString');
// for deepObject property named "propDeepObject" child param "propDeepObject[address][city]" is of incorrect type
expect(resultObj.mismatches[3].transactionJsonPath).to.eql('$.request.body.urlencoded[8].value');
expect(resultObj.mismatches[3].suggestedFix.actualValue).to.eql('123');
expect(resultObj.mismatches[3].suggestedFix.suggestedValue).to.eql('Delhi');
done(); done();
}); });
}); });