diff --git a/packages/bruno-converters/src/openapi/openapi-to-bruno.js b/packages/bruno-converters/src/openapi/openapi-to-bruno.js index b910c443e..4435f6420 100644 --- a/packages/bruno-converters/src/openapi/openapi-to-bruno.js +++ b/packages/bruno-converters/src/openapi/openapi-to-bruno.js @@ -117,6 +117,130 @@ const getBodyTypeFromContentType = (contentType) => { return 'text'; }; +/** + * Gets a default value for a schema based on its type, format, and constraints + * Prioritizes: explicit example > enum first value > format-specific example > type default + * @param {Object} schema - The OpenAPI schema object + * @param {Map} visited - Map to track circular references + * @returns {*} - The default value for the schema + */ +const getDefaultValueForSchema = (schema, visited = new Map()) => { + // Check for explicit example first + if (schema.example !== undefined) { + return schema.example; + } + + // Check for enum and use first value + if (schema.enum && schema.enum.length > 0) { + return schema.enum[0]; + } + + // Handle different types + if (schema.type === 'object' || schema.properties) { + return buildEmptyJsonBody(schema, visited); + } + + if (schema.type === 'array') { + // Check for array-level example + if (schema.example !== undefined) { + return schema.example; + } + + if (schema.items) { + if (schema.items.type === 'object' || schema.items.properties) { + return [buildEmptyJsonBody(schema.items, visited)]; + } + // For primitive arrays, get example from items + if (schema.items.example !== undefined) { + return Array.isArray(schema.items.example) ? schema.items.example : [schema.items.example]; + } + // Return array with a single default primitive value + const itemDefault = getDefaultValueForSchema(schema.items, visited); + if (itemDefault !== '' && itemDefault !== 0 && itemDefault !== false) { + return [itemDefault]; + } + } + return []; + } + + if (schema.type === 'integer' || schema.type === 'number') { + return 0; + } + + if (schema.type === 'boolean') { + return false; + } + + // Default for strings and other types + return ''; +}; + +/** + * Builds XML string from OpenAPI schema + * @param {Object} bodySchema - The OpenAPI schema object + * @returns {string} - XML string + */ +const buildXmlBody = (bodySchema) => { + if (!bodySchema) return ''; + + // String example = raw XML, return as-is + if (typeof bodySchema.example === 'string') { + return bodySchema.example; + } + + const exampleValues = typeof bodySchema.example === 'object' ? bodySchema.example : null; + + if (!bodySchema.properties && !exampleValues) return ''; + + const rootName = bodySchema.xml?.name || 'root'; + + // Build a single XML element + const buildElement = (name, prop = {}, value, indent = ' ') => { + const xmlName = prop.xml?.name || name; + + if (prop.xml?.attribute) return null; + + // Nested object - recurse into children + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + const children = Object.entries(value) + .map(([k, v]) => buildElement(k, prop.properties?.[k] || {}, v, indent + ' ')) + .filter(Boolean); + return `${indent}<${xmlName}>${children.length ? '\n' + children.join('\n') + '\n' + indent : ''}`; + } + + // Object schema without value - build empty structure from schema + if (prop.type === 'object' || prop.properties) { + const children = Object.entries(prop.properties || {}) + .map(([k, p]) => buildElement(k, p, undefined, indent + ' ')) + .filter(Boolean); + return `${indent}<${xmlName}>${children.length ? '\n' + children.join('\n') + '\n' + indent : ''}`; + } + + // Primitive value + const content = value != null ? String(value) : ''; + return `${indent}<${xmlName}>${content}`; + }; + + // Collect attributes + const attributes = Object.entries(bodySchema.properties || {}) + .filter(([, p]) => p.xml?.attribute) + .map(([name, p]) => `${p.xml?.name || name}="${exampleValues?.[name] ?? ''}"`); + + // Build child elements + const entries = bodySchema.properties + ? Object.entries(bodySchema.properties).map(([k, p]) => [k, p, exampleValues?.[k]]) + : Object.entries(exampleValues || {}).map(([k, v]) => [k, {}, v]); + + const children = entries + .map(([name, prop, value]) => buildElement(name, prop, value)) + .filter(Boolean); + + const attrStr = attributes.length ? ' ' + attributes.join(' ') : ''; + const childrenStr = children.length ? '\n' + children.join('\n') + '\n' : ''; + + return `\n<${rootName}${attrStr}>${childrenStr}`; +}; + const buildEmptyJsonBody = (bodySchema, visited = new Map()) => { // Check for circular references if (visited.has(bodySchema)) { @@ -128,25 +252,101 @@ const buildEmptyJsonBody = (bodySchema, visited = new Map()) => { let _jsonBody = {}; each(bodySchema.properties || {}, (prop, name) => { - if (prop.type === 'object' || prop.properties) { - _jsonBody[name] = buildEmptyJsonBody(prop, visited); - } else if (prop.type === 'array') { - if (prop.items && (prop.items.type === 'object' || prop.items.properties)) { - _jsonBody[name] = [buildEmptyJsonBody(prop.items, visited)]; - } else { - _jsonBody[name] = []; - } - } else if (prop.type === 'integer' || prop.type === 'number') { - _jsonBody[name] = 0; - } else if (prop.type === 'boolean') { - _jsonBody[name] = false; - } else { - _jsonBody[name] = ''; - } + _jsonBody[name] = getDefaultValueForSchema(prop, visited); }); return _jsonBody; }; +/** + * Body type handlers for different content types + * Each handler has: + * - match: function to test if this handler should process the mime type + * - mode: the Bruno body mode to set + * - handle: function to populate the body content + */ +const BODY_TYPE_HANDLERS = [ + { + match: (mimeType) => CONTENT_TYPE_PATTERNS.JSON.test(mimeType), + mode: 'json', + handle: (body, bodySchema) => { + if (bodySchema) { + if (bodySchema.example !== undefined) { + body.json = JSON.stringify(bodySchema.example, null, 2); + } else if (bodySchema.type === 'array') { + body.json = JSON.stringify(bodySchema.items ? [buildEmptyJsonBody(bodySchema.items)] : [], null, 2); + } else { + body.json = JSON.stringify(buildEmptyJsonBody(bodySchema), null, 2); + } + } + } + }, + { + match: (mimeType) => mimeType === 'application/x-www-form-urlencoded', + mode: 'formUrlEncoded', + handle: (body, bodySchema) => { + if (!bodySchema) return; + const fields = bodySchema.example || bodySchema.properties || {}; + const isExample = !!bodySchema.example; + + each(fields, (prop, name) => { + const value = isExample ? prop : (prop.example ?? prop.default ?? ''); + body.formUrlEncoded.push({ + uid: uuid(), + name, + value: value !== undefined ? String(value) : '', + description: prop.description || '', + enabled: true + }); + }); + } + }, + { + match: (mimeType) => mimeType === 'multipart/form-data', + mode: 'multipartForm', + handle: (body, bodySchema) => { + if (!bodySchema) return; + const fields = bodySchema.example || bodySchema.properties || {}; + const isExample = !!bodySchema.example; + + each(fields, (prop, name) => { + const isFileField = !isExample && prop.type === 'string' && prop.format === 'binary'; + const value = isFileField ? [] : isExample ? prop : (prop.example ?? prop.default ?? ''); + body.multipartForm.push({ + uid: uuid(), + type: isFileField ? 'file' : 'text', + name, + value: isFileField ? [] : (value !== undefined ? String(value) : ''), + description: prop.description || '', + enabled: true + }); + }); + } + }, + { + match: (mimeType) => CONTENT_TYPE_PATTERNS.XML.test(mimeType) || mimeType === 'application/xml', + mode: 'xml', + handle: (body, bodySchema) => { + body.xml = buildXmlBody(bodySchema); + } + }, + { + match: (mimeType) => mimeType === 'application/sparql-query', + mode: 'sparql', + handle: (body, bodySchema) => { + // Use example from schema if available + body.sparql = bodySchema?.example !== undefined ? String(bodySchema.example) : ''; + } + }, + { + match: (mimeType) => ['text/plain', 'application/octet-stream', '*/*'].includes(mimeType), + mode: 'text', + handle: (body, bodySchema) => { + // Use example from schema if available + body.text = bodySchema?.example !== undefined ? String(bodySchema.example) : ''; + } + } +]; + /** * Extracts or generates an example value from an OpenAPI schema * Handles objects, arrays, primitives, and explicit examples @@ -191,34 +391,33 @@ const getExampleFromSchema = (schema) => { }; /** - * Populates request body in Bruno example from a value - * Uses pattern matching to handle various MIME type variants + * Populates request body in Bruno example from schema + * Reuses BODY_TYPE_HANDLERS for consistent body generation * @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 {Object} params.bodySchema - The OpenAPI schema for the request body * @param {string} params.contentType - Content type (e.g., 'application/json', 'application/ld+json') */ -const populateRequestBody = ({ body, requestBodyValue, contentType }) => { - if (!requestBodyValue || !contentType || typeof contentType !== 'string') return; +const populateRequestBody = ({ body, bodySchema, contentType }) => { + if (!contentType || typeof contentType !== 'string') 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); + // Find matching handler and use it (same as main request body) + const handler = BODY_TYPE_HANDLERS.find((h) => h.match(normalizedContentType)); + if (handler) { + body.mode = handler.mode; + + // Clear arrays for form-based content types to avoid duplicates + // (since the body was deep-copied from the main request) + if (normalizedContentType === 'application/x-www-form-urlencoded') { + body.formUrlEncoded = []; + } else if (normalizedContentType === 'multipart/form-data') { + body.multipartForm = []; + } + + handler.handle(body, bodySchema); } }; @@ -231,11 +430,22 @@ const populateRequestBody = ({ body, requestBodyValue, contentType }) => { * @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 {Object} [params.requestBodySchema] - Optional request body schema 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 createBrunoExample = ({ brunoRequestItem, exampleValue, exampleName, exampleDescription, statusCode, contentType, requestBodySchema = null, requestBodyContentType = null }) => { + // Deep copy the body to avoid shared references + const bodyCopy = { + mode: brunoRequestItem.request.body.mode, + json: brunoRequestItem.request.body.json, + text: brunoRequestItem.request.body.text, + xml: brunoRequestItem.request.body.xml, + sparql: brunoRequestItem.request.body.sparql || null, + formUrlEncoded: [...(brunoRequestItem.request.body.formUrlEncoded || [])], + multipartForm: [...(brunoRequestItem.request.body.multipartForm || [])] + }; + const brunoExample = { uid: uuid(), itemUid: brunoRequestItem.uid, @@ -247,7 +457,7 @@ const createBrunoExample = ({ brunoRequestItem, exampleValue, exampleName, examp method: brunoRequestItem.request.method, headers: [...brunoRequestItem.request.headers], params: [...brunoRequestItem.request.params], - body: { ...brunoRequestItem.request.body } + body: bodyCopy }, response: { status: String(statusCode), @@ -268,9 +478,9 @@ const createBrunoExample = ({ brunoRequestItem, exampleValue, exampleName, examp } }; - // Populate request body if provided - if (requestBodyValue !== null) { - populateRequestBody({ body: brunoExample.request.body, requestBodyValue, contentType: requestBodyContentType }); + // Populate request body from schema if provided (reuses BODY_TYPE_HANDLERS) + if (requestBodySchema !== null) { + populateRequestBody({ body: brunoExample.request.body, bodySchema: requestBodySchema, contentType: requestBodyContentType }); } return brunoExample; @@ -518,56 +728,19 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => { // TODO: handle allOf/anyOf/oneOf if (_operationObject.requestBody) { - let content = get(_operationObject, 'requestBody.content', {}); - let mimeType = Object.keys(content)[0]; - let body = content[mimeType] || {}; - let bodySchema = body.schema; + const content = get(_operationObject, 'requestBody.content', {}); + const mimeType = Object.keys(content)[0]; + const bodyContent = content[mimeType] || {}; + const bodySchema = bodyContent.schema; // Normalize: lowercase (object keys may vary in case) const normalizedMimeType = typeof mimeType === 'string' ? mimeType.toLowerCase() : ''; - if (CONTENT_TYPE_PATTERNS.JSON.test(normalizedMimeType)) { - brunoRequestItem.request.body.mode = 'json'; - if (bodySchema && (bodySchema.type === 'object' || bodySchema.properties)) { - let _jsonBody = buildEmptyJsonBody(bodySchema); - brunoRequestItem.request.body.json = JSON.stringify(_jsonBody, null, 2); - } - if (bodySchema && bodySchema.type === 'array') { - brunoRequestItem.request.body.json = JSON.stringify([buildEmptyJsonBody(bodySchema.items)], null, 2); - } - } else if (normalizedMimeType === 'application/x-www-form-urlencoded') { - brunoRequestItem.request.body.mode = 'formUrlEncoded'; - if (bodySchema && (bodySchema.type === 'object' || bodySchema.properties)) { - each(bodySchema.properties || {}, (prop, name) => { - brunoRequestItem.request.body.formUrlEncoded.push({ - uid: uuid(), - name: name, - value: '', - description: prop.description || '', - enabled: true - }); - }); - } - } else if (normalizedMimeType === 'multipart/form-data') { - brunoRequestItem.request.body.mode = 'multipartForm'; - if (bodySchema && (bodySchema.type === 'object' || bodySchema.properties)) { - each(bodySchema.properties || {}, (prop, name) => { - brunoRequestItem.request.body.multipartForm.push({ - uid: uuid(), - type: 'text', - name: name, - value: '', - description: prop.description || '', - enabled: true - }); - }); - } - } else if (normalizedMimeType === 'text/plain') { - brunoRequestItem.request.body.mode = 'text'; - brunoRequestItem.request.body.text = ''; - } else if (CONTENT_TYPE_PATTERNS.XML.test(normalizedMimeType)) { - brunoRequestItem.request.body.mode = 'xml'; - brunoRequestItem.request.body.xml = ''; + // Find matching handler for this content type + const handler = BODY_TYPE_HANDLERS.find((h) => h.match(normalizedMimeType)); + if (handler) { + brunoRequestItem.request.body.mode = handler.mode; + handler.handle(brunoRequestItem.request.body, bodySchema); } } @@ -627,7 +800,7 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => { exampleDescription, statusCode, contentType: responseContentType, - requestBodyValue: matchingRequestBodyExample.value, + requestBodySchema: matchingRequestBodyExample.schema, requestBodyContentType: matchingRequestBodyExample.contentType })); } else if (requestBodyExamplesWithKeys.length > 0) { @@ -642,7 +815,7 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => { exampleDescription: combinedExampleDescription, statusCode, contentType: responseContentType, - requestBodyValue: rbExample.value, + requestBodySchema: rbExample.schema, requestBodyContentType: rbExample.contentType })); }); @@ -656,7 +829,7 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => { exampleDescription, statusCode, contentType: responseContentType, - requestBodyValue: rbExample.value, + requestBodySchema: rbExample.schema, requestBodyContentType: rbExample.contentType })); } else { @@ -677,32 +850,32 @@ const transformOpenapiRequestItem = (request, usedNames = new Set()) => { if (content.examples) { // Multiple request body examples Object.entries(content.examples).forEach(([exampleKey, example]) => { + const exampleValue = example.value !== undefined ? example.value : example; requestBodyExamples.push({ key: exampleKey, - value: example.value !== undefined ? example.value : example, + schema: { example: exampleValue }, // Wrap in schema format for BODY_TYPE_HANDLERS summary: example.summary, description: example.description, contentType: contentType }); }); } else if (content.example !== undefined) { - // Single request body example - convert to unified structure + // Single request body example - wrap in schema-like object requestBodyExamples.push({ - key: null, // No key for single example - value: content.example, + key: null, + schema: { example: content.example }, // Wrap in schema format for BODY_TYPE_HANDLERS summary: null, description: null, contentType: contentType }); } else if (content.schema) { - // Schema-based request body - convert to unified structure + // Schema-based request body - pass schema directly requestBodyExamples.push({ - key: null, // No key for schema - value: getExampleFromSchema(content.schema), + key: null, + schema: content.schema, summary: null, description: null, - contentType: contentType, - isSchema: true + contentType: contentType }); } }); diff --git a/packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-body.spec.js b/packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-body.spec.js index 670b51756..24a319612 100644 --- a/packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-body.spec.js +++ b/packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-body.spec.js @@ -232,3 +232,680 @@ paths: expect(bodyJson.active).toBe(false); }); }); + +describe('openapi requestBody content types', () => { + it('should handle raw body with */* content type as text', () => { + const openApiSpec = ` +openapi: "3.0.0" +info: + version: "1.0.0" + title: "Raw Body Test" +servers: + - url: "https://api.example.com" +paths: + /raw: + post: + summary: "Raw body endpoint" + operationId: "postRaw" + requestBody: + required: true + content: + "*/*": + schema: + type: string + responses: + '200': + description: "Success" +`; + + const result = openApiToBruno(openApiSpec); + + expect(result.items.length).toBe(1); + const request = result.items[0]; + + expect(request.request.body.mode).toBe('text'); + expect(request.request.body.text).toBe(''); + }); + + it('should handle binary body with application/octet-stream as text', () => { + const openApiSpec = ` +openapi: "3.0.0" +info: + version: "1.0.0" + title: "Binary Body Test" +servers: + - url: "https://api.example.com" +paths: + /binary: + post: + summary: "Binary body endpoint" + operationId: "postBinary" + requestBody: + required: true + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + '200': + description: "Success" +`; + + const result = openApiToBruno(openApiSpec); + + expect(result.items.length).toBe(1); + const request = result.items[0]; + + expect(request.request.body.mode).toBe('text'); + expect(request.request.body.text).toBe(''); + }); + + it('should handle XML body with application/xml content type', () => { + const openApiSpec = ` +openapi: "3.0.0" +info: + version: "1.0.0" + title: "XML Body Test" +servers: + - url: "https://api.example.com" +paths: + /xml: + post: + summary: "XML body endpoint" + operationId: "postXml" + requestBody: + required: true + content: + application/xml: + schema: + type: object + properties: + name: + type: string + responses: + '200': + description: "Success" +`; + + const result = openApiToBruno(openApiSpec); + + expect(result.items.length).toBe(1); + const request = result.items[0]; + + expect(request.request.body.mode).toBe('xml'); + expect(request.request.body.xml).toContain(''); + expect(request.request.body.xml).toContain(''); + }); + + it('should handle XML body with text/xml content type', () => { + const openApiSpec = ` +openapi: "3.0.0" +info: + version: "1.0.0" + title: "Text XML Body Test" +servers: + - url: "https://api.example.com" +paths: + /xml: + post: + summary: "XML body endpoint" + operationId: "postXml" + requestBody: + required: true + content: + text/xml: + schema: + type: object + responses: + '200': + description: "Success" +`; + + const result = openApiToBruno(openApiSpec); + + expect(result.items.length).toBe(1); + const request = result.items[0]; + + expect(request.request.body.mode).toBe('xml'); + }); + + it('should handle SPARQL query with application/sparql-query content type', () => { + const openApiSpec = ` +openapi: "3.0.0" +info: + version: "1.0.0" + title: "SPARQL Query Test" +servers: + - url: "https://api.example.com" +paths: + /sparql: + post: + summary: "SPARQL query endpoint" + operationId: "postSparql" + requestBody: + required: true + content: + application/sparql-query: + schema: + type: string + responses: + '200': + description: "Success" +`; + + const result = openApiToBruno(openApiSpec); + + expect(result.items.length).toBe(1); + const request = result.items[0]; + + expect(request.request.body.mode).toBe('sparql'); + expect(request.request.body.sparql).toBe(''); + }); + + it('should handle text/plain content type', () => { + const openApiSpec = ` +openapi: "3.0.0" +info: + version: "1.0.0" + title: "Text Plain Test" +servers: + - url: "https://api.example.com" +paths: + /text: + post: + summary: "Text body endpoint" + operationId: "postText" + requestBody: + required: true + content: + text/plain: + schema: + type: string + responses: + '200': + description: "Success" +`; + + const result = openApiToBruno(openApiSpec); + + expect(result.items.length).toBe(1); + const request = result.items[0]; + + expect(request.request.body.mode).toBe('text'); + expect(request.request.body.text).toBe(''); + }); + + it('should detect file fields in multipart/form-data based on format: binary', () => { + const openApiSpec = ` +openapi: "3.0.0" +info: + version: "1.0.0" + title: "Multipart File Detection Test" +servers: + - url: "https://api.example.com" +paths: + /upload: + post: + summary: "Upload with file and text fields" + operationId: "uploadFile" + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + description: "The file to upload" + description: + type: string + description: "File description" + userId: + type: integer + description: "User ID" + responses: + '200': + description: "Success" +`; + + const result = openApiToBruno(openApiSpec); + + expect(result.items.length).toBe(1); + const request = result.items[0]; + + expect(request.request.body.mode).toBe('multipartForm'); + expect(request.request.body.multipartForm.length).toBe(3); + + // Find the file field + const fileField = request.request.body.multipartForm.find((f) => f.name === 'file'); + expect(fileField).toBeDefined(); + expect(fileField.type).toBe('file'); + expect(fileField.value).toEqual([]); // File fields should have array value + + // Find the text fields + const descField = request.request.body.multipartForm.find((f) => f.name === 'description'); + expect(descField).toBeDefined(); + expect(descField.type).toBe('text'); + expect(descField.value).toBe(''); // Text fields should have string value + + const userIdField = request.request.body.multipartForm.find((f) => f.name === 'userId'); + expect(userIdField).toBeDefined(); + expect(userIdField.type).toBe('text'); + expect(userIdField.value).toBe(''); + }); + + it('should handle JSON variants like application/ld+json', () => { + const openApiSpec = ` +openapi: "3.0.0" +info: + version: "1.0.0" + title: "JSON-LD Test" +servers: + - url: "https://api.example.com" +paths: + /jsonld: + post: + summary: "JSON-LD endpoint" + operationId: "postJsonLd" + requestBody: + required: true + content: + application/ld+json: + schema: + type: object + properties: + "@context": + type: string + name: + type: string + responses: + '200': + description: "Success" +`; + + const result = openApiToBruno(openApiSpec); + + expect(result.items.length).toBe(1); + const request = result.items[0]; + + expect(request.request.body.mode).toBe('json'); + expect(request.request.body.json).not.toBeNull(); + }); + + it('should handle XML variants like application/atom+xml', () => { + const openApiSpec = ` +openapi: "3.0.0" +info: + version: "1.0.0" + title: "Atom XML Test" +servers: + - url: "https://api.example.com" +paths: + /feed: + post: + summary: "Atom feed endpoint" + operationId: "postFeed" + requestBody: + required: true + content: + application/atom+xml: + schema: + type: object + responses: + '200': + description: "Success" +`; + + const result = openApiToBruno(openApiSpec); + + expect(result.items.length).toBe(1); + const request = result.items[0]; + + expect(request.request.body.mode).toBe('xml'); + }); +}); + +describe('openapi example request body - should match main request body handling', () => { + const bodyTypesOpenApiSpec = ` +openapi: 3.1.0 +info: + title: Body Types Demo API + version: 1.0.0 +servers: + - url: https://api.example.com +paths: + /raw-body: + post: + summary: Raw body + requestBody: + content: + "*/*": + schema: + type: string + responses: + "200": + description: Success + /json-body: + post: + summary: JSON body + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: "John" + responses: + "200": + description: Success + content: + application/json: + schema: + type: object + /xml-body: + post: + summary: XML body + requestBody: + content: + application/xml: + schema: + type: object + xml: + name: Root + properties: + name: + type: string + responses: + "200": + description: Success + /multipart-body: + post: + summary: Multipart body + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + desc: + type: string + responses: + "200": + description: Success + /form-body: + post: + summary: Form body + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + query: + type: string + page: + type: integer + default: 1 + responses: + "200": + description: Success + /sparql-body: + post: + summary: SPARQL body + requestBody: + content: + application/sparql-query: + schema: + type: string + example: "SELECT * WHERE { ?s ?p ?o }" + responses: + "200": + description: Success +`; + + it('should match body mode between request and example for all content types', () => { + const result = openApiToBruno(bodyTypesOpenApiSpec); + const tests = [ + { name: 'Raw body', mode: 'text' }, + { name: 'JSON body', mode: 'json' }, + { name: 'XML body', mode: 'xml' }, + { name: 'Multipart body', mode: 'multipartForm' }, + { name: 'Form body', mode: 'formUrlEncoded' }, + { name: 'SPARQL body', mode: 'sparql' } + ]; + + tests.forEach(({ name, mode }) => { + const request = result.items.find((item) => item.name === name); + expect(request.request.body.mode).toBe(mode); + expect(request.examples[0].request.body.mode).toBe(mode); + }); + }); + + it('should generate proper XML in example (not JSON)', () => { + const result = openApiToBruno(bodyTypesOpenApiSpec); + const xmlRequest = result.items.find((item) => item.name === 'XML body'); + + expect(xmlRequest.examples[0].request.body.xml).toContain(' { + const result = openApiToBruno(bodyTypesOpenApiSpec); + const multipartRequest = result.items.find((item) => item.name === 'Multipart body'); + const fileField = multipartRequest.examples[0].request.body.multipartForm.find((f) => f.name === 'file'); + expect(fileField.type).toBe('file'); + }); + + it('should use default values in form example', () => { + const result = openApiToBruno(bodyTypesOpenApiSpec); + const formRequest = result.items.find((item) => item.name === 'Form body'); + const pageField = formRequest.examples[0].request.body.formUrlEncoded.find((f) => f.name === 'page'); + expect(pageField.value).toBe('1'); + }); + + it('should use example and enum values from schema in request body', () => { + const openApiSpec = ` +openapi: "3.0.0" +info: + version: "1.0.0" + title: "Test" +servers: + - url: "https://api.example.com" +paths: + /test: + post: + summary: "Test" + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: "John" + status: + type: string + enum: [active, inactive] + responses: + "200": + description: "OK" +`; + const result = openApiToBruno(openApiSpec); + const bodyJson = JSON.parse(result.items[0].request.body.json); + expect(bodyJson.name).toBe('John'); + expect(bodyJson.status).toBe('active'); + }); + + it('should use schema example values in main request body (not just examples)', () => { + const openApiSpec = ` +openapi: "3.0.0" +info: + version: "1.0.0" + title: "Schema Example Values Test" +servers: + - url: "https://api.example.com" +paths: + /users: + post: + summary: "Create user" + requestBody: + content: + application/json: + schema: + type: object + properties: + id: + type: integer + example: 9007199254740991 + name: + type: string + example: "example string" + email: + type: string + format: email + example: "user@example.com" + status: + type: string + enum: [pending, active, inactive] + createdDate: + type: string + format: date + example: "2025-01-01" + score: + type: number + example: 3.1415926535 + responses: + "201": + description: "Created" +`; + const result = openApiToBruno(openApiSpec); + const request = result.items[0]; + + // Main request body should use example values from schema + const bodyJson = JSON.parse(request.request.body.json); + expect(bodyJson.id).toBe(9007199254740991); + expect(bodyJson.name).toBe('example string'); + expect(bodyJson.email).toBe('user@example.com'); + expect(bodyJson.status).toBe('pending'); // first enum value + expect(bodyJson.createdDate).toBe('2025-01-01'); + expect(bodyJson.score).toBe(3.1415926535); + }); + + it('should handle XML body with object example (not produce [object Object])', () => { + const openApiSpec = ` +openapi: "3.0.0" +info: + version: "1.0.0" + title: "XML Object Example Test" +servers: + - url: "https://api.example.com" +paths: + /user: + post: + summary: "Create user" + operationId: "createUser" + requestBody: + required: true + content: + application/xml: + schema: + type: object + example: + name: "John" + age: 30 + properties: + name: + type: string + age: + type: integer + responses: + "201": + description: "Created" +`; + const result = openApiToBruno(openApiSpec); + const request = result.items[0]; + + expect(request.request.body.mode).toBe('xml'); + // Should NOT contain [object Object] + expect(request.request.body.xml).not.toContain('[object Object]'); + // Should contain the example values + expect(request.request.body.xml).toContain('John'); + expect(request.request.body.xml).toContain('30'); + }); + + it('should handle XML body with string example (raw XML)', () => { + const openApiSpec = ` +openapi: "3.0.0" +info: + version: "1.0.0" + title: "XML String Example Test" +servers: + - url: "https://api.example.com" +paths: + /user: + post: + summary: "Create user" + operationId: "createUser" + requestBody: + required: true + content: + application/xml: + schema: + type: string + example: 'John' + responses: + "201": + description: "Created" +`; + const result = openApiToBruno(openApiSpec); + const request = result.items[0]; + + expect(request.request.body.mode).toBe('xml'); + // Should preserve the raw XML string + expect(request.request.body.xml).toBe('John'); + }); + + it('should not crash when array schema has no items defined', () => { + const openApiSpec = ` +openapi: "3.0.0" +info: + version: "1.0.0" + title: "Array Without Items Test" +servers: + - url: "https://api.example.com" +paths: + /items: + post: + summary: "Create items" + operationId: "createItems" + requestBody: + required: true + content: + application/json: + schema: + type: array + responses: + "201": + description: "Created" +`; + // Should not throw an error + expect(() => openApiToBruno(openApiSpec)).not.toThrow(); + + const result = openApiToBruno(openApiSpec); + const request = result.items[0]; + + expect(request.request.body.mode).toBe('json'); + // Should produce an empty array + expect(request.request.body.json).toBe('[]'); + }); +}); diff --git a/packages/bruno-lang/v2/src/example/request/bruToJson.js b/packages/bruno-lang/v2/src/example/request/bruToJson.js index 9340cf651..162e5f98d 100644 --- a/packages/bruno-lang/v2/src/example/request/bruToJson.js +++ b/packages/bruno-lang/v2/src/example/request/bruToJson.js @@ -102,16 +102,12 @@ const astRequestAttribute = { }, requestmode(_1, _2, _3, _4, value) { const modeValue = value.sourceString ? value.sourceString.trim() : ''; - // If mode is "none", return a body with mode: "none" - if (modeValue === 'none') { - return { - body: { - mode: 'none' - } - }; - } - // For other modes, return nothing since the body parser will handle it - return {}; + // Return body with the mode set + return { + body: { + mode: modeValue || 'none' + } + }; }, requestparamspath(_1, _2, _3, _4, dictionary) { return {