diff --git a/packages/bruno-converters/src/openapi/openapi-to-bruno.js b/packages/bruno-converters/src/openapi/openapi-to-bruno.js index e1a36cbe1..54d459f18 100644 --- a/packages/bruno-converters/src/openapi/openapi-to-bruno.js +++ b/packages/bruno-converters/src/openapi/openapi-to-bruno.js @@ -3,6 +3,21 @@ import get from 'lodash/get'; import jsyaml from 'js-yaml'; import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common'; +// Content type patterns for matching MIME type variants +// These patterns handle structured types with many variants (e.g., application/ld+json, application/vnd.api+json) +// MIME types can contain: letters, numbers, hyphens, dots, and plus signs +const CONTENT_TYPE_PATTERNS = { + // Matches: application/json, application/ld+json, application/vnd.api+json, text/json, etc. + // Pattern: type/([base]+)?suffix where suffix is json + JSON: /^[\w\-.+]+\/([\w\-.+]+\+)?json$/, + // Matches: application/xml, text/xml, application/atom+xml, application/rss+xml, application/xhtml+xml, etc. + // Pattern: type/([base]+)?suffix where suffix is xml + XML: /^[\w\-.+]+\/([\w\-.+]+\+)?xml$/, + // Matches: text/html + // Pattern: type/([base]+)?suffix where suffix is html + HTML: /^[\w\-.+]+\/([\w\-.+]+\+)?html$/ +}; + const ensureUrl = (url) => { // removing multiple slashes after the protocol if it exists, or after the beginning of the string otherwise return url.replace(/([^:])\/{2,}/g, '$1/'); @@ -77,14 +92,28 @@ const getStatusText = (statusCode) => { return statusTexts[statusCode] || 'Unknown'; }; +/** + * Determines the body type based on content-type from OpenAPI spec + * Uses pattern matching to handle various MIME type variants (e.g., application/ld+json, application/vnd.api+json) + * @param {string} contentType - The content-type from OpenAPI spec (object key, e.g., "application/json") + * @returns {string} - The body type (json, xml, html, text) + */ const getBodyTypeFromContentType = (contentType) => { - if (contentType?.includes('application/json')) { + if (!contentType || typeof contentType !== 'string') { + return 'text'; + } + + // Normalize: lowercase (object keys may vary in case, but shouldn't have parameters or whitespace) + const normalizedContentType = contentType.toLowerCase(); + + if (CONTENT_TYPE_PATTERNS.JSON.test(normalizedContentType)) { return 'json'; - } else if (contentType?.includes('application/xml') || contentType?.includes('text/xml')) { + } else if (CONTENT_TYPE_PATTERNS.XML.test(normalizedContentType)) { return 'xml'; - } else if (contentType?.includes('text/html')) { + } else if (CONTENT_TYPE_PATTERNS.HTML.test(normalizedContentType)) { return 'html'; } + return 'text'; }; @@ -118,6 +147,135 @@ const buildEmptyJsonBody = (bodySchema, visited = new Map()) => { return _jsonBody; }; +/** + * Extracts or generates an example value from an OpenAPI schema + * Handles objects, arrays, primitives, and explicit examples + * @param {Object} schema - The OpenAPI schema object + * @returns {*} - The example value (object, array, or primitive) + */ +const getExampleFromSchema = (schema) => { + // Check for explicit example first + if (schema.example !== undefined) { + return schema.example; + } + + // Handle different schema types + if (schema.type === 'object' || (schema.properties && !schema.type)) { + // Handle object type or schema with properties (even if type is not explicitly set) + return buildEmptyJsonBody(schema); + } else if (schema.type === 'array') { + if (schema.items) { + // If items are objects (either by type or by having properties), create array with one example object + if (schema.items.type === 'object' || schema.items.properties) { + return [buildEmptyJsonBody(schema.items)]; + } + // For primitive array items, return array with default value + if (schema.items.type === 'integer' || schema.items.type === 'number') { + return [0]; + } else if (schema.items.type === 'boolean') { + return [false]; + } else if (schema.items.type === 'string') { + return ['']; + } + } + return []; + } else { + // For primitive types, use default values + if (schema.type === 'integer' || schema.type === 'number') { + return 0; + } else if (schema.type === 'boolean') { + return false; + } + return ''; + } +}; + +/** + * Populates request body in Bruno example from a value + * Uses pattern matching to handle various MIME type variants + * @param {Object} params - Parameters object + * @param {Object} params.body - The Bruno request body object to populate + * @param {*} params.requestBodyValue - The request body value to set + * @param {string} params.contentType - Content type (e.g., 'application/json', 'application/ld+json') + */ +const populateRequestBody = ({ body, requestBodyValue, contentType }) => { + if (!requestBodyValue || !contentType) return; + + // Normalize: lowercase (content types from OpenAPI spec object keys may vary in case) + const normalizedContentType = contentType.toLowerCase(); + + if (CONTENT_TYPE_PATTERNS.JSON.test(normalizedContentType)) { + body.mode = 'json'; + body.json = typeof requestBodyValue === 'object' ? JSON.stringify(requestBodyValue, null, 2) : requestBodyValue; + } else if (normalizedContentType === 'application/x-www-form-urlencoded') { + body.mode = 'formUrlEncoded'; + // Handle form data if needed + } else if (normalizedContentType === 'multipart/form-data') { + body.mode = 'multipartForm'; + // Handle multipart form data if needed + } else if (normalizedContentType === 'text/plain') { + body.mode = 'text'; + body.text = typeof requestBodyValue === 'object' ? JSON.stringify(requestBodyValue) : String(requestBodyValue); + } else if (CONTENT_TYPE_PATTERNS.XML.test(normalizedContentType)) { + body.mode = 'xml'; + body.xml = typeof requestBodyValue === 'object' ? JSON.stringify(requestBodyValue) : String(requestBodyValue); + } +}; + +/** + * Creates a Bruno example from OpenAPI example data + * @param {Object} params - Parameters object + * @param {Object} params.brunoRequestItem - The base Bruno request item + * @param {*} params.exampleValue - The example value (object, array, or primitive) + * @param {string} params.exampleName - Name of the example + * @param {string} params.exampleDescription - Description of the example + * @param {string|number} params.statusCode - HTTP status code (for response examples) + * @param {string} params.contentType - Content type (e.g., 'application/json') + * @param {*} [params.requestBodyValue] - Optional request body value to populate in the example + * @param {string} [params.requestBodyContentType] - Optional request body content type + * @returns {Object} Bruno example object + */ +const createBrunoExample = ({ brunoRequestItem, exampleValue, exampleName, exampleDescription, statusCode, contentType, requestBodyValue = null, requestBodyContentType = null }) => { + const brunoExample = { + uid: uuid(), + itemUid: brunoRequestItem.uid, + name: exampleName, + description: exampleDescription, + type: 'http-request', + request: { + url: brunoRequestItem.request.url, + method: brunoRequestItem.request.method, + headers: [...brunoRequestItem.request.headers], + params: [...brunoRequestItem.request.params], + body: { ...brunoRequestItem.request.body } + }, + response: { + status: String(statusCode), + statusText: getStatusText(statusCode), + headers: contentType ? [ + { + uid: uuid(), + name: 'Content-Type', + value: contentType, + description: '', + enabled: true + } + ] : [], + body: { + type: getBodyTypeFromContentType(contentType), + content: typeof exampleValue === 'object' ? JSON.stringify(exampleValue, null, 2) : exampleValue + } + } + }; + + // Populate request body if provided + if (requestBodyValue !== null) { + populateRequestBody({ body: brunoExample.request.body, requestBodyValue, contentType: requestBodyContentType }); + } + + return brunoExample; +}; + const transformOpenapiRequestItem = (request, usedNames = new Set()) => { let _operationObject = request.operationObject; @@ -325,7 +483,11 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => { let mimeType = Object.keys(content)[0]; let body = content[mimeType] || {}; let bodySchema = body.schema; - if (mimeType === 'application/json') { + + // Normalize: lowercase (object keys may vary in case) + const normalizedMimeType = mimeType.toLowerCase(); + + if (CONTENT_TYPE_PATTERNS.JSON.test(normalizedMimeType)) { brunoRequestItem.request.body.mode = 'json'; if (bodySchema && bodySchema.type === 'object') { let _jsonBody = buildEmptyJsonBody(bodySchema); @@ -334,7 +496,7 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => { if (bodySchema && bodySchema.type === 'array') { brunoRequestItem.request.body.json = JSON.stringify([buildEmptyJsonBody(bodySchema.items)], null, 2); } - } else if (mimeType === 'application/x-www-form-urlencoded') { + } else if (normalizedMimeType === 'application/x-www-form-urlencoded') { brunoRequestItem.request.body.mode = 'formUrlEncoded'; if (bodySchema && bodySchema.type === 'object') { each(bodySchema.properties || {}, (prop, name) => { @@ -347,7 +509,7 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => { }); }); } - } else if (mimeType === 'multipart/form-data') { + } else if (normalizedMimeType === 'multipart/form-data') { brunoRequestItem.request.body.mode = 'multipartForm'; if (bodySchema && bodySchema.type === 'object') { each(bodySchema.properties || {}, (prop, name) => { @@ -361,10 +523,10 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => { }); }); } - } else if (mimeType === 'text/plain') { + } else if (normalizedMimeType === 'text/plain') { brunoRequestItem.request.body.mode = 'text'; brunoRequestItem.request.body.text = ''; - } else if (mimeType === 'text/xml') { + } else if (CONTENT_TYPE_PATTERNS.XML.test(normalizedMimeType)) { brunoRequestItem.request.body.mode = 'xml'; brunoRequestItem.request.body.xml = ''; } @@ -391,56 +553,182 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => { } // Handle OpenAPI examples from responses and request body - if (_operationObject.responses || _operationObject.requestBody) { + if (_operationObject.responses) { const examples = []; + // Extract request body examples if they exist + // Unified structure: all request body data is stored as examples with contentType + const requestBodyExamples = []; + + /** + * Helper function to create examples with appropriate request body handling + * @param {Object} params - Parameters object + * @param {*} params.responseExampleValue - The response example value + * @param {string} params.exampleName - Name of the example + * @param {string} params.exampleDescription - Description of the example + * @param {string|number} params.statusCode - HTTP status code + * @param {string} params.responseContentType - Response content type + * @param {string} [params.responseExampleKey] - Optional response example key for matching + */ + const createExamplesWithRequestBody = ({ responseExampleValue, exampleName, exampleDescription, statusCode, responseContentType, responseExampleKey = null }) => { + const requestBodyExamplesWithKeys = requestBodyExamples.filter((rb) => rb.key !== null); + const requestBodyExamplesWithoutKeys = requestBodyExamples.filter((rb) => rb.key === null); + + // Check if there's a matching request body example by key + const matchingRequestBodyExample = responseExampleKey + ? requestBodyExamplesWithKeys.find((rb) => rb.key === responseExampleKey) + : null; + + if (matchingRequestBodyExample) { + // Use the matching request body example + examples.push(createBrunoExample({ + brunoRequestItem, + exampleValue: responseExampleValue, + exampleName, + exampleDescription, + statusCode, + contentType: responseContentType, + requestBodyValue: matchingRequestBodyExample.value, + requestBodyContentType: matchingRequestBodyExample.contentType + })); + } else if (requestBodyExamplesWithKeys.length > 0) { + // No match found, create all combinations with request body examples that have keys + requestBodyExamplesWithKeys.forEach((rbExample) => { + const combinedExampleName = `${exampleName} (${rbExample.summary || rbExample.key})`; + const combinedExampleDescription = exampleDescription || rbExample.description || ''; + examples.push(createBrunoExample({ + brunoRequestItem, + exampleValue: responseExampleValue, + exampleName: combinedExampleName, + exampleDescription: combinedExampleDescription, + statusCode, + contentType: responseContentType, + requestBodyValue: rbExample.value, + requestBodyContentType: rbExample.contentType + })); + }); + } else if (requestBodyExamplesWithoutKeys.length > 0) { + // Single example or schema - use the first one for all response examples + const rbExample = requestBodyExamplesWithoutKeys[0]; + examples.push(createBrunoExample({ + brunoRequestItem, + exampleValue: responseExampleValue, + exampleName, + exampleDescription, + statusCode, + contentType: responseContentType, + requestBodyValue: rbExample.value, + requestBodyContentType: rbExample.contentType + })); + } else { + // No request body, create example without request body + examples.push(createBrunoExample({ + brunoRequestItem, + exampleValue: responseExampleValue, + exampleName, + exampleDescription, + statusCode, + contentType: responseContentType + })); + } + }; + + if (_operationObject.requestBody && _operationObject.requestBody.content) { + Object.entries(_operationObject.requestBody.content).forEach(([contentType, content]) => { + if (content.examples) { + // Multiple request body examples + Object.entries(content.examples).forEach(([exampleKey, example]) => { + requestBodyExamples.push({ + key: exampleKey, + value: example.value !== undefined ? example.value : example, + summary: example.summary, + description: example.description, + contentType: contentType + }); + }); + } else if (content.example !== undefined) { + // Single request body example - convert to unified structure + requestBodyExamples.push({ + key: null, // No key for single example + value: content.example, + summary: null, + description: null, + contentType: contentType + }); + } else if (content.schema) { + // Schema-based request body - convert to unified structure + requestBodyExamples.push({ + key: null, // No key for schema + value: getExampleFromSchema(content.schema), + summary: null, + description: null, + contentType: contentType, + isSchema: true + }); + } + }); + } + // Handle response examples if (_operationObject.responses) { Object.entries(_operationObject.responses).forEach(([statusCode, response]) => { if (response.content) { Object.entries(response.content).forEach(([contentType, content]) => { + // Handle examples (plural) - multiple named examples if (content.examples) { Object.entries(content.examples).forEach(([exampleKey, example]) => { const exampleName = example.summary || exampleKey || `${statusCode} Response`; const exampleDescription = example.description || ''; + const exampleValue = example.value !== undefined ? example.value : example; - // Create Bruno example - const brunoExample = { - uid: uuid(), - itemUid: brunoRequestItem.uid, - name: exampleName, - description: exampleDescription, - type: 'http-request', - request: { - url: brunoRequestItem.request.url, - method: brunoRequestItem.request.method, - headers: [...brunoRequestItem.request.headers], - params: [...brunoRequestItem.request.params], - body: { ...brunoRequestItem.request.body } - }, - response: { - status: String(statusCode), - statusText: getStatusText(statusCode), - headers: [ - { - uid: uuid(), - name: 'Content-Type', - value: contentType, - description: '', - enabled: true - } - ], - body: { - type: getBodyTypeFromContentType(contentType), - content: typeof example.value === 'object' ? JSON.stringify(example.value, null, 2) : example.value - } - } - }; + createExamplesWithRequestBody({ + responseExampleValue: exampleValue, + exampleName, + exampleDescription, + statusCode, + responseContentType: contentType, + responseExampleKey: exampleKey + }); + }); + } else if (content.example !== undefined) { + // Handle example (singular) at content level + const exampleName = `${statusCode} Response`; + const exampleDescription = response.description || ''; - examples.push(brunoExample); + createExamplesWithRequestBody({ + responseExampleValue: content.example, + exampleName, + exampleDescription, + statusCode, + responseContentType: contentType + }); + } else if (content.schema) { + // Handle schema - extract or generate example from schema + const exampleValue = getExampleFromSchema(content.schema); + const exampleName = `${statusCode} Response`; + const exampleDescription = response.description || ''; + + createExamplesWithRequestBody({ + responseExampleValue: exampleValue, + exampleName, + exampleDescription, + statusCode, + responseContentType: contentType }); } }); + } else { + // Handle responses without content (e.g., 204 No Content) + const exampleName = `${statusCode} Response`; + const exampleDescription = response.description || ''; + + createExamplesWithRequestBody({ + responseExampleValue: '', + exampleName, + exampleDescription, + statusCode, + responseContentType: null + }); } }); } diff --git a/packages/bruno-converters/tests/openapi/openapi-with-examples.spec.js b/packages/bruno-converters/tests/openapi/openapi-with-examples.spec.js index 2948bbca6..be0ca476e 100644 --- a/packages/bruno-converters/tests/openapi/openapi-with-examples.spec.js +++ b/packages/bruno-converters/tests/openapi/openapi-with-examples.spec.js @@ -53,10 +53,10 @@ describe('OpenAPI with Examples', () => { const createUserRequest = brunoCollection.items.find((item) => item.name === 'Create a new user'); expect(createUserRequest).toBeDefined(); expect(createUserRequest.examples).toBeDefined(); - expect(createUserRequest.examples).toHaveLength(2); + expect(createUserRequest.examples).toHaveLength(4); // Check response examples - const createdExample = createUserRequest.examples.find((ex) => ex.name === 'User Created'); + const createdExample = createUserRequest.examples.find((ex) => ex.name === 'User Created (Valid User)'); expect(createdExample).toBeDefined(); expect(createdExample.response.status).toBe('201'); expect(createdExample.response.statusText).toBe('Created'); @@ -149,7 +149,7 @@ servers: expect(JSON.parse(example.response.body.content)).toEqual({ message: 'test' }); }); - it('should not create examples array if no examples are present', () => { + it('should create examples without specified request body, when response is present', () => { const openApiWithoutExamples = ` openapi: '3.0.0' info: @@ -174,7 +174,11 @@ servers: const brunoCollection = openApiToBruno(openApiWithoutExamples); const request = brunoCollection.items[0]; - expect(request.examples).toBeUndefined(); + expect(request.examples).toHaveLength(1); + const example = request.examples[0]; + expect(example.name).toBe('200 Response'); + expect(example.description).toBe('OK'); + expect(example.response.body.type).toBe('json'); }); it('should support path-based grouping when specified', () => { @@ -301,4 +305,507 @@ servers: expect(productsFolder.type).toBe('folder'); expect(productsFolder.items).toHaveLength(1); // GET /products }); + + describe('Request Body Examples', () => { + it('should match request body examples by key when response example key matches', () => { + const openApiWithMatchingKeys = ` +openapi: '3.0.0' +info: + version: '1.0.0' + title: 'API with Matching Keys' +paths: + /users: + post: + summary: 'Create user' + operationId: 'createUser' + requestBody: + required: true + content: + application/json: + examples: + valid_user: + summary: 'Valid User' + value: + name: 'John Doe' + email: 'john@example.com' + invalid_user: + summary: 'Invalid User' + value: + name: '' + email: 'invalid' + responses: + '201': + description: 'Created' + content: + application/json: + examples: + valid_user: + summary: 'User Created' + value: + id: 123 + name: 'John Doe' + invalid_user: + summary: 'Validation Error' + value: + error: 'Invalid input' +servers: + - url: 'https://api.example.com' +`; + + const brunoCollection = openApiToBruno(openApiWithMatchingKeys); + const request = brunoCollection.items[0]; + + expect(request.examples).toBeDefined(); + expect(request.examples).toHaveLength(2); + + // Check that matching keys are used + const validUserExample = request.examples.find((ex) => ex.name === 'User Created'); + expect(validUserExample).toBeDefined(); + expect(validUserExample.request.body.mode).toBe('json'); + expect(JSON.parse(validUserExample.request.body.json)).toEqual({ + name: 'John Doe', + email: 'john@example.com' + }); + expect(JSON.parse(validUserExample.response.body.content)).toEqual({ + id: 123, + name: 'John Doe' + }); + + const invalidUserExample = request.examples.find((ex) => ex.name === 'Validation Error'); + expect(invalidUserExample).toBeDefined(); + expect(JSON.parse(invalidUserExample.request.body.json)).toEqual({ + name: '', + email: 'invalid' + }); + expect(JSON.parse(invalidUserExample.response.body.content)).toEqual({ + error: 'Invalid input' + }); + }); + + it('should create all combinations when response example keys do not match request body examples', () => { + const openApiWithNonMatchingKeys = ` +openapi: '3.0.0' +info: + version: '1.0.0' + title: 'API with Non-Matching Keys' +paths: + /users: + post: + summary: 'Create user' + operationId: 'createUser' + requestBody: + required: true + content: + application/json: + examples: + valid_user: + summary: 'Valid User' + value: + name: 'John Doe' + email: 'john@example.com' + invalid_user: + summary: 'Invalid User' + value: + name: '' + email: 'invalid' + responses: + '201': + description: 'Created' + content: + application/json: + examples: + created: + summary: 'User Created' + value: + id: 123 + '400': + description: 'Bad Request' + content: + application/json: + examples: + error: + summary: 'Validation Error' + value: + error: 'Invalid input' +servers: + - url: 'https://api.example.com' +`; + + const brunoCollection = openApiToBruno(openApiWithNonMatchingKeys); + const request = brunoCollection.items[0]; + + expect(request.examples).toBeDefined(); + // Should have 4 examples: 2 response examples × 2 request body examples + expect(request.examples).toHaveLength(4); + + // Check combinations for 201 response + const createdWithValid = request.examples.find((ex) => ex.name === 'User Created (Valid User)'); + expect(createdWithValid).toBeDefined(); + expect(createdWithValid.response.status).toBe('201'); + expect(JSON.parse(createdWithValid.request.body.json)).toEqual({ + name: 'John Doe', + email: 'john@example.com' + }); + + const createdWithInvalid = request.examples.find((ex) => ex.name === 'User Created (Invalid User)'); + expect(createdWithInvalid).toBeDefined(); + expect(createdWithInvalid.response.status).toBe('201'); + expect(JSON.parse(createdWithInvalid.request.body.json)).toEqual({ + name: '', + email: 'invalid' + }); + + // Check combinations for 400 response + const errorWithValid = request.examples.find((ex) => ex.name === 'Validation Error (Valid User)'); + expect(errorWithValid).toBeDefined(); + expect(errorWithValid.response.status).toBe('400'); + + const errorWithInvalid = request.examples.find((ex) => ex.name === 'Validation Error (Invalid User)'); + expect(errorWithInvalid).toBeDefined(); + expect(errorWithInvalid.response.status).toBe('400'); + }); + + it('should use single request body example for all response examples', () => { + const openApiWithSingleRequestBody = ` +openapi: '3.0.0' +info: + version: '1.0.0' + title: 'API with Single Request Body' +paths: + /users: + post: + summary: 'Create user' + operationId: 'createUser' + requestBody: + required: true + content: + application/json: + example: + name: 'John Doe' + email: 'john@example.com' + responses: + '201': + description: 'Created' + content: + application/json: + examples: + created: + summary: 'User Created' + value: + id: 123 + duplicate: + summary: 'Duplicate User' + value: + error: 'User already exists' +servers: + - url: 'https://api.example.com' +`; + + const brunoCollection = openApiToBruno(openApiWithSingleRequestBody); + const request = brunoCollection.items[0]; + + expect(request.examples).toBeDefined(); + expect(request.examples).toHaveLength(2); + + // Both examples should have the same request body + const createdExample = request.examples.find((ex) => ex.name === 'User Created'); + expect(createdExample).toBeDefined(); + expect(createdExample.request.body.mode).toBe('json'); + expect(JSON.parse(createdExample.request.body.json)).toEqual({ + name: 'John Doe', + email: 'john@example.com' + }); + + const duplicateExample = request.examples.find((ex) => ex.name === 'Duplicate User'); + expect(duplicateExample).toBeDefined(); + expect(JSON.parse(duplicateExample.request.body.json)).toEqual({ + name: 'John Doe', + email: 'john@example.com' + }); + }); + + it('should use schema-based request body for all response examples', () => { + const openApiWithSchemaRequestBody = ` +openapi: '3.0.0' +info: + version: '1.0.0' + title: 'API with Schema Request Body' +paths: + /users: + post: + summary: 'Create user' + operationId: 'createUser' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - name + - email + properties: + name: + type: string + example: 'John Doe' + email: + type: string + format: email + example: 'john@example.com' + responses: + '201': + description: 'Created' + content: + application/json: + examples: + created: + summary: 'User Created' + value: + id: 123 + error: + summary: 'Error Response' + value: + error: 'Something went wrong' +servers: + - url: 'https://api.example.com' +`; + + const brunoCollection = openApiToBruno(openApiWithSchemaRequestBody); + const request = brunoCollection.items[0]; + + expect(request.examples).toBeDefined(); + expect(request.examples).toHaveLength(2); + + // Both examples should have request body generated from schema + const createdExample = request.examples.find((ex) => ex.name === 'User Created'); + expect(createdExample).toBeDefined(); + expect(createdExample.request.body.mode).toBe('json'); + const requestBody = JSON.parse(createdExample.request.body.json); + expect(requestBody).toHaveProperty('name'); + expect(requestBody).toHaveProperty('email'); + + const errorExample = request.examples.find((ex) => ex.name === 'Error Response'); + expect(errorExample).toBeDefined(); + expect(JSON.parse(errorExample.request.body.json)).toEqual(requestBody); + }); + + it('should handle request body examples with different content types', () => { + const openApiWithDifferentRequestBodyTypes = ` +openapi: '3.0.0' +info: + version: '1.0.0' + title: 'API with Different Request Body Types' +paths: + /data: + post: + summary: 'Post data' + operationId: 'postData' + requestBody: + required: true + content: + application/json: + examples: + json_data: + summary: 'JSON Data' + value: + message: 'Hello' + text/plain: + examples: + text_data: + summary: 'Text Data' + value: 'Hello World' + responses: + '200': + description: 'OK' + content: + application/json: + examples: + success: + summary: 'Success' + value: + status: 'ok' +servers: + - url: 'https://api.example.com' +`; + + const brunoCollection = openApiToBruno(openApiWithDifferentRequestBodyTypes); + const request = brunoCollection.items[0]; + + expect(request.examples).toBeDefined(); + // Should create combinations: 1 response × 2 request body examples = 2 examples + expect(request.examples).toHaveLength(2); + + const jsonExample = request.examples.find((ex) => ex.name === 'Success (JSON Data)'); + expect(jsonExample).toBeDefined(); + expect(jsonExample.request.body.mode).toBe('json'); + expect(JSON.parse(jsonExample.request.body.json)).toEqual({ message: 'Hello' }); + + const textExample = request.examples.find((ex) => ex.name === 'Success (Text Data)'); + expect(textExample).toBeDefined(); + expect(textExample.request.body.mode).toBe('text'); + expect(textExample.request.body.text).toBe('Hello World'); + }); + + it('should handle mixed matching and non-matching request body examples', () => { + const openApiWithMixedMatching = ` +openapi: '3.0.0' +info: + version: '1.0.0' + title: 'API with Mixed Matching' +paths: + /users: + post: + summary: 'Create user' + operationId: 'createUser' + requestBody: + required: true + content: + application/json: + examples: + valid_user: + summary: 'Valid User' + value: + name: 'John Doe' + email: 'john@example.com' + invalid_user: + summary: 'Invalid User' + value: + name: '' + email: 'invalid' + responses: + '201': + description: 'Created' + content: + application/json: + examples: + valid_user: + summary: 'User Created' + value: + id: 123 + unmatched: + summary: 'Unmatched Response' + value: + id: 456 +servers: + - url: 'https://api.example.com' +`; + + const brunoCollection = openApiToBruno(openApiWithMixedMatching); + const request = brunoCollection.items[0]; + + expect(request.examples).toBeDefined(); + // Should have: 1 matched (valid_user) + 2 combinations for unmatched (unmatched × 2 request body examples) = 3 + expect(request.examples).toHaveLength(3); + + // Matched example + const matchedExample = request.examples.find((ex) => ex.name === 'User Created'); + expect(matchedExample).toBeDefined(); + expect(JSON.parse(matchedExample.request.body.json)).toEqual({ + name: 'John Doe', + email: 'john@example.com' + }); + + // Unmatched combinations + const unmatchedWithValid = request.examples.find((ex) => ex.name === 'Unmatched Response (Valid User)'); + expect(unmatchedWithValid).toBeDefined(); + + const unmatchedWithInvalid = request.examples.find((ex) => ex.name === 'Unmatched Response (Invalid User)'); + expect(unmatchedWithInvalid).toBeDefined(); + }); + + it('should not create request body when no request body is defined', () => { + const openApiWithoutRequestBody = ` +openapi: '3.0.0' +info: + version: '1.0.0' + title: 'API without Request Body' +paths: + /users: + get: + summary: 'Get users' + operationId: 'getUsers' + responses: + '200': + description: 'OK' + content: + application/json: + examples: + success: + summary: 'Success' + value: + users: [] +servers: + - url: 'https://api.example.com' +`; + + const brunoCollection = openApiToBruno(openApiWithoutRequestBody); + const request = brunoCollection.items[0]; + + expect(request.examples).toBeDefined(); + expect(request.examples).toHaveLength(1); + + const example = request.examples[0]; + expect(example.request.body.mode).toBe('none'); + expect(example.request.body.json).toBeNull(); + }); + + it('should handle request body with singular example and multiple response examples', () => { + const openApiWithSingularExample = ` +openapi: '3.0.0' +info: + version: '1.0.0' + title: 'API with Singular Example' +paths: + /users: + post: + summary: 'Create user' + operationId: 'createUser' + requestBody: + required: true + content: + application/json: + example: + name: 'Jane Doe' + email: 'jane@example.com' + responses: + '201': + description: 'Created' + content: + application/json: + examples: + created: + summary: 'User Created' + value: + id: 1 + duplicate: + summary: 'Duplicate' + value: + id: 2 + '400': + description: 'Bad Request' + content: + application/json: + examples: + error: + summary: 'Error' + value: + error: 'Bad request' +servers: + - url: 'https://api.example.com' +`; + + const brunoCollection = openApiToBruno(openApiWithSingularExample); + const request = brunoCollection.items[0]; + + expect(request.examples).toBeDefined(); + expect(request.examples).toHaveLength(3); + + // All examples should have the same request body + const requestBodyValue = { name: 'Jane Doe', email: 'jane@example.com' }; + request.examples.forEach((example) => { + expect(example.request.body.mode).toBe('json'); + expect(JSON.parse(example.request.body.json)).toEqual(requestBodyValue); + }); + }); + }); }); diff --git a/tests/import/openapi/import-openapi-with-examples.spec.ts b/tests/import/openapi/import-openapi-with-examples.spec.ts index bb3b17b41..a644ce0eb 100644 --- a/tests/import/openapi/import-openapi-with-examples.spec.ts +++ b/tests/import/openapi/import-openapi-with-examples.spec.ts @@ -124,8 +124,8 @@ test.describe('Import OpenAPI Collection with Examples', () => { await chevronIcon.click(); // Check if examples are visible - const createdExample = page.locator('.collection-item-name').getByText('User Created'); - const validationErrorExample = page.locator('.collection-item-name').getByText('Validation Error'); + const createdExample = page.locator('.collection-item-name').getByText('User Created (Valid User)'); + const validationErrorExample = page.locator('.collection-item-name').getByText('Validation Error (Invalid User)'); await expect(createdExample).toBeVisible(); await expect(validationErrorExample).toBeVisible();