diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/auth-utils.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/auth-utils.js index 6236ae72b..f9885f0df 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/auth-utils.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/auth-utils.js @@ -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' }); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/auth-utils.spec.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/auth-utils.spec.js index 407f2af87..ad5afc3e6 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/auth-utils.spec.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/auth-utils.spec.js @@ -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 { diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/interpolation.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/interpolation.js index b9aa5ba2e..22a52f84f 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/interpolation.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/interpolation.js @@ -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: diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js index ccab4c022..60f181ed1 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js @@ -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 }; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.spec.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.spec.js index b765f3026..941ea7a76 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.spec.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.spec.js @@ -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/); + }); }); \ No newline at end of file diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js index 0898ada87..5ee08cac6 100644 --- a/packages/bruno-app/src/utils/codegenerator/har.js +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -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, diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 3f391c6e3..9085a5b53 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -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) {