Merge pull request #6039 from sanish-bruno/feat/openapi-examples

fix: import multiple types of example formats from openapi
This commit is contained in:
Bijin A B
2025-11-17 14:12:35 +05:30
committed by GitHub
3 changed files with 843 additions and 48 deletions

View File

@@ -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
});
}
});
}

View File

@@ -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);
});
});
});
});

View File

@@ -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();