mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-15 03:41:28 +00:00
Merge pull request #6039 from sanish-bruno/feat/openapi-examples
fix: import multiple types of example formats from openapi
This commit is contained in:
@@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user