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 ea4f35b69..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,12 +1,29 @@
-import { interpolate } from '@usebruno/common';
+import { interpolate, interpolateObject } from '@usebruno/common';
import { cloneDeep } from 'lodash';
+export const interpolateAuth = (auth, variables = {}) => {
+ if (!auth) return auth;
+ return interpolateObject(auth, variables);
+};
+
export const interpolateHeaders = (headers = [], variables = {}) => {
- return headers.map((header) => ({
- ...header,
- name: interpolate(header.name, variables),
- value: interpolate(header.value, 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 = {}) => {
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 b174bf380..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,48 +1,139 @@
-import { interpolateHeaders, interpolateBody } from './interpolation';
+import { interpolateAuth, interpolateHeaders, interpolateBody, interpolateParams } from './interpolation';
describe('interpolation utils', () => {
+ describe('interpolateAuth', () => {
+ it('should interpolate auth object', () => {
+ const auth = {
+ mode: 'basic',
+ basic: {
+ username: '{{user}}',
+ password: '{{pass}}'
+ }
+ };
+ const variables = { user: 'admin', pass: 'secret' };
+
+ const result = interpolateAuth(auth, variables);
+
+ expect(result).toEqual({
+ mode: 'basic',
+ basic: {
+ username: 'admin',
+ password: 'secret'
+ }
+ });
+ });
+
+ it('should return null for null auth', () => {
+ expect(interpolateAuth(null, {})).toBeNull();
+ });
+
+ it('should return undefined for undefined auth', () => {
+ expect(interpolateAuth(undefined, {})).toBeUndefined();
+ });
+ });
+
describe('interpolateHeaders', () => {
- it('should interpolate variables in header name and value while preserving other props', () => {
+ it('should interpolate header names and values', () => {
const headers = [
- { uid: '1', name: 'X-{{var}}', value: 'value-{{var}}', enabled: true }
+ { name: 'X-{{headerName}}', value: '{{headerValue}}', enabled: true },
+ { name: 'Content-Type', value: 'application/json', enabled: true }
];
- const variables = { var: 'test' };
+ const variables = { headerName: 'Custom', headerValue: 'test-value' };
const result = interpolateHeaders(headers, variables);
+
expect(result).toEqual([
- {
- uid: '1',
- name: 'X-test',
- value: 'value-test',
- enabled: true
- }
+ { 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 interpolate JSON body strings and keep formatting', () => {
+ it('should return null for null body', () => {
+ expect(interpolateBody(null, {})).toBeNull();
+ });
+
+ it('should interpolate JSON body with escaping', () => {
const body = {
mode: 'json',
- json: '{"name": "{{username}}"}'
+ json: '{"name": "{{name}}", "count": {{count}}}'
};
- const variables = { username: 'bruno' };
+ const variables = { name: 'Test', count: 42 };
const result = interpolateBody(body, variables);
- expect(result.json).toBe('{\n "name": "bruno"\n}');
+
+ 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 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(interpolateBody(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 f52faf118..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,8 +1,7 @@
import { buildHarRequest } from 'utils/codegenerator/har';
import { getAuthHeaders } from 'utils/codegenerator/auth';
import { getAllVariables, getTreePathFromCollectionToItem, mergeHeaders } from 'utils/collections/index';
-import { interpolateHeaders, interpolateBody } from './interpolation';
-import { get } from 'lodash';
+import { interpolateAuth, interpolateHeaders, interpolateBody, interpolateParams } from './interpolation';
const generateSnippet = ({ language, item, collection, shouldInterpolate = false }) => {
try {
@@ -10,26 +9,27 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
const { HTTPSnippet } = require('httpsnippet');
const variables = getAllVariables(collection, item);
-
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 and body if needed
+ // Interpolate headers, body and params if needed
if (shouldInterpolate) {
headers = interpolateHeaders(headers, variables);
- if (request.body) {
- request.body = interpolateBody(request.body, variables);
- }
+ request.body = interpolateBody(request.body, variables);
+ request.params = interpolateParams(request.params, variables);
}
// Build HAR request
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 0946ce2bc..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
@@ -1,3 +1,5 @@
+import { getAuthHeaders } from 'utils/codegenerator/auth';
+
jest.mock('httpsnippet', () => {
return {
HTTPSnippet: jest.fn().mockImplementation((harRequest) => ({
@@ -56,7 +58,9 @@ jest.mock('utils/collections/index', () => {
...collection?.processEnvVariables,
baseUrl: 'https://api.example.com',
apiKey: 'secret-key-123',
- userId: '12345'
+ userId: '12345',
+ user: 'admin',
+ pass: 'secret123'
})),
getTreePathFromCollectionToItem: jest.fn(() => [])
};
@@ -426,6 +430,55 @@ 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}}}\'');
});
+
+ 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',
+ url: 'https://api.example.com',
+ auth: {
+ mode: 'basic',
+ basic: {
+ username: '{{user}}',
+ password: '{{pass}}'
+ }
+ }
+ }
+ };
+
+ const collection = {
+ root: {
+ request: {
+ auth: { mode: 'none' }
+ }
+ }
+ };
+
+ const { HTTPSnippet: mockedHTTPSnippet } = require('httpsnippet');
+ const { getAuthHeaders: actualGetAuthHeaders } = jest.requireActual('utils/codegenerator/auth');
+ getAuthHeaders.mockImplementation(actualGetAuthHeaders);
+
+ const language = { target: 'shell', client: 'curl' };
+
+ generateSnippet({
+ language,
+ item,
+ collection,
+ shouldInterpolate: true
+ });
+
+ const harRequest = mockedHTTPSnippet.mock.calls[0][0];
+
+ // "admin:secret123" encoded is "YWRtaW46c2VjcmV0MTIz"
+ expect(harRequest.headers).toContainEqual(
+ expect.objectContaining({
+ name: 'Authorization',
+ value: 'Basic YWRtaW46c2VjcmV0MTIz'
+ })
+ );
+ });
});
// Snippet should include inherited headers
@@ -563,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');