Added support for Auth params in response/example. (#286)

* Added support for correct auth params in original request of response

* Updated collection json schema to latest

* Added tests for feature - response auth params support

* Moved auth params in response behind an option

* Updated option name and description to follow consistent casing

* Updated option id for including auth params in response to be more suitable

Co-authored-by: Vishal Shingala <vishalkumar.shingala@postman.com>
This commit is contained in:
Vishal Shingala
2020-09-09 12:28:17 +05:30
committed by GitHub
parent 70f374904b
commit b5f9fb1c49
7 changed files with 310 additions and 80 deletions

View File

@@ -116,6 +116,15 @@ module.exports = {
external: false,
usage: ['CONVERSION']
},
{
name: 'Include auth info in example requests',
id: 'includeAuthInfoInExample',
type: 'boolean',
default: true,
description: 'Select whether to include authentication parameters in the example request',
external: true,
usage: ['CONVERSION']
},
{
name: 'Short error messages during request <> schema validation',
id: 'shortValidationErrors',

View File

@@ -1007,14 +1007,46 @@ module.exports = {
securitySet.forEach((security) => {
securityDef = openapi.securityDefs[Object.keys(security)[0]];
if (!securityDef) {
return false;
if (!_.isObject(securityDef)) {
return;
}
else if (securityDef.type === 'http') {
if (_.toLower(securityDef.scheme) === 'basic') {
helper = {
type: securityDef.scheme
type: 'basic',
basic: [
{ key: 'username', value: '<Basic Auth Username>' },
{ key: 'password', value: '<Basic Auth Password>' }
]
};
}
else if (_.toLower(securityDef.scheme) === 'bearer') {
helper = {
type: 'bearer',
bearer: [{ key: 'token', value: '<Bearer Token>' }]
};
}
else if (_.toLower(securityDef.scheme) === 'digest') {
helper = {
type: 'digest',
digest: [
{ key: 'username', value: '<Digest Auth Username>' },
{ key: 'password', value: '<Digest Auth Password>' },
{ key: 'realm', value: '<realm>' }
]
};
}
else if (_.toLower(securityDef.scheme) === 'oauth' || _.toLower(securityDef.scheme) === 'oauth1') {
helper = {
type: 'oauth1',
oauth1: [
{ key: 'consumerSecret', value: '<OAuth 1.0 Consumer Key>' },
{ key: 'consumerKey', value: '<OAuth 1.0 Consumer Secret>' },
{ key: 'addParamsToHeader', value: true }
]
};
}
}
else if (securityDef.type === 'oauth2') {
helper = {
type: 'oauth2'
@@ -1022,15 +1054,103 @@ module.exports = {
}
else if (securityDef.type === 'apiKey') {
helper = {
type: 'api-key',
properties: securityDef
type: 'apikey',
apikey: [
{
key: 'key',
value: _.isString(securityDef.name) ? securityDef.name : '<API Key Name>'
},
{ key: 'value', value: true },
{
key: 'in',
value: _.includes(['query', 'header'], securityDef.in) ? securityDef.in : 'header'
}
]
};
}
// stop searching for helper if valid auth scheme is found
if (!_.isEmpty(helper)) {
return false;
}
});
return helper;
},
/**
* Generates Auth helper for response, params (query, headers) in helper object is added in
* request (originalRequest) part of example.
*
* @param {*} requestAuthHelper - Auth helper object of corresponding request
* @returns {Object} - Response Auth helper object containing params to be added
*/
getResponseAuthHelper: function (requestAuthHelper) {
var responseAuthHelper = {
query: [],
header: []
},
getValueFromHelper = function (authParams, keyName) {
return _.find(authParams, { key: keyName }).value;
},
paramLocation,
description;
if (!_.isObject(requestAuthHelper)) {
return responseAuthHelper;
}
description = 'Added as a part of security scheme: ' + requestAuthHelper.type;
switch (requestAuthHelper.type) {
case 'apikey':
// find location of parameter from auth helper
paramLocation = getValueFromHelper(requestAuthHelper.apikey, 'in');
responseAuthHelper[paramLocation].push({
key: getValueFromHelper(requestAuthHelper.apikey, 'key'),
value: '<API Key>',
description
});
break;
case 'basic':
responseAuthHelper.header.push({
key: 'Authorization',
value: 'Basic <credentials>',
description
});
break;
case 'bearer':
responseAuthHelper.header.push({
key: 'Authorization',
value: 'Bearer <token>',
description
});
break;
case 'digest':
responseAuthHelper.header.push({
key: 'Authorization',
value: 'Digest <credentials>',
description
});
break;
case 'oauth1':
responseAuthHelper.header.push({
key: 'Authorization',
value: 'OAuth <credentials>',
description
});
break;
case 'oauth2':
responseAuthHelper.header.push({
key: 'Authorization',
value: '<token>',
description
});
break;
default:
break;
}
return responseAuthHelper;
},
/**
* Converts a 'content' object into Postman response body. Any content-type header determined
* from the body is returned as well
@@ -2077,25 +2197,6 @@ module.exports = {
thisAuthObject[authMap[authMeta.currentHelper]] = authMeta.helperAttributes;
item.request.auth = new sdk.RequestAuth(thisAuthObject);
}
// TODO: Figure out what happens if type!=api-key
else if (authHelper && authHelper.type === 'api-key') {
if (authHelper.properties.in === 'header') {
item.request.addHeader(this.convertToPmHeader(authHelper.properties,
REQUEST_TYPE.ROOT, PARAMETER_SOURCE.REQUEST, components, options, schemaCache));
item.request.auth = {
type: 'noauth'
};
}
else if (authHelper.properties.in === 'query') {
this.convertToPmQueryParameters(authHelper.properties, REQUEST_TYPE.ROOT,
components, options, schemaCache).forEach((pmParam) => {
item.request.url.addQueryParams(pmParam);
});
item.request.auth = {
type: 'noauth'
};
}
}
else {
item.request.auth = authHelper;
}
@@ -2162,27 +2263,57 @@ module.exports = {
// adding responses to request item
if (operation.responses) {
let thisOriginalRequest = {},
responseAuthHelper,
authQueryParams,
convertedResponse;
if (options.includeAuthInfoInExample) {
responseAuthHelper = this.getResponseAuthHelper(authHelper);
// override auth helper with global security definition if no operation security definition found
if (_.isEmpty(authHelper)) {
responseAuthHelper = this.getResponseAuthHelper(this.getAuthHelper(openapi, openapi.security));
}
authQueryParams = _.map(responseAuthHelper.query, (queryParam) => {
// key-value pair will be added as transformed query string
return queryParam.key + '=' + queryParam.value;
});
}
_.forOwn(operation.responses, (response, code) => {
let originalRequestHeaders = [];
let originalRequestHeaders = [],
originalRequestQueryParams = this.convertToPmQueryArray(reqParams, REQUEST_TYPE.EXAMPLE,
components, options, schemaCache);
swagResponse = response;
if (response.$ref) {
swagResponse = this.getRefObject(response.$ref, components, options);
}
if (options.includeAuthInfoInExample) {
// add Authorization params if present
originalRequestQueryParams = _.concat(originalRequestQueryParams, authQueryParams);
originalRequestHeaders = _.concat(originalRequestHeaders, responseAuthHelper.header);
}
// Try and set fields for originalRequest (example.request)
thisOriginalRequest.method = item.request.method;
// setting URL
thisOriginalRequest.url = displayUrl;
// setting query params
if (originalRequestQueryParams.length) {
thisOriginalRequest.url += '?';
thisOriginalRequest.url += this.convertToPmQueryArray(reqParams, REQUEST_TYPE.EXAMPLE, components,
options, schemaCache).join('&');
thisOriginalRequest.url += originalRequestQueryParams.join('&');
}
// setting headers
_.forEach(reqParams.header, (header) => {
originalRequestHeaders.push(this.convertToPmHeader(header, REQUEST_TYPE.EXAMPLE,
PARAMETER_SOURCE.REQUEST, components, options, schemaCache));
});
thisOriginalRequest.header = originalRequestHeaders;
// setting request body
try {

View File

@@ -266,15 +266,6 @@ class SchemaPack {
if (openapi.security) {
authHelper = schemaUtils.getAuthHelper(openapi, openapi.security);
if (authHelper) {
if (authHelper.type === 'api-key') {
// if authHelper has type apikey and has properties in header or query
// we override authHelper to 'noauth'
if (authHelper.properties.in === 'header' || authHelper.properties.in === 'query') {
authHelper = {
type: 'noauth'
};
}
}
generatedStore.collection.auth = authHelper;
}
}

View File

@@ -14,7 +14,8 @@ module.exports.schemas = {
'description': 'Items are the basic unit for a Postman collection. You can think of them as corresponding to a single API endpoint. Each Item has one request and may have multiple API responses associated with it.',
'items': {
'title': 'Items',
'oneOf': [{
'oneOf': [
{
'$ref': '#/definitions/item'
},
{
@@ -30,13 +31,17 @@ module.exports.schemas = {
'$ref': '#/definitions/variable-list'
},
'auth': {
'oneOf': [{
'oneOf': [
{
'type': 'null'
},
{
'$ref': '#/definitions/auth'
}
]
},
'protocolProfileBehavior': {
'$ref': '#/definitions/protocol-profile-behavior'
}
},
'required': [
@@ -71,6 +76,7 @@ module.exports.schemas = {
'type': {
'type': 'string',
'enum': [
'apikey',
'awsv4',
'basic',
'bearer',
@@ -83,6 +89,14 @@ module.exports.schemas = {
]
},
'noauth': {},
'apikey': {
'type': 'array',
'title': 'API Key Authentication',
'description': 'The attributes for API Key Authentication.',
'items': {
'$ref': '#/definitions/auth-attribute'
}
},
'awsv4': {
'type': 'array',
'title': 'AWS Signature v4',
@@ -274,7 +288,8 @@ module.exports.schemas = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'$id': '#/definitions/description',
'description': 'A Description can be a raw text, or be an object, which holds the description along with its format.',
'oneOf': [{
'oneOf': [
{
'type': 'object',
'title': 'Description',
'properties': {
@@ -428,7 +443,8 @@ module.exports.schemas = {
'type': 'array',
'items': {
'title': 'Items',
'anyOf': [{
'anyOf': [
{
'$ref': '#/definitions/item'
},
{
@@ -441,13 +457,17 @@ module.exports.schemas = {
'$ref': '#/definitions/event-list'
},
'auth': {
'oneOf': [{
'oneOf': [
{
'type': 'null'
},
{
'$ref': '#/definitions/auth'
}
]
},
'protocolProfileBehavior': {
'$ref': '#/definitions/protocol-profile-behavior'
}
},
'required': [
@@ -488,22 +508,20 @@ module.exports.schemas = {
}
},
'protocolProfileBehavior': {
'type': 'object',
'title': 'Protocol Profile Behavior',
'description': 'Set of configurations used to alter the usual behavior of sending the request',
'properties': {
'disableBodyPruning': {
'type': 'boolean',
'default': false,
'description': 'Disable body pruning for GET, COPY, HEAD, PURGE and UNLOCK request methods.'
}
}
'$ref': '#/definitions/protocol-profile-behavior'
}
},
'required': [
'request'
]
},
'protocol-profile-behavior': {
'$schema': 'http://json-schema.org/draft-07/schema#',
'type': 'object',
'title': 'Protocol Profile Behavior',
'$id': '#/definitions/protocol-profile-behavior',
'description': 'Set of configurations used to alter the usual behavior of sending the request'
},
'proxy-config': {
'$schema': 'http://json-schema.org/draft-07/schema#',
'$id': '#/definitions/proxy-config',
@@ -543,7 +561,8 @@ module.exports.schemas = {
'$id': '#/definitions/request',
'title': 'Request',
'description': 'A request represents an HTTP request. If a string, the string is assumed to be the request URL and the method is assumed to be \'GET\'.',
'oneOf': [{
'oneOf': [
{
'type': 'object',
'title': 'Request',
'properties': {
@@ -551,7 +570,8 @@ module.exports.schemas = {
'$ref': '#/definitions/url'
},
'auth': {
'oneOf': [{
'oneOf': [
{
'type': 'null'
},
{
@@ -566,7 +586,8 @@ module.exports.schemas = {
'$ref': '#/definitions/certificate'
},
'method': {
'anyOf': [{
'anyOf': [
{
'description': 'The Standard HTTP method associated with this request.',
'type': 'string',
'enum': [
@@ -597,7 +618,8 @@ module.exports.schemas = {
'$ref': '#/definitions/description'
},
'header': {
'oneOf': [{
'oneOf': [
{
'$ref': '#/definitions/header-list'
},
{
@@ -606,7 +628,8 @@ module.exports.schemas = {
]
},
'body': {
'oneOf': [{
'oneOf': [
{
'type': 'object',
'description': 'This field contains the data usually contained in the request body.',
'properties': {
@@ -616,12 +639,16 @@ module.exports.schemas = {
'raw',
'urlencoded',
'formdata',
'file'
'file',
'graphql'
]
},
'raw': {
'type': 'string'
},
'graphql': {
'type': 'object'
},
'urlencoded': {
'type': 'array',
'items': {
@@ -652,7 +679,8 @@ module.exports.schemas = {
'items': {
'type': 'object',
'title': 'FormParameter',
'oneOf': [{
'oneOf': [
{
'properties': {
'key': {
'type': 'string'
@@ -688,6 +716,7 @@ module.exports.schemas = {
},
'src': {
'type': [
'array',
'string',
'null'
]
@@ -720,7 +749,8 @@ module.exports.schemas = {
'type': 'object',
'properties': {
'src': {
'oneOf': [{
'oneOf': [
{
'type': 'string',
'description': 'Contains the name of the file to upload. _Not the path_.'
},
@@ -776,14 +806,24 @@ module.exports.schemas = {
],
'description': 'The time taken by the request to complete. If a number, the unit is milliseconds. If the response is manually created, this can be set to `null`.'
},
'timings': {
'title': 'Response Timings',
'description': 'Set of timing information related to request and response in milliseconds',
'type': [
'object',
'null'
]
},
'header': {
'title': 'Headers',
'oneOf': [{
'oneOf': [
{
'type': 'array',
'title': 'Header',
'description': 'No HTTP request is complete without its headers, and the same is true for a Postman request. This field is an array containing all the headers.',
'items': {
'oneOf': [{
'oneOf': [
{
'$ref': '#/definitions/header'
},
{
@@ -808,7 +848,10 @@ module.exports.schemas = {
}
},
'body': {
'type': 'string',
'type': [
'null',
'string'
],
'description': 'The raw text of the response.'
},
'status': {
@@ -837,7 +880,8 @@ module.exports.schemas = {
'type': 'string'
},
'exec': {
'oneOf': [{
'oneOf': [
{
'type': 'array',
'description': 'This is an array of strings, where each line represents a single line of code. Having lines separate makes it possible to easily track changes made to scripts.',
'items': {
@@ -863,7 +907,8 @@ module.exports.schemas = {
'description': 'If object, contains the complete broken-down URL for this request. If string, contains the literal request URL.',
'$id': '#/definitions/url',
'title': 'Url',
'oneOf': [{
'oneOf': [
{
'type': 'object',
'properties': {
'raw': {
@@ -877,7 +922,8 @@ module.exports.schemas = {
'host': {
'title': 'Host',
'description': 'The host for the URL, E.g: api.yourdomain.com. Can be stored as a string or as an array of strings.',
'oneOf': [{
'oneOf': [
{
'type': 'string'
},
{
@@ -890,14 +936,16 @@ module.exports.schemas = {
]
},
'path': {
'oneOf': [{
'oneOf': [
{
'type': 'string'
},
{
'type': 'array',
'description': 'The complete path of the current url, broken down into segments. A segment could be a string, or a path variable.',
'items': {
'oneOf': [{
'oneOf': [
{
'type': 'string'
},
{
@@ -1023,7 +1071,8 @@ module.exports.schemas = {
'default': false
}
},
'anyOf': [{
'anyOf': [
{
'required': [
'id'
]
@@ -1046,7 +1095,8 @@ module.exports.schemas = {
'$id': '#/definitions/version',
'title': 'Collection Version',
'description': 'Postman allows you to version your collections as they grow, and this field holds the version number. While optional, it is recommended that you use this field to its fullest extent!',
'oneOf': [{
'oneOf': [
{
'type': 'object',
'properties': {
'major': {

View File

@@ -8,6 +8,7 @@ const optionIds = [
'folderStrategy',
'indentCharacter',
'requestNameSource',
'includeAuthInfoInExample',
'shortValidationErrors',
'validationPropertiesToIgnore',
'showMissingInSchemaErrors',
@@ -77,6 +78,12 @@ const optionIds = [
' If “Fallback” is selected, the request will be named after one of the following schema' +
' values: `description`, `operationid`, `url`.'
},
includeAuthInfoInExample: {
name: 'Include auth info in example requests',
type: 'boolean',
default: true,
description: 'Select whether to include authentication parameters in the example request'
},
shortValidationErrors: {
name: 'Short error messages during request <> schema validation',
type: 'boolean',

View File

@@ -64,7 +64,7 @@ describe('CONVERT FUNCTION TESTS ', function() {
});
});
it('Should have noauth at the collection level if auth type is api-key and properties in header' +
it('Should add collection level auth with type as `apiKey`' +
emptySecurityTestCase, function(done) {
var openapi = fs.readFileSync(emptySecurityTestCase, 'utf8');
Converter.convert({ type: 'string', data: openapi }, {}, (err, conversionResult) => {
@@ -76,7 +76,7 @@ describe('CONVERT FUNCTION TESTS ', function() {
expect(conversionResult.output[0].data).to.have.property('info');
expect(conversionResult.output[0].data).to.have.property('item');
expect(conversionResult.output[0].data.auth).to.have.property('type');
expect(conversionResult.output[0].data.auth.type).to.equal('noauth');
expect(conversionResult.output[0].data.auth.type).to.equal('apikey');
done();
});
});

View File

@@ -2286,6 +2286,48 @@ describe('SCHEMA UTILITY FUNCTION TESTS ', function () {
expect(jsonContentType).to.be.undefined;
});
});
describe('getResponseAuthHelper function', function () {
var authTypes = {
'basic': 'Basic <credentials>',
'bearer': 'Bearer <token>',
'digest': 'Digest <credentials>',
'oauth1': 'OAuth <credentials>',
'oauth2': '<token>'
};
it('should correctly generate params needed for securityScheme: apikey', function () {
let apiKeyHeaderHelper = SchemaUtils.getResponseAuthHelper({
type: 'apikey',
apikey: [{ key: 'in', value: 'header' }, { key: 'key', value: 'api-key-header' }]
}),
apiKeyQueryHelper = SchemaUtils.getResponseAuthHelper({
type: 'apikey',
apikey: [{ key: 'in', value: 'query' }, { key: 'key', value: 'api-key-query' }]
});
expect(apiKeyHeaderHelper.header).to.have.lengthOf(1);
expect(apiKeyHeaderHelper.query).to.have.lengthOf(0);
expect(apiKeyHeaderHelper.header[0].key).to.eql('api-key-header');
expect(apiKeyQueryHelper.query).to.have.lengthOf(1);
expect(apiKeyQueryHelper.header).to.have.lengthOf(0);
expect(apiKeyQueryHelper.query[0].key).to.eql('api-key-query');
});
_.forEach(authTypes, (authHeaderValue, authType) => {
it('should correctly generate params needed for securityScheme: ' + authType, function () {
let authHelper = SchemaUtils.getResponseAuthHelper({
type: authType
});
expect(authHelper.header).to.have.lengthOf(1);
expect(authHelper.query).to.have.lengthOf(0);
expect(authHelper.header[0].key).to.eql('Authorization');
expect(authHelper.header[0].value).to.eql(authHeaderValue);
});
});
});
});
describe('Get header family function ', function() {