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 b4697ddc1..cc79bf3cb 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
@@ -1,38 +1,88 @@
-import { interpolate } from '@usebruno/common';
-import { isPlainObject, mapValues } from 'lodash-es';
+import { interpolate, interpolateObject } from '@usebruno/common';
+import { cloneDeep } from 'lodash';
-/**
- * Traverses an object and interpolates any strings it finds.
- */
-export const interpolateObject = (obj, variables) => {
- const seen = new WeakSet();
-
- const walk = (value) => {
- if (value == null) return value;
-
- if (typeof value === 'string') {
- return interpolate(value, variables);
- }
-
- if (typeof value === 'object') {
- if (seen.has(value)) {
- throw new Error(
- 'Circular reference detected during interpolation.'
- );
- }
- seen.add(value);
- }
-
- if (Array.isArray(value)) {
- return value.map(walk);
- }
-
- if (isPlainObject(value)) {
- return mapValues(value, walk);
- }
-
- return value;
- };
-
- return walk(obj);
+export const interpolateAuth = (auth, variables = {}) => {
+ if (!auth) return auth;
+ return interpolateObject(auth, variables);
+};
+
+export const interpolateHeaders = (headers = [], variables = {}) => {
+ if (!headers) return [];
+ return headers.map((header) => {
+ if (header.enabled) {
+ return interpolateObject(header, variables);
+ }
+ return header;
+ });
+};
+
+export const interpolateParams = (params = [], variables = {}) => {
+ if (!params) return [];
+ return params.map((param) => {
+ if (param.enabled) {
+ return interpolateObject(param, variables);
+ }
+ return param;
+ });
+};
+
+export const interpolateBody = (body, variables = {}) => {
+ if (!body) return null;
+
+ const interpolatedBody = cloneDeep(body);
+
+ switch (body.mode) {
+ case 'json':
+ let parsed = body.json;
+ // If it's already a string, use it directly; if it's an object, stringify it first
+ if (typeof parsed === 'object') {
+ parsed = JSON.stringify(parsed);
+ }
+ parsed = interpolate(parsed, variables, { escapeJSONStrings: true });
+ try {
+ const jsonObj = JSON.parse(parsed);
+ interpolatedBody.json = JSON.stringify(jsonObj, null, 2);
+ } catch {
+ interpolatedBody.json = parsed;
+ }
+ break;
+
+ case 'text':
+ interpolatedBody.text = interpolate(body.text, variables);
+ break;
+
+ case 'xml':
+ interpolatedBody.xml = interpolate(body.xml, variables);
+ break;
+
+ case 'sparql':
+ interpolatedBody.sparql = interpolate(body.sparql, variables);
+ break;
+
+ case 'formUrlEncoded':
+ 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 = Array.isArray(body.multipartForm)
+ ? body.multipartForm.map((param) => ({
+ ...param,
+ value:
+ param.type === 'text' && param.enabled
+ ? interpolate(param.value, variables)
+ : param.value
+ }))
+ : [];
+ break;
+
+ default:
+ break;
+ }
+
+ return interpolatedBody;
};
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/interpolation.spec.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/interpolation.spec.js
index 1810f4016..212e6b68f 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/interpolation.spec.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/interpolation.spec.js
@@ -1,97 +1,139 @@
-import { interpolateObject } from './interpolation';
+import { interpolateAuth, interpolateHeaders, interpolateBody, interpolateParams } from './interpolation';
describe('interpolation utils', () => {
- describe('interpolateObject', () => {
- it('should interpolate variables across all data types and nesting levels', () => {
- const complexRequest = {
- url: 'https://{{host}}/api',
- method: 'POST',
- headers: [{ name: 'X-{{headerName}}', value: '{{headerValue}}', enabled: true }],
- auth: {
- basic: {
- username: '{{user}}',
- password: 'pass-{{passVar}}'
- }
- },
- body: {
- mode: 'json',
- json: '{"id": "{{id}}"}'
- },
- params: {
- someArray: ['tag-{{id}}', 'stable'],
- value: 100,
- enabled: true,
- isNull: null
+ describe('interpolateAuth', () => {
+ it('should interpolate auth object', () => {
+ const auth = {
+ mode: 'basic',
+ basic: {
+ username: '{{user}}',
+ password: '{{pass}}'
}
};
+ const variables = { user: 'admin', pass: 'secret' };
- const variables = {
- host: 'api.example.com',
- headerName: 'App-ID',
- headerValue: 'val-123',
- user: 'admin',
- passVar: 'secure',
- id: '99'
- };
-
- const result = interpolateObject(complexRequest, variables);
+ const result = interpolateAuth(auth, variables);
expect(result).toEqual({
- url: 'https://api.example.com/api',
- method: 'POST',
- headers: [{ name: 'X-App-ID', value: 'val-123', enabled: true }],
- auth: {
- basic: {
- username: 'admin',
- password: 'pass-secure'
- }
- },
- body: {
- mode: 'json',
- json: '{"id": "99"}'
- },
- params: {
- someArray: ['tag-99', 'stable'],
- value: 100,
- enabled: true,
- isNull: null
+ mode: 'basic',
+ basic: {
+ username: 'admin',
+ password: 'secret'
}
});
});
- it('should not iterate endlessly for circular references', () => {
- const variables = { x: 'ok' };
-
- const obj = { value: '{{x}}' };
- obj.self = obj;
-
- expect(() => interpolateObject(obj, variables)).toThrow('Circular reference detected during interpolation.');
+ it('should return null for null auth', () => {
+ expect(interpolateAuth(null, {})).toBeNull();
});
- it('should leave the placeholder intact if the variable is missing', () => {
- const variables = { known: 'value' };
- const obj = {
- field: '{{known}} and {{missing}}'
+ it('should return undefined for undefined auth', () => {
+ expect(interpolateAuth(undefined, {})).toBeUndefined();
+ });
+ });
+
+ describe('interpolateHeaders', () => {
+ it('should interpolate header names and values', () => {
+ const headers = [
+ { name: 'X-{{headerName}}', value: '{{headerValue}}', enabled: true },
+ { name: 'Content-Type', value: 'application/json', enabled: true }
+ ];
+ const variables = { headerName: 'Custom', headerValue: 'test-value' };
+
+ const result = interpolateHeaders(headers, variables);
+
+ expect(result).toEqual([
+ { name: 'X-Custom', value: 'test-value', enabled: true },
+ { name: 'Content-Type', value: 'application/json', enabled: true }
+ ]);
+ });
+
+ it('should return empty array for empty headers', () => {
+ expect(interpolateHeaders([], {})).toEqual([]);
+ });
+ });
+
+ describe('interpolateBody', () => {
+ it('should return null for null body', () => {
+ expect(interpolateBody(null, {})).toBeNull();
+ });
+
+ it('should interpolate JSON body with escaping', () => {
+ const body = {
+ mode: 'json',
+ json: '{"name": "{{name}}", "count": {{count}}}'
};
+ const variables = { name: 'Test', count: 42 };
- const result = interpolateObject(obj, variables);
+ const result = interpolateBody(body, variables);
- expect(result).toEqual({
- field: 'value and {{missing}}'
- });
+ expect(result.mode).toBe('json');
+ expect(JSON.parse(result.json)).toEqual({ name: 'Test', count: 42 });
});
it('should interpolate text body', () => {
- const body = {
- mode: 'text',
- text: 'Hello {{name}}'
- };
- const result = interpolateObject(body, { name: 'World' });
+ const body = { mode: 'text', text: 'Hello {{name}}' };
+ const result = interpolateBody(body, { name: 'World' });
expect(result.text).toBe('Hello World');
});
- it('should return null when body is null', () => {
- expect(interpolateObject(null, { a: 1 })).toBeNull();
+ it('should interpolate xml body', () => {
+ const body = { mode: 'xml', xml: '{{name}}' };
+ const result = interpolateBody(body, { name: 'Alice' });
+ expect(result.xml).toBe('Alice');
+ });
+
+ it('should interpolate formUrlEncoded body for enabled params only', () => {
+ const body = {
+ mode: 'formUrlEncoded',
+ formUrlEncoded: [
+ { name: 'key1', value: '{{val1}}', enabled: true },
+ { name: 'key2', value: '{{val2}}', enabled: false }
+ ]
+ };
+ const variables = { val1: 'value1', val2: 'value2' };
+
+ const result = interpolateBody(body, variables);
+
+ expect(result.formUrlEncoded[0].value).toBe('value1');
+ expect(result.formUrlEncoded[1].value).toBe('{{val2}}');
+ });
+
+ it('should interpolate multipartForm body for enabled text params only', () => {
+ const body = {
+ mode: 'multipartForm',
+ multipartForm: [
+ { name: 'field1', value: '{{val}}', type: 'text', enabled: true },
+ { name: 'field2', value: '{{val}}', type: 'file', enabled: true }
+ ]
+ };
+ const variables = { val: 'interpolated' };
+
+ const result = interpolateBody(body, variables);
+
+ expect(result.multipartForm[0].value).toBe('interpolated');
+ expect(result.multipartForm[1].value).toBe('{{val}}');
+ });
+ });
+
+ describe('interpolateParams', () => {
+ it('should interpolate param names and values', () => {
+ const params = [
+ { name: '{{paramName}}', value: '{{paramValue}}', enabled: true },
+ { name: 'static', value: '{{val}}', enabled: false }
+ ];
+ const variables = { paramName: 'key', paramValue: 'value', val: 'skipped' };
+
+ const result = interpolateParams(params, variables);
+
+ expect(result[0].name).toBe('key');
+ expect(result[0].value).toBe('value');
+ expect(result[1].name).toBe('static');
+ expect(result[1].value).toBe('{{val}}');
+ });
+
+ it('should return empty array for empty params', () => {
+ expect(interpolateParams([], {})).toEqual([]);
});
});
});
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 746d2b74f..b7a573baf 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,9 +1,7 @@
import { buildHarRequest } from 'utils/codegenerator/har';
import { getAuthHeaders } from 'utils/codegenerator/auth';
-import { getAllVariables, getTreePathFromCollectionToItem, mergeHeaders } from 'utils/collections';
-import { interpolateObject } from './interpolation';
-import { get } from 'lodash';
-import interpolateVars from 'bruno/src/ipc/network/interpolate-vars';
+import { getAllVariables, getTreePathFromCollectionToItem, mergeHeaders } from 'utils/collections/index';
+import { interpolateAuth, interpolateHeaders, interpolateBody, interpolateParams } from './interpolation';
const generateSnippet = ({ language, item, collection, shouldInterpolate = false }) => {
try {
@@ -11,24 +9,29 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
const { HTTPSnippet } = require('httpsnippet');
const variables = getAllVariables(collection, item);
-
- let request = item.request;
-
- if (shouldInterpolate) {
- request = interpolateObject(request, variables);
- }
+ const request = item.request;
// Get the request tree path and merge headers
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
let headers = mergeHeaders(collection, request, requestTreePath);
- // Add auth headers if needed
+ // Add auth headers if needed (auth inheritance is resolved upstream)
if (request.auth && request.auth.mode !== 'none') {
- const collectionAuth = collection?.draft?.root ? get(collection, 'draft.root.request.auth', null) : get(collection, 'root.request.auth', null);
- const authHeaders = getAuthHeaders(collectionAuth, request.auth, collection, item);
+ if (shouldInterpolate) {
+ request.auth = interpolateAuth(request.auth, variables);
+ }
+
+ const authHeaders = getAuthHeaders(request.auth, collection, item);
headers = [...headers, ...authHeaders];
}
+ // Interpolate headers, body and params if needed
+ if (shouldInterpolate) {
+ headers = interpolateHeaders(headers, variables);
+ request.body = interpolateBody(request.body, variables);
+ request.params = interpolateParams(request.params, variables);
+ }
+
// Build HAR request
const harRequest = buildHarRequest({
request,
@@ -37,8 +40,9 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
// Generate snippet using HTTPSnippet
const snippet = new HTTPSnippet(harRequest);
+ const result = snippet.convert(language.target, language.client);
- return snippet.convert(language.target, language.client);
+ return result;
} catch (error) {
console.error('Error generating code snippet:', error);
return 'Error generating code snippet';
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 1133b5a77..3c7e2c5ef 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
@@ -150,8 +150,11 @@ describe('Snippet Generator - Simple Tests', () => {
shouldInterpolate: true
});
- const expectedBody = `{"message": "Hello World", "count": 42}`;
- expect(result).toBe(`curl -X POST https://api.example.com/data -H "Content-Type: application/json" -d '${expectedBody}'`);
+ const expectedBody = `{
+ "message": "Hello World",
+ "count": 42
+}`;
+ expect(result).toBe(`curl -X POST https://api.example.com/{{endpoint}} -H "Content-Type: application/json" -d '${expectedBody}'`);
});
it('should handle GET requests', () => {
@@ -204,8 +207,11 @@ describe('Snippet Generator - Simple Tests', () => {
});
// Body should have interpolated variables with proper formatting
- const expectedBody = `{"message": "Hello World", "count": 42}`;
- expect(result).toBe(`curl -X POST https://api.example.com/data -H "Content-Type: application/json" -d '${expectedBody}'`);
+ const expectedBody = `{
+ "message": "Hello World",
+ "count": 42
+}`;
+ expect(result).toBe(`curl -X POST https://api.example.com/{{endpoint}} -H "Content-Type: application/json" -d '${expectedBody}'`);
});
it('should handle complex nested JSON body', () => {
@@ -267,7 +273,7 @@ describe('Snippet Generator - Simple Tests', () => {
}
}, null, 2);
- expect(result).toBe(`curl -X POST https://api.example.com/data -H "Content-Type: application/json" -d '${expectedComplexBody}'`);
+ expect(result).toBe(`curl -X POST https://api.example.com/{{endpoint}} -H "Content-Type: application/json" -d '${expectedComplexBody}'`);
});
it('should handle errors gracefully', () => {
@@ -367,9 +373,13 @@ describe('Snippet Generator - Simple Tests', () => {
shouldInterpolate: true
});
- const expectedInterpolatedBody = `{"name": "John Smith", "email": "john@test.com", "age": 30}`;
+ const expectedInterpolatedBody = `{
+ "name": "John Smith",
+ "email": "john@test.com",
+ "age": 30
+}`;
- expect(result).toBe(`curl -X POST https://api.test.com/users -H "Content-Type: application/json" -d '${expectedInterpolatedBody}'`);
+ expect(result).toBe(`curl -X POST https://api.test.com/{{endpoint}} -H "Content-Type: application/json" -d '${expectedInterpolatedBody}'`);
});
it('should NOT interpolate when shouldInterpolate is false', () => {
@@ -418,12 +428,12 @@ describe('Snippet Generator - Simple Tests', () => {
shouldInterpolate: false
});
- expect(result).toBe(
- 'curl -X POST https://api.test.com/{{endpoint}} -H "Content-Type: application/json" -d \'{"name": "{{userName}}", "email": "{{userEmail}}", "age": {{userAge}}}\''
- );
+ expect(result).toBe('curl -X POST https://api.test.com/{{endpoint}} -H "Content-Type: application/json" -d \'{"name": "{{userName}}", "email": "{{userEmail}}", "age": {{userAge}}}\'');
});
- it('should interpolate basic auth credentials correctly', () => {
+ it('should interpolate auth credentials correctly', () => {
+ // Auth inheritance is resolved upstream in index.js before calling generateSnippet
+ // So the item already has the resolved auth (not 'inherit' mode)
const item = {
request: {
method: 'GET',
@@ -441,12 +451,7 @@ describe('Snippet Generator - Simple Tests', () => {
const collection = {
root: {
request: {
- vars: {
- req: [
- { name: 'user', value: 'admin', enabled: true },
- { name: 'pass', value: 'secret123', enabled: true }
- ]
- }
+ auth: { mode: 'none' }
}
}
};
@@ -611,7 +616,7 @@ describe('generateSnippet with OAuth2 authentication', () => {
jest.clearAllMocks();
// Mock getAuthHeaders to return OAuth2 headers based on the auth config
const authUtils = require('utils/codegenerator/auth');
- authUtils.getAuthHeaders.mockImplementation((collectionRootAuth, requestAuth, collection = null, item = null) => {
+ authUtils.getAuthHeaders.mockImplementation((requestAuth, collection = null, item = null) => {
if (requestAuth?.mode === 'oauth2') {
const oauth2Config = requestAuth.oauth2 || {};
const tokenPlacement = oauth2Config.tokenPlacement || 'header';
diff --git a/packages/bruno-app/src/utils/codegenerator/auth.js b/packages/bruno-app/src/utils/codegenerator/auth.js
index eefb82cfe..51da6d3c9 100644
--- a/packages/bruno-app/src/utils/codegenerator/auth.js
+++ b/packages/bruno-app/src/utils/codegenerator/auth.js
@@ -3,21 +3,16 @@ import { find } from 'lodash';
import { interpolate } from '@usebruno/common';
import { getAllVariables } from 'utils/collections/index';
-export const getAuthHeaders = (collectionRootAuth, requestAuth, collection = null, item = null) => {
- // Discovered edge case where code generation fails when you create a collection which has not been saved yet:
- // Collection auth therefore null, and request inherits from collection, therefore it is also null
- // TypeError: Cannot read properties of undefined (reading 'mode')
- // at getAuthHeaders
- if (!collectionRootAuth && !requestAuth) {
+export const getAuthHeaders = (requestAuth, collection = null, item = null) => {
+ // Auth inheritance is resolved upstream, so requestAuth should never have mode 'inherit'
+ if (!requestAuth) {
return [];
}
- const auth = collectionRootAuth && ['inherit'].includes(requestAuth?.mode) ? collectionRootAuth : requestAuth;
-
- switch (auth.mode) {
+ switch (requestAuth.mode) {
case 'basic':
- const username = get(auth, 'basic.username', '');
- const password = get(auth, 'basic.password', '');
+ const username = get(requestAuth, 'basic.username', '');
+ const password = get(requestAuth, 'basic.password', '');
const basicToken = Buffer.from(`${username}:${password}`).toString('base64');
return [
@@ -32,11 +27,11 @@ export const getAuthHeaders = (collectionRootAuth, requestAuth, collection = nul
{
enabled: true,
name: 'Authorization',
- value: `Bearer ${get(auth, 'bearer.token', '')}`
+ value: `Bearer ${get(requestAuth, 'bearer.token', '')}`
}
];
case 'apikey':
- const apiKeyAuth = get(auth, 'apikey', {});
+ const apiKeyAuth = get(requestAuth, 'apikey', {});
const key = get(apiKeyAuth, 'key', '');
const value = get(apiKeyAuth, 'value', '');
const placement = get(apiKeyAuth, 'placement', 'header');
@@ -52,7 +47,7 @@ export const getAuthHeaders = (collectionRootAuth, requestAuth, collection = nul
}
return [];
case 'oauth2': {
- const oauth2Config = get(auth, 'oauth2', {});
+ const oauth2Config = get(requestAuth, 'oauth2', {});
const tokenPlacement = get(oauth2Config, 'tokenPlacement', 'header');
const tokenHeaderPrefix = get(oauth2Config, 'tokenHeaderPrefix', 'Bearer');