mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-01 00:24:08 +00:00
fix: code generator headers and multipartForm bug (#5056)
* fix: code generator headers and multipartForm bug
This commit is contained in:
@@ -1,18 +1,7 @@
|
||||
import { get } from 'lodash';
|
||||
import {
|
||||
findItemInCollection,
|
||||
findParentItemInCollection
|
||||
} from 'utils/collections';
|
||||
|
||||
export const getTreePathFromCollectionToItem = (collection, _itemUid) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _itemUid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item?.uid);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
getTreePathFromCollectionToItem
|
||||
} from 'utils/collections/index';
|
||||
|
||||
// Resolve inherited auth by traversing up the folder hierarchy
|
||||
export const resolveInheritedAuth = (item, collection) => {
|
||||
@@ -29,7 +18,7 @@ export const resolveInheritedAuth = (item, collection) => {
|
||||
}
|
||||
|
||||
// Get the tree path from collection to item
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item.uid);
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
|
||||
// Default to collection auth
|
||||
const collectionAuth = get(collection, 'root.request.auth', { mode: 'none' });
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import { resolveInheritedAuth } from './auth-utils';
|
||||
|
||||
jest.mock('utils/collections/index', () => ({
|
||||
getTreePathFromCollectionToItem: (collection, item) => {
|
||||
const itemUid = item.uid;
|
||||
|
||||
if (itemUid === 'r1') {
|
||||
return [collection.items[0], collection.items[0].items[0]];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}));
|
||||
|
||||
// Helper to build mock collection structure
|
||||
const buildCollection = () => {
|
||||
return {
|
||||
|
||||
@@ -43,20 +43,24 @@ export const interpolateBody = (body, variables = {}) => {
|
||||
break;
|
||||
|
||||
case 'formUrlEncoded':
|
||||
interpolatedBody.formUrlEncoded = body.formUrlEncoded.map((param) => ({
|
||||
...param,
|
||||
value: param.enabled ? interpolate(param.value, variables) : param.value
|
||||
}));
|
||||
interpolatedBody.formUrlEncoded = Array.isArray(body.formUrlEncoded)
|
||||
? body.formUrlEncoded.map((param) => ({
|
||||
...param,
|
||||
value: param.enabled ? interpolate(param.value, variables) : param.value
|
||||
}))
|
||||
: [];
|
||||
break;
|
||||
|
||||
case 'multipartForm':
|
||||
interpolatedBody.multipartForm = body.multipartForm.map((param) => ({
|
||||
...param,
|
||||
value:
|
||||
param.type === 'text' && param.enabled
|
||||
? interpolate(param.value, variables)
|
||||
: param.value
|
||||
}));
|
||||
interpolatedBody.multipartForm = Array.isArray(body.multipartForm)
|
||||
? body.multipartForm.map((param) => ({
|
||||
...param,
|
||||
value:
|
||||
param.type === 'text' && param.enabled
|
||||
? interpolate(param.value, variables)
|
||||
: param.value
|
||||
}))
|
||||
: [];
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
@@ -1,8 +1,46 @@
|
||||
import { buildHarRequest } from 'utils/codegenerator/har';
|
||||
import { getAuthHeaders } from 'utils/codegenerator/auth';
|
||||
import { getAllVariables } from 'utils/collections/index';
|
||||
import { getAllVariables, getTreePathFromCollectionToItem } from 'utils/collections/index';
|
||||
import { interpolateHeaders, interpolateBody, createVariablesObject } from './interpolation';
|
||||
|
||||
// Merge headers from collection, folders, and request
|
||||
const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
let headers = new Map();
|
||||
|
||||
// Add collection headers first
|
||||
const collectionHeaders = collection?.root?.request?.headers || [];
|
||||
collectionHeaders.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header);
|
||||
}
|
||||
});
|
||||
|
||||
// Add folder headers next, traversing from root to leaf
|
||||
if (requestTreePath && requestTreePath.length > 0) {
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
const folderHeaders = i?.root?.request?.headers || [];
|
||||
folderHeaders.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add request headers last (they take precedence)
|
||||
const requestHeaders = request.headers || [];
|
||||
requestHeaders.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert Map back to array
|
||||
return Array.from(headers.values());
|
||||
};
|
||||
|
||||
const generateSnippet = ({ language, item, collection, shouldInterpolate = false }) => {
|
||||
try {
|
||||
// Get HTTPSnippet dynamically so mocks can be applied in tests
|
||||
@@ -22,8 +60,9 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
|
||||
|
||||
const request = item.request;
|
||||
|
||||
// Prepare headers
|
||||
let headers = [...(request.headers || [])];
|
||||
// Get the request tree path and merge headers
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
let headers = mergeHeaders(collection, request, requestTreePath);
|
||||
|
||||
// Add auth headers if needed
|
||||
if (request.auth && request.auth.mode !== 'none') {
|
||||
@@ -58,5 +97,6 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
|
||||
};
|
||||
|
||||
export {
|
||||
generateSnippet
|
||||
generateSnippet,
|
||||
mergeHeaders
|
||||
};
|
||||
@@ -50,10 +50,11 @@ jest.mock('utils/collections/index', () => ({
|
||||
baseUrl: 'https://api.example.com',
|
||||
apiKey: 'secret-key-123',
|
||||
userId: '12345'
|
||||
}))
|
||||
})),
|
||||
getTreePathFromCollectionToItem: jest.fn(() => [])
|
||||
}));
|
||||
|
||||
import { generateSnippet } from './snippet-generator';
|
||||
import { generateSnippet, mergeHeaders } from './snippet-generator';
|
||||
|
||||
describe('Snippet Generator - Simple Tests', () => {
|
||||
|
||||
@@ -418,4 +419,166 @@ describe('Snippet Generator - Simple Tests', () => {
|
||||
|
||||
expect(result).toBe('curl -X POST https://api.test.com/{{endpoint}} -H "Content-Type: application/json" -d \'{"name": "{{userName}}", "email": "{{userEmail}}", "age": {{userAge}}}\'');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeHeaders', () => {
|
||||
it('should include headers from collection, folder and request (with correct precedence)', () => {
|
||||
const collection = {
|
||||
root: {
|
||||
request: {
|
||||
headers: [
|
||||
{ name: 'X-Collection', value: 'c', enabled: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const folder = {
|
||||
type: 'folder',
|
||||
root: {
|
||||
request: {
|
||||
headers: [
|
||||
{ name: 'X-Folder', value: 'f', enabled: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const request = {
|
||||
headers: [
|
||||
{ name: 'X-Request', value: 'r', enabled: true }
|
||||
]
|
||||
};
|
||||
|
||||
const headers = mergeHeaders(collection, request, [folder]);
|
||||
const names = headers.map((h) => h.name);
|
||||
expect(names).toEqual(expect.arrayContaining(['X-Collection', 'X-Folder', 'X-Request']));
|
||||
});
|
||||
});
|
||||
|
||||
// Snippet should include inherited headers
|
||||
describe('generateSnippet – header inclusion in output', () => {
|
||||
it('should include collection and folder headers in generated snippet', () => {
|
||||
const language = { target: 'shell', client: 'curl' };
|
||||
|
||||
const collection = {
|
||||
root: {
|
||||
request: {
|
||||
headers: [
|
||||
{ name: 'X-Collection', value: 'c', enabled: true }
|
||||
],
|
||||
auth: { mode: 'none' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const folder = {
|
||||
uid: 'f1',
|
||||
type: 'folder',
|
||||
root: {
|
||||
request: {
|
||||
headers: [
|
||||
{ name: 'X-Folder', value: 'f', enabled: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const item = {
|
||||
uid: 'r1',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://example.com',
|
||||
headers: [],
|
||||
auth: { mode: 'none' }
|
||||
}
|
||||
};
|
||||
|
||||
// Override tree path to include folder
|
||||
const utilsCollections = require('utils/collections/index');
|
||||
utilsCollections.getTreePathFromCollectionToItem.mockImplementation(() => [folder]);
|
||||
|
||||
// Custom HTTPSnippet mock that outputs headers list
|
||||
const originalHTTPSnippet = require('httpsnippet').HTTPSnippet;
|
||||
require('httpsnippet').HTTPSnippet = jest.fn().mockImplementation((harRequest) => ({
|
||||
convert: jest.fn(() => `HEADERS:${harRequest.headers.map((h) => h.name).join(',')}`)
|
||||
}));
|
||||
|
||||
const result = generateSnippet({ language, item, collection, shouldInterpolate: false });
|
||||
|
||||
// Restore original mock
|
||||
require('httpsnippet').HTTPSnippet = originalHTTPSnippet;
|
||||
|
||||
expect(result).toContain('X-Collection');
|
||||
expect(result).toContain('X-Folder');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateSnippet with edge-case bodies', () => {
|
||||
const language = { target: 'shell', client: 'curl' };
|
||||
const baseCollection = { root: { request: { auth: { mode: 'none' }, headers: [] } } };
|
||||
|
||||
it('should generate snippet for empty formUrlEncoded body when interpolation is disabled', () => {
|
||||
const item = {
|
||||
uid: 'req1',
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: 'https://example.com',
|
||||
headers: [],
|
||||
body: { mode: 'formUrlEncoded', formUrlEncoded: [] },
|
||||
auth: { mode: 'none' }
|
||||
}
|
||||
};
|
||||
|
||||
const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
|
||||
expect(result).toMatch(/^curl -X POST/);
|
||||
});
|
||||
|
||||
it('should generate snippet for empty multipartForm body when interpolation is disabled', () => {
|
||||
const item = {
|
||||
uid: 'req2',
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: 'https://example.com',
|
||||
headers: [],
|
||||
body: { mode: 'multipartForm' },
|
||||
auth: { mode: 'none' }
|
||||
}
|
||||
};
|
||||
|
||||
const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
|
||||
expect(result).toMatch(/^curl -X POST/);
|
||||
});
|
||||
|
||||
it('should generate snippet for undefined formUrlEncoded array with interpolation enabled', () => {
|
||||
const item = {
|
||||
uid: 'req3',
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: 'https://example.com',
|
||||
headers: [],
|
||||
body: { mode: 'formUrlEncoded' },
|
||||
auth: { mode: 'none' }
|
||||
}
|
||||
};
|
||||
|
||||
const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: true });
|
||||
expect(result).toMatch(/^curl -X POST/);
|
||||
});
|
||||
|
||||
it('should generate snippet for empty multipartForm array with interpolation enabled', () => {
|
||||
const item = {
|
||||
uid: 'req4',
|
||||
request: {
|
||||
method: 'POST',
|
||||
url: 'https://example.com',
|
||||
headers: [],
|
||||
body: { mode: 'multipartForm', multipartForm: [] },
|
||||
auth: { mode: 'none' }
|
||||
}
|
||||
};
|
||||
|
||||
const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: true });
|
||||
expect(result).toMatch(/^curl -X POST/);
|
||||
});
|
||||
});
|
||||
@@ -68,15 +68,15 @@ const createPostData = (body, type) => {
|
||||
return {
|
||||
mimeType: contentType,
|
||||
text: new URLSearchParams(
|
||||
body[body.mode]
|
||||
.filter((param) => param.enabled)
|
||||
(Array.isArray(body[body.mode]) ? body[body.mode] : [])
|
||||
.filter((param) => param?.enabled)
|
||||
.reduce((acc, param) => {
|
||||
acc[param.name] = param.value;
|
||||
return acc;
|
||||
}, {})
|
||||
).toString(),
|
||||
params: body[body.mode]
|
||||
.filter((param) => param.enabled)
|
||||
params: (Array.isArray(body[body.mode]) ? body[body.mode] : [])
|
||||
.filter((param) => param?.enabled)
|
||||
.map((param) => ({
|
||||
name: param.name,
|
||||
value: param.value
|
||||
@@ -85,8 +85,8 @@ const createPostData = (body, type) => {
|
||||
case 'multipartForm':
|
||||
return {
|
||||
mimeType: contentType,
|
||||
params: body[body.mode]
|
||||
.filter((param) => param.enabled)
|
||||
params: (Array.isArray(body[body.mode]) ? body[body.mode] : [])
|
||||
.filter((param) => param?.enabled)
|
||||
.map((param) => ({
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
|
||||
@@ -954,7 +954,7 @@ export const maskInputValue = (value) => {
|
||||
.join('');
|
||||
};
|
||||
|
||||
const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
export const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _item?.uid);
|
||||
while (item) {
|
||||
|
||||
Reference in New Issue
Block a user