diff --git a/CHANGELOG.md b/CHANGELOG.md index b2922b4e..fac6e3f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +## [v4.18.0] - 2023-09-28 + +- [#425](https://github.com/postmanlabs/openapi-to-postman/issues/425) [8413](https://github.com/postmanlabs/postman-app-support/issues/8413) Added support for multiple request and response examples. + ## [v4.17.0] - 2023-09-12 ## [v4.16.0] - 2023-08-18 @@ -596,7 +600,9 @@ Newer releases follow the [Keep a Changelog](https://keepachangelog.com/en/1.0.0 - Base release -[Unreleased]: https://github.com/postmanlabs/openapi-to-postman/compare/v4.17.0...HEAD +[Unreleased]: https://github.com/postmanlabs/openapi-to-postman/compare/v4.18.0...HEAD + +[v4.18.0]: https://github.com/postmanlabs/openapi-to-postman/compare/v4.17.0...v4.18.0 [v4.17.0]: https://github.com/postmanlabs/openapi-to-postman/compare/v4.16.0...v4.17.0 diff --git a/libV2/schemaUtils.js b/libV2/schemaUtils.js index 78fe122d..64a68887 100644 --- a/libV2/schemaUtils.js +++ b/libV2/schemaUtils.js @@ -1054,16 +1054,165 @@ let QUERYPARAM = 'query', return HEADER_TYPE.INVALID; }, - resolveRequestBodyData = (context, requestBodySchema, bodyType) => { + /** + * Gets XML Example data in correct format based on schema + * + * @param {Object} context - Global context object + * @param {Object} exampleData - Example data to be used + * @param {Object} requestBodySchema - Schema of the request body + * @returns {String} XML Example data + */ + getXMLExampleData = (context, exampleData, requestBodySchema) => { + const { parametersResolution, indentCharacter } = context.computedOptions; + + let reqBodySchemaWithExample = requestBodySchema; + + // Assign example at schema level to be faked by xmlSchemaFaker + if (typeof requestBodySchema === 'object') { + reqBodySchemaWithExample = Object.assign({}, requestBodySchema, { example: exampleData }); + } + + return xmlFaker(null, reqBodySchemaWithExample, indentCharacter, parametersResolution); + }, + + /** + * Generates postman equivalent examples which contains request and response mappings of + * each example based on examples mentioned ind definition + * + * @param {Object} context - Global context object + * @param {Object} responseExamples - Examples defined in the response + * @param {Object} requestBodyExamples - Examples defined in the request body + * @param {Object} responseBodySchema - Schema of the response body + * @param {Boolean} isXMLExample - Whether the example is XML example + * @returns {Array} Examples for corresponding operation + */ + generateExamples = (context, responseExamples, requestBodyExamples, responseBodySchema, isXMLExample) => { + const pmExamples = []; + + _.forEach(responseExamples, (responseExample, index) => { + + if (!_.isObject(responseExample)) { + return; + } + + let responseExampleData = getExampleData(context, { [responseExample.key]: responseExample.value }), + requestExample; + + if (isXMLExample) { + responseExampleData = getXMLExampleData(context, responseExampleData, responseBodySchema); + } + + if (_.isEmpty(requestBodyExamples)) { + pmExamples.push({ + response: responseExampleData, + name: _.get(responseExample, 'value.summary') || responseExample.key + }); + return; + } + + requestExample = _.find(requestBodyExamples, (example, index) => { + if ( + example.contentType === responseExample.contentType && + _.toLower(example.key) === _.toLower(responseExample.key) + ) { + requestBodyExamples[index].isUsed = true; + return true; + } + return false; + }); + + // If exact content type is not matching, pick first content type with same example key + if (!requestExample) { + requestExample = _.find(requestBodyExamples, (example, index) => { + if (_.toLower(example.key) === _.toLower(responseExample.key)) { + requestBodyExamples[index].isUsed = true; + return true; + } + return false; + }); + } + + if (!requestExample) { + if (requestBodyExamples[index] && !requestBodyExamples[index].isUsed) { + requestExample = requestBodyExamples[index]; + requestBodyExamples[index].isUsed = true; + } + else { + for (let i = 0; i < requestBodyExamples.length; i++) { + if (!requestBodyExamples[i].isUsed) { + requestExample = requestBodyExamples[i]; + requestBodyExamples[i].isUsed = true; + break; + } + } + + if (!requestExample) { + requestExample = requestBodyExamples[0]; + } + } + } + + pmExamples.push({ + request: getExampleData(context, { [requestExample.key]: requestExample.value }), + response: responseExampleData, + name: _.get(responseExample, 'value.summary') || (responseExample.key !== '_default' && responseExample.key) || + _.get(requestExample, 'value.summary') || requestExample.key || 'Example' + }); + }); + + let responseExample, + responseExampleData; + + for (let i = 0; i < requestBodyExamples.length; i++) { + + if (!requestBodyExamples[i].isUsed || pmExamples.length === 0) { + if (!responseExample) { + responseExample = _.head(responseExamples); + + if (responseExample) { + responseExampleData = getExampleData(context, { [responseExample.key]: responseExample.value }); + } + + if (isXMLExample) { + responseExampleData = getXMLExampleData(context, responseExampleData, responseBodySchema); + } + } + pmExamples.push({ + request: getExampleData(context, { [requestBodyExamples[i].key]: requestBodyExamples[i].value }), + response: responseExampleData, + name: _.get(requestBodyExamples[i], 'value.summary') || + (requestBodyExamples[i].key !== '_default' && requestBodyExamples[i].key) || + _.get(responseExample, 'value.summary') || 'Example' + }); + } + } + + return pmExamples; + }, + + /** + * Resolves the request/response body data + * + * @param {Object} context - Global context object + * @param {Object} requestBodySchema - Schema of the request / response body + * @param {String} bodyType - Content type of the body + * @param {Boolean} isExampleBody - Whether the body is example body + * @param {Object} requestBodyExamples - Examples defined in the request body + * @returns {Array} Request / Response body data + */ + resolveBodyData = (context, requestBodySchema, bodyType, isExampleBody = false, requestBodyExamples) => { let { parametersResolution, indentCharacter } = context.computedOptions, headerFamily = getHeaderFamily(bodyType), bodyData = '', shouldGenerateFromExample = parametersResolution === 'example', + isBodyTypeXML = bodyType === APP_XML || bodyType === TEXT_XML || headerFamily === HEADER_TYPE.XML, + bodyKey = isExampleBody ? 'response' : 'request', + responseExamples, example, examples; if (_.isEmpty(requestBodySchema)) { - return bodyData; + return [{ [bodyKey]: bodyData }]; } if (requestBodySchema.$ref) { @@ -1130,15 +1279,8 @@ let QUERYPARAM = 'query', */ const exampleData = example || getExampleData(context, examples); - if (bodyType === APP_XML || bodyType === TEXT_XML || headerFamily === HEADER_TYPE.XML) { - let reqBodySchemaWithExample = requestBodySchema; - - // Assign example at schema level to be faked by xmlSchemaFaker - if (typeof requestBodySchema === 'object') { - reqBodySchemaWithExample = Object.assign({}, requestBodySchema, { example: exampleData }); - } - - return xmlFaker(null, reqBodySchemaWithExample, indentCharacter, parametersResolution); + if (isBodyTypeXML) { + bodyData = getXMLExampleData(context, exampleData, requestBodySchema); } else { bodyData = exampleData; @@ -1151,47 +1293,71 @@ let QUERYPARAM = 'query', requestBodySchema = resolveSchema(context, requestBodySchema); } - if (bodyType === APP_XML || bodyType === TEXT_XML || headerFamily === HEADER_TYPE.XML) { - return xmlFaker(null, requestBodySchema, indentCharacter, parametersResolution); + if (isBodyTypeXML) { + bodyData = xmlFaker(null, requestBodySchema, indentCharacter, parametersResolution); } + else { + if (requestBodySchema.properties) { + // If any property exists with format:binary or byte schemaFaker crashes + // we just delete based on that format + + // TODO: This could have properties inside properties which needs to be handled + // That's why for some properties we are not deleting the format + _.forOwn(requestBodySchema.properties, (schema, prop) => { + if (!_.isObject(requestBodySchema.properties[prop])) { + return; + } + if ( + requestBodySchema.properties[prop].format === 'binary' || + requestBodySchema.properties[prop].format === 'byte' || + requestBodySchema.properties[prop].format === 'decimal' + ) { + delete requestBodySchema.properties[prop].format; + } + }); + } - if (requestBodySchema.properties) { - // If any property exists with format:binary or byte schemaFaker crashes - // we just delete based on that format - - // TODO: This could have properties inside properties which needs to be handled - // That's why for some properties we are not deleting the format - _.forOwn(requestBodySchema.properties, (schema, prop) => { - if (!_.isObject(requestBodySchema.properties[prop])) { - return; - } + // This is to handle cases when the jsf throws errors on finding unsupported types/formats + try { + bodyData = fakeSchema(context, requestBodySchema, shouldGenerateFromExample); + } + catch (e) { + console.warn( + 'Error faking a schema. Not faking this schema. Schema:', requestBodySchema, + 'Error', e.message + ); - if ( - requestBodySchema.properties[prop].format === 'binary' || - requestBodySchema.properties[prop].format === 'byte' || - requestBodySchema.properties[prop].format === 'decimal' - ) { - delete requestBodySchema.properties[prop].format; - } - }); + bodyData = ''; + } } + } - // This is to handle cases when the jsf throws errors on finding unsupported types/formats - try { - bodyData = fakeSchema(context, requestBodySchema, shouldGenerateFromExample); - } - catch (e) { - console.warn( - 'Error faking a schema. Not faking this schema. Schema:', requestBodySchema, - 'Error', e.message - ); + // Generate multiple examples when either request or response contains more than one example + if ( + isExampleBody && + shouldGenerateFromExample && + (_.size(examples) > 1 || _.size(requestBodyExamples) > 1) + ) { + responseExamples = [{ + key: '_default', + value: bodyData, + contentType: bodyType + }]; - return ''; + if (!_.isEmpty(examples)) { + responseExamples = _.map(examples, (example, key) => { + return { + key, + value: example, + contentType: bodyType + }; + }); } + return generateExamples(context, responseExamples, requestBodyExamples, requestBodySchema, isBodyTypeXML); } - return bodyData; + return [{ [bodyKey]: bodyData }]; }, resolveUrlEncodedRequestBodyForPostmanRequest = (context, requestBodyContent) => { @@ -1200,7 +1366,8 @@ let QUERYPARAM = 'query', requestBodyData = { mode: 'urlencoded', urlencoded: urlEncodedParams - }; + }, + resolvedBody; if (_.isEmpty(requestBodyContent)) { return requestBodyData; @@ -1210,7 +1377,8 @@ let QUERYPARAM = 'query', requestBodyContent.schema = resolveSchema(context, requestBodyContent.schema); } - bodyData = resolveRequestBodyData(context, requestBodyContent.schema); + resolvedBody = resolveBodyData(context, requestBodyContent.schema)[0]; + resolvedBody && (bodyData = resolvedBody.request); const encoding = requestBodyContent.encoding || {}; @@ -1255,13 +1423,16 @@ let QUERYPARAM = 'query', requestBodyData = { mode: 'formdata', formdata: formDataParams - }; + }, + resolvedBody; if (_.isEmpty(requestBodyContent)) { return requestBodyData; } - bodyData = resolveRequestBodyData(context, requestBodyContent.schema); + resolvedBody = resolveBodyData(context, requestBodyContent.schema)[0]; + resolvedBody && (bodyData = resolvedBody.request); + encoding = _.get(requestBodyContent, 'encoding', {}); _.forOwn(bodyData, (value, key) => { @@ -1332,12 +1503,23 @@ let QUERYPARAM = 'query', else if (content.hasOwnProperty(APP_XML)) { bodyType = APP_XML; } else if (content.hasOwnProperty(TEXT_XML)) { bodyType = TEXT_XML; } else { - // take the first property it has - // types like image/png etc - for (const cType in content) { - if (content.hasOwnProperty(cType)) { - bodyType = cType; - break; + // prefer JSON type of body if available + _.forOwn(content, (value, key) => { + if (content.hasOwnProperty(key) && getHeaderFamily(key) === HEADER_TYPE.JSON) { + bodyType = key; + return false; + } + }); + + // use first available type of body if no JSON or XML body is available + if (!bodyType) { + // take the first property it has + // types like image/png etc + for (const cType in content) { + if (content.hasOwnProperty(cType)) { + bodyType = cType; + break; + } } } } @@ -1350,7 +1532,8 @@ let QUERYPARAM = 'query', bodyData, headerFamily, dataToBeReturned = {}, - { concreteUtils } = context; + { concreteUtils } = context, + resolvedBody; headerFamily = getHeaderFamily(bodyType); @@ -1361,7 +1544,8 @@ let QUERYPARAM = 'query', } // Handling for Raw mode data else { - bodyData = resolveRequestBodyData(context, requestContent[bodyType], bodyType); + resolvedBody = resolveBodyData(context, requestContent[bodyType], bodyType)[0]; + resolvedBody && (bodyData = resolvedBody.request); if ((bodyType === TEXT_XML || bodyType === APP_XML || headerFamily === HEADER_TYPE.XML)) { bodyData = getXmlVersionContent(bodyData); @@ -1616,11 +1800,27 @@ let QUERYPARAM = 'query', return pmParams; }, - resolveResponseBody = (context, responseBody = {}) => { - let responseContent, bodyType, bodyData, headerFamily, acceptHeader; + /** + * Resolve the responses from definition which will be converted to request examples. + * This includes both request and response body of corresponding example. + * + * @param {Object} context - Global context object + * @param {Object} responseBody - Response body schema + * @param {Object} requestBodyExamples - Examples defined in the request body of corresponding operation + * @returns {Array} - Postman examples + */ + resolveResponseBody = (context, responseBody = {}, requestBodyExamples) => { + let responseContent, + bodyType, + allBodyData, + headerFamily, + acceptHeader, + emptyResponse = [{ + body: undefined + }]; if (_.isEmpty(responseBody)) { - return responseBody; + return emptyResponse; } if (responseBody.$ref) { @@ -1630,40 +1830,54 @@ let QUERYPARAM = 'query', responseContent = responseBody.content; if (_.isEmpty(responseContent)) { - return responseContent; + return emptyResponse; } bodyType = getRawBodyType(responseContent); headerFamily = getHeaderFamily(bodyType); - bodyData = resolveRequestBodyData(context, responseContent[bodyType], bodyType); + allBodyData = resolveBodyData(context, responseContent[bodyType], bodyType, true, requestBodyExamples); - if ((bodyType === TEXT_XML || bodyType === APP_XML || headerFamily === HEADER_TYPE.XML)) { - bodyData = getXmlVersionContent(bodyData); - } + return _.map(allBodyData, (bodyData) => { + let requestBodyData = bodyData.request, + responseBodyData = bodyData.response, + exampleName = bodyData.name; - const { indentCharacter } = context.computedOptions, - rawModeData = !_.isObject(bodyData) && _.isFunction(_.get(bodyData, 'toString')) ? - bodyData.toString() : - JSON.stringify(bodyData, null, indentCharacter), - responseMediaTypes = _.keys(responseContent); - - if (responseMediaTypes.length > 0) { - acceptHeader = [{ - key: 'Accept', - value: responseMediaTypes[0] - }]; - } + if ((bodyType === TEXT_XML || bodyType === APP_XML || headerFamily === HEADER_TYPE.XML)) { + responseBodyData && (responseBodyData = getXmlVersionContent(responseBodyData)); + } - return { - body: rawModeData, - contentHeader: [{ - key: 'Content-Type', - value: bodyType - }], - bodyType, - acceptHeader - }; + const { indentCharacter } = context.computedOptions, + getRawModeData = (bodyData) => { + return !_.isObject(bodyData) && _.isFunction(_.get(bodyData, 'toString')) ? + bodyData.toString() : + JSON.stringify(bodyData, null, indentCharacter); + }, + requestRawModeData = getRawModeData(requestBodyData), + responseRawModeData = getRawModeData(responseBodyData), + responseMediaTypes = _.keys(responseContent); + + if (responseMediaTypes.length > 0) { + acceptHeader = [{ + key: 'Accept', + value: responseMediaTypes[0] + }]; + } + + return { + request: { + body: requestRawModeData + }, + body: responseRawModeData, + contentHeader: [{ + key: 'Content-Type', + value: bodyType + }], + name: exampleName, + bodyType, + acceptHeader + }; + }); }, resolveResponseHeaders = (context, responseHeaders) => { @@ -1784,59 +1998,119 @@ let QUERYPARAM = 'query', resolveResponseForPostmanRequest = (context, operationItem, request) => { let responses = [], - requestAcceptHeader; + requestBodyExamples = [], + requestAcceptHeader, + requestBody = operationItem.requestBody, + requestContent, + rawBodyType, + headerFamily, + isBodyTypeXML; + + // store all request examples which will be used for creation of examples with correct request and response matching + if (typeof requestBody === 'object') { + if (requestBody.$ref) { + requestBody = resolveSchema(context, requestBody); + } + + requestContent = requestBody.content; + + if (typeof requestContent === 'object') { + rawBodyType = getRawBodyType(requestContent); + headerFamily = getHeaderFamily(rawBodyType); + isBodyTypeXML = rawBodyType === APP_XML || rawBodyType === TEXT_XML || headerFamily === HEADER_TYPE.XML; + + _.forEach(requestContent, (content, contentType) => { + if (_.has(content, 'examples')) { + _.forEach(content.examples, (example, name) => { + const exampleObj = example; + + if (isBodyTypeXML && exampleObj.value) { + const exampleData = getExampleData(context, { [name]: exampleObj }); + + if (isBodyTypeXML) { + let bodyData = getXMLExampleData(context, exampleData, resolveSchema(context, content.schema)); + + exampleObj.value = getXmlVersionContent(bodyData); + } + } + + requestBodyExamples.push({ + contentType, + key: name, + value: example + }); + }); + } + }); + } + } _.forOwn(operationItem.responses, (responseObj, code) => { - let response, - responseSchema = _.has(responseObj, '$ref') ? resolveSchema(context, responseObj) : responseObj, + let responseSchema = _.has(responseObj, '$ref') ? resolveSchema(context, responseObj) : responseObj, { includeAuthInfoInExample } = context.computedOptions, - responseAuthHelper, auth = request.auth, - { body, contentHeader = [], bodyType, acceptHeader } = resolveResponseBody(context, responseSchema) || {}, - headers = resolveResponseHeaders(context, responseSchema.headers), - originalRequest = request, - reqHeaders = _.clone(request.headers) || [], - reqQueryParams = _.clone(_.get(request, 'params.queryParams', [])); - - // add Accept header in example's original request headers - _.isArray(acceptHeader) && (reqHeaders.push(...acceptHeader)); - - if (includeAuthInfoInExample) { - if (!auth) { - auth = generateAuthForCollectionFromOpenAPI(context.openapi, context.openapi.security); + resolvedExamples = resolveResponseBody(context, responseSchema, requestBodyExamples) || {}, + headers = resolveResponseHeaders(context, responseSchema.headers); + + _.forOwn(resolvedExamples, (resolvedExample = {}) => { + let { body, contentHeader = [], bodyType, acceptHeader, name } = resolvedExample, + resolvedRequestBody = _.get(resolvedExample, 'request.body'), + originalRequest, + response, + responseAuthHelper, + requestBodyObj = {}, + reqHeaders = _.clone(request.headers) || [], + reqQueryParams = _.clone(_.get(request, 'params.queryParams', [])); + + // add Accept header in example's original request headers + _.isArray(acceptHeader) && (reqHeaders.push(...acceptHeader)); + + if (_.get(request, 'body.mode') === 'raw' && !_.isNil(resolvedRequestBody)) { + requestBodyObj = { + body: Object.assign({}, request.body, { raw: resolvedRequestBody }) + }; } - responseAuthHelper = getResponseAuthHelper(auth); + if (includeAuthInfoInExample) { + if (!auth) { + auth = generateAuthForCollectionFromOpenAPI(context.openapi, context.openapi.security); + } - reqHeaders.push(...responseAuthHelper.header); - reqQueryParams.push(...responseAuthHelper.query); + responseAuthHelper = getResponseAuthHelper(auth); - originalRequest = _.assign({}, request, { - headers: reqHeaders, - params: _.assign({}, request.params, { queryParams: reqQueryParams }) - }); - } - else { - originalRequest = _.assign({}, request, { - headers: reqHeaders - }); - } + reqHeaders.push(...responseAuthHelper.header); + reqQueryParams.push(...responseAuthHelper.query); - // set accept header value as first found response content's media type - if (_.isEmpty(requestAcceptHeader)) { - requestAcceptHeader = acceptHeader; - } + originalRequest = _.assign({}, request, { + headers: reqHeaders, + params: _.assign({}, request.params, { queryParams: reqQueryParams }) + }, requestBodyObj); + } + else { + originalRequest = _.assign({}, request, { headers: reqHeaders }, requestBodyObj); + } - response = { - name: _.get(responseSchema, 'description'), - body, - headers: _.concat(contentHeader, headers), - code, - originalRequest, - _postman_previewlanguage: getPreviewLangugaForResponseBody(bodyType) - }; + // When example key is not available, key name will be `_default` naming should be done based on description + if (_.get(resolvedExample, 'name') === '_default' || !(typeof name === 'string' && name.length)) { + name = _.get(responseSchema, 'description', `${code} response`); + } - responses.push(response); + // set accept header value as first found response content's media type + if (_.isEmpty(requestAcceptHeader)) { + requestAcceptHeader = acceptHeader; + } + + response = { + name, + body, + headers: _.concat(contentHeader, headers), + code, + originalRequest, + _postman_previewlanguage: getPreviewLangugaForResponseBody(bodyType) + }; + + responses.push(response); + }); }); return { responses, acceptHeader: requestAcceptHeader }; diff --git a/package-lock.json b/package-lock.json index b127c365..b7af3034 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openapi-to-postmanv2", - "version": "4.17.0", + "version": "4.18.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "openapi-to-postmanv2", - "version": "4.17.0", + "version": "4.18.0", "license": "Apache-2.0", "dependencies": { "ajv": "8.11.0", diff --git a/package.json b/package.json index 62f44e1c..50b75f75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openapi-to-postmanv2", - "version": "4.17.0", + "version": "4.18.0", "description": "Convert a given OpenAPI specification to Postman Collection v2.0", "homepage": "https://github.com/postmanlabs/openapi-to-postman", "bugs": "https://github.com/postmanlabs/openapi-to-postman/issues", diff --git a/test/data/valid_openapi/custom_headers.json b/test/data/valid_openapi/custom_headers.json index 4d3e9349..66acb073 100644 --- a/test/data/valid_openapi/custom_headers.json +++ b/test/data/valid_openapi/custom_headers.json @@ -16,12 +16,6 @@ "type": "integer", "format": "int32" } - }, - "application/vnd.retailer.v3+json": { - "schema": { - "type": "integer", - "format": "int32" - } } } } @@ -34,4 +28,4 @@ "url": "https://api.com" } ] -} \ No newline at end of file +} diff --git a/test/data/valid_openapi/multiExampleMatchingRequestResponse.yaml b/test/data/valid_openapi/multiExampleMatchingRequestResponse.yaml new file mode 100644 index 00000000..050a9347 --- /dev/null +++ b/test/data/valid_openapi/multiExampleMatchingRequestResponse.yaml @@ -0,0 +1,69 @@ +openapi: 3.0.0 +info: + title: None + version: 1.0.0 + description: None +paths: + /v1: + post: + requestBody: + content: + 'application/json': + schema: + $ref: "#/components/schemas/World" + examples: + valid-request: + value: + includedFields: + - user + - height + - weight + missing-required-parameter: + value: + includedFields: + - user + responses: + 200: + description: None + content: + 'application/json': + schema: + $ref: "#/components/schemas/Request" + examples: + valid-request: + summary: Complete request + value: + { + "user": 1, + "height": 168, + "weight": 44 + } + missing-required-parameter: + summary: Request with only required params + value: + { + "user": 1 + } +components: + schemas: + World: + type: object + properties: + includedFields: + type: array + Request: + type: object + required: + - user + - height + - weight + properties: + user: + type: integer + description: None + height: + type: integer + description: None + weight: + type: integer + description: None diff --git a/test/data/valid_openapi/multiExampleRequest.yaml b/test/data/valid_openapi/multiExampleRequest.yaml new file mode 100644 index 00000000..5909e548 --- /dev/null +++ b/test/data/valid_openapi/multiExampleRequest.yaml @@ -0,0 +1,50 @@ +openapi: 3.0.0 +info: + title: None + version: 1.0.0 + description: None +paths: + /v1/: + post: + requestBody: + content: + 'application/json': + schema: + $ref: "#/components/schemas/Request" + examples: + valid-request: + value: + { + "user": 1, + "height": 168, + "weight": 44 + } + missing-required-parameter: + value: + { + "user": 1 + } + responses: + 200: + description: None + content: + 'application/json': + example: { hello: 'world' } +components: + schemas: + Request: + type: object + required: + - user + - height + - weight + properties: + user: + type: integer + description: None + height: + type: integer + description: None + weight: + type: integer + description: None diff --git a/test/data/valid_openapi/multiExampleRequestResponse.yaml b/test/data/valid_openapi/multiExampleRequestResponse.yaml new file mode 100644 index 00000000..3b4c31d0 --- /dev/null +++ b/test/data/valid_openapi/multiExampleRequestResponse.yaml @@ -0,0 +1,70 @@ +openapi: 3.0.0 +info: + title: None + version: 1.0.0 + description: None +paths: + /v1: + post: + requestBody: + content: + 'application/json': + schema: + $ref: "#/components/schemas/World" + examples: + valid-request: + value: + includedFields: + - user + - height + - weight + missing-required-parameter: + value: + includedFields: + - user + responses: + 200: + description: None + content: + 'application/json': + schema: + $ref: "#/components/schemas/Request" + examples: + not-matching-key: + summary: Request with only required params + value: + { + "user": 1 + } + not-matching-key-2: + summary: Complete request + value: + { + "user": 1, + "height": 168, + "weight": 44 + } + +components: + schemas: + World: + type: object + properties: + includedFields: + type: array + Request: + type: object + required: + - user + - height + - weight + properties: + user: + type: integer + description: None + height: + type: integer + description: None + weight: + type: integer + description: None diff --git a/test/data/valid_openapi/multiExampleResponse.yaml b/test/data/valid_openapi/multiExampleResponse.yaml new file mode 100644 index 00000000..9386ae38 --- /dev/null +++ b/test/data/valid_openapi/multiExampleResponse.yaml @@ -0,0 +1,60 @@ +openapi: 3.0.0 +info: + title: None + version: 1.0.0 + description: None +paths: + /v1: + post: + requestBody: + content: + 'application/json': + schema: + $ref: "#/components/schemas/World" + examples: + hello-world: + value: + hello: world + responses: + 200: + description: None + content: + 'application/json': + schema: + $ref: "#/components/schemas/Request" + examples: + valid-request: + value: + { + "user": 1, + "height": 168, + "weight": 44 + } + missing-required-parameter: + value: + { + "user": 1 + } +components: + schemas: + World: + type: object + properties: + hello: + type: string + Request: + type: object + required: + - user + - height + - weight + properties: + user: + type: integer + description: None + height: + type: integer + description: None + weight: + type: integer + description: None diff --git a/test/unit/base.test.js b/test/unit/base.test.js index 410c45b3..1e7e9090 100644 --- a/test/unit/base.test.js +++ b/test/unit/base.test.js @@ -371,7 +371,7 @@ describe('CONVERT FUNCTION TESTS ', function() { Converter.convert({ type: 'string', data: openapi }, { schemaFaker: true }, (err, conversionResult) => { expect(err).to.be.null; expect(conversionResult.output[0].data.item[0].response[0].header[0].value) - .to.equal('application/vnd.retailer.v3+json'); + .to.equal('application/vnd.retailer.v3+xml'); done(); }); }); diff --git a/test/unit/convertV2.test.js b/test/unit/convertV2.test.js index ef27ceb5..19e77cfd 100644 --- a/test/unit/convertV2.test.js +++ b/test/unit/convertV2.test.js @@ -94,7 +94,15 @@ const expect = require('chai').expect, recursiveRefComponents = path.join(__dirname, VALID_OPENAPI_PATH, '/recursiveRefComponents.yaml'), securityAuthUnresolvedInPathItem = - path.join(__dirname, VALID_OPENAPI_PATH, '/securityAuthUnresolvedInPathItem.yaml'); + path.join(__dirname, VALID_OPENAPI_PATH, '/securityAuthUnresolvedInPathItem.yaml'), + multiExampleRequest = + path.join(__dirname, VALID_OPENAPI_PATH, '/multiExampleRequest.yaml'), + multiExampleResponse = + path.join(__dirname, VALID_OPENAPI_PATH, '/multiExampleResponse.yaml'), + multiExampleRequestResponse = + path.join(__dirname, VALID_OPENAPI_PATH, '/multiExampleRequestResponse.yaml'), + multiExampleMatchingRequestResponse = + path.join(__dirname, VALID_OPENAPI_PATH, '/multiExampleMatchingRequestResponse.yaml'); describe('The convert v2 Function', function() { @@ -2418,4 +2426,159 @@ describe('The convert v2 Function', function() { done(); }); }); + + describe('Should generate multiple examples when', function() { + it('request body contains multiple examples but request body has single example', function(done) { + var openapi = fs.readFileSync(multiExampleRequest, 'utf8'); + Converter.convertV2({ type: 'string', data: openapi }, { parametersResolution: 'Example' }, + (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.length).to.equal(1); + + const item = conversionResult.output[0].data.item[0].item[0]; + + expect(JSON.parse(item.request.body.raw)).to.eql({ + user: 1, + height: 168, + weight: 44 + }); + expect(item.response).to.have.lengthOf(2); + expect(item.response[0].name).to.eql('valid-request'); + expect(item.response[0]._postman_previewlanguage).to.eql('json'); + expect(JSON.parse(item.response[0].body)).to.eql({ hello: 'world' }); + expect(JSON.parse(item.response[0].originalRequest.body.raw)).to.eql({ + user: 1, + height: 168, + weight: 44 + }); + + expect(item.response[1].name).to.eql('missing-required-parameter'); + expect(item.response[1]._postman_previewlanguage).to.eql('json'); + expect(JSON.parse(item.response[1].body)).to.eql({ hello: 'world' }); + expect(JSON.parse(item.response[1].originalRequest.body.raw)).to.eql({ + user: 1 + }); + done(); + }); + }); + + it('response body contains multiple examples but response body has single example', function(done) { + var openapi = fs.readFileSync(multiExampleResponse, 'utf8'); + Converter.convertV2({ type: 'string', data: openapi }, { parametersResolution: 'Example' }, + (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.length).to.equal(1); + + const item = conversionResult.output[0].data.item[0].item[0]; + + expect(JSON.parse(item.request.body.raw)).to.eql({ hello: 'world' }); + expect(item.response).to.have.lengthOf(2); + expect(item.response[0].name).to.eql('valid-request'); + expect(item.response[0]._postman_previewlanguage).to.eql('json'); + expect(JSON.parse(item.response[0].body)).to.eql({ + user: 1, + height: 168, + weight: 44 + }); + expect(JSON.parse(item.response[0].originalRequest.body.raw)).to.eql({ hello: 'world' }); + + expect(item.response[1].name).to.eql('missing-required-parameter'); + expect(item.response[1]._postman_previewlanguage).to.eql('json'); + expect(JSON.parse(item.response[1].body)).to.eql({ user: 1 }); + expect(JSON.parse(item.response[1].originalRequest.body.raw)).to.eql({ hello: 'world' }); + done(); + }); + }); + + it('both request and response body contains multiple examples with matching keys', function(done) { + var openapi = fs.readFileSync(multiExampleMatchingRequestResponse, 'utf8'); + Converter.convertV2({ type: 'string', data: openapi }, { parametersResolution: 'Example' }, + (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.length).to.equal(1); + + const item = conversionResult.output[0].data.item[0].item[0]; + + expect(JSON.parse(item.request.body.raw)).to.eql({ + includedFields: ['user', 'height', 'weight'] + }); + expect(item.response).to.have.lengthOf(2); + expect(item.response[0].name).to.eql('Complete request'); + expect(item.response[0]._postman_previewlanguage).to.eql('json'); + expect(JSON.parse(item.response[0].body)).to.eql({ + user: 1, + height: 168, + weight: 44 + }); + expect(JSON.parse(item.response[0].originalRequest.body.raw)).to.eql({ + includedFields: ['user', 'height', 'weight'] + }); + + expect(item.response[1].name).to.eql('Request with only required params'); + expect(item.response[1]._postman_previewlanguage).to.eql('json'); + expect(JSON.parse(item.response[1].body)).to.eql({ user: 1 }); + expect(JSON.parse(item.response[1].originalRequest.body.raw)).to.eql({ + includedFields: ['user'] + }); + done(); + }); + }); + + it('both request and response body contains multiple examples in mentioned order when no matching keys', + function(done) { + var openapi = fs.readFileSync(multiExampleRequestResponse, 'utf8'); + Converter.convertV2({ type: 'string', data: openapi }, { parametersResolution: 'Example' }, + (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.length).to.equal(1); + + const item = conversionResult.output[0].data.item[0].item[0]; + + expect(JSON.parse(item.request.body.raw)).to.eql({ + includedFields: ['user', 'height', 'weight'] + }); + expect(item.response).to.have.lengthOf(2); + expect(item.response[0].name).to.eql('Request with only required params'); + expect(item.response[0]._postman_previewlanguage).to.eql('json'); + expect(JSON.parse(item.response[0].body)).to.eql({ + user: 1 + }); + expect(JSON.parse(item.response[0].originalRequest.body.raw)).to.eql({ + includedFields: ['user', 'height', 'weight'] + }); + + expect(item.response[1].name).to.eql('Complete request'); + expect(item.response[1]._postman_previewlanguage).to.eql('json'); + expect(JSON.parse(item.response[1].body)).to.eql({ + user: 1, + height: 168, + weight: 44 + }); + expect(JSON.parse(item.response[1].originalRequest.body.raw)).to.eql({ + includedFields: ['user'] + }); + done(); + }); + }); + }); });