mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
fix: openapi spec with example values (#6476)
* fix: openapi spec with example values * fix * fix
This commit is contained in:
@@ -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 : ''}</${xmlName}>`;
|
||||
}
|
||||
|
||||
// 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 : ''}</${xmlName}>`;
|
||||
}
|
||||
|
||||
// Primitive value
|
||||
const content = value != null ? String(value) : '';
|
||||
return `${indent}<${xmlName}>${content}</${xmlName}>`;
|
||||
};
|
||||
|
||||
// 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 `<?xml version="1.0" encoding="UTF-8"?>\n<${rootName}${attrStr}>${childrenStr}</${rootName}>`;
|
||||
};
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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('<?xml version="1.0" encoding="UTF-8"?>');
|
||||
expect(request.request.body.xml).toContain('<name></name>');
|
||||
});
|
||||
|
||||
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('<?xml');
|
||||
expect(xmlRequest.examples[0].request.body.xml).toContain('<Root');
|
||||
expect(xmlRequest.examples[0].request.body.xml).not.toContain('{');
|
||||
});
|
||||
|
||||
it('should detect file fields in multipart example', () => {
|
||||
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('<name>John</name>');
|
||||
expect(request.request.body.xml).toContain('<age>30</age>');
|
||||
});
|
||||
|
||||
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: '<user><name>John</name></user>'
|
||||
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('<user><name>John</name></user>');
|
||||
});
|
||||
|
||||
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('[]');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user