From c94785f5215f65a36f8f5120d24d018fd30f46aa Mon Sep 17 00:00:00 2001 From: Cmarvin1 <44953569+Cmarvin1@users.noreply.github.com> Date: Wed, 17 Dec 2025 22:32:16 -0500 Subject: [PATCH 1/2] Adding interpolation utilities Refactor interpolation Refactor interpolation updating tests updating tests minor refinements to interpolation logic update snippet generator to handle basic auth credentials move interpolation upstream --- .../GenerateCodeItem/utils/interpolation.js | 87 +++++--------- .../utils/interpolation.spec.js | 107 +++++++++++++----- .../utils/snippet-generator.js | 22 ++-- .../utils/snippet-generator.spec.js | 86 ++++++++++---- 4 files changed, 181 insertions(+), 121 deletions(-) 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..b4697ddc1 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,71 +1,38 @@ import { interpolate } from '@usebruno/common'; -import { cloneDeep } from 'lodash'; +import { isPlainObject, mapValues } from 'lodash-es'; -export const interpolateHeaders = (headers = [], variables = {}) => { - return headers.map((header) => ({ - ...header, - name: interpolate(header.name, variables), - value: interpolate(header.value, variables) - })); -}; +/** + * Traverses an object and interpolates any strings it finds. + */ +export const interpolateObject = (obj, variables) => { + const seen = new WeakSet(); -export const interpolateBody = (body, variables = {}) => { - if (!body) return null; + const walk = (value) => { + if (value == null) return value; - const interpolatedBody = cloneDeep(body); + if (typeof value === 'string') { + return interpolate(value, variables); + } - 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); + if (typeof value === 'object') { + if (seen.has(value)) { + throw new Error( + 'Circular reference detected during interpolation.' + ); } - parsed = interpolate(parsed, variables, { escapeJSONStrings: true }); - try { - const jsonObj = JSON.parse(parsed); - interpolatedBody.json = JSON.stringify(jsonObj, null, 2); - } catch { - interpolatedBody.json = parsed; - } - break; + seen.add(value); + } - case 'text': - interpolatedBody.text = interpolate(body.text, variables); - break; + if (Array.isArray(value)) { + return value.map(walk); + } - case 'xml': - interpolatedBody.xml = interpolate(body.xml, variables); - break; + if (isPlainObject(value)) { + return mapValues(value, walk); + } - case 'sparql': - interpolatedBody.sparql = interpolate(body.sparql, variables); - break; + return value; + }; - 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; + return walk(obj); }; 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..1810f4016 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,35 +1,84 @@ -import { interpolateHeaders, interpolateBody } from './interpolation'; +import { interpolateObject } from './interpolation'; describe('interpolation utils', () => { - describe('interpolateHeaders', () => { - it('should interpolate variables in header name and value while preserving other props', () => { - const headers = [ - { uid: '1', name: 'X-{{var}}', value: 'value-{{var}}', enabled: true } - ]; - const variables = { var: 'test' }; - - const result = interpolateHeaders(headers, variables); - expect(result).toEqual([ - { - uid: '1', - name: 'X-test', - value: 'value-test', - enabled: true + 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('interpolateBody', () => { - it('should interpolate JSON body strings and keep formatting', () => { - const body = { - mode: 'json', - json: '{"name": "{{username}}"}' }; - const variables = { username: 'bruno' }; - const result = interpolateBody(body, variables); - expect(result.json).toBe('{\n "name": "bruno"\n}'); + const variables = { + host: 'api.example.com', + headerName: 'App-ID', + headerValue: 'val-123', + user: 'admin', + passVar: 'secure', + id: '99' + }; + + const result = interpolateObject(complexRequest, 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 + } + }); + }); + + 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 leave the placeholder intact if the variable is missing', () => { + const variables = { known: 'value' }; + const obj = { + field: '{{known}} and {{missing}}' + }; + + const result = interpolateObject(obj, variables); + + expect(result).toEqual({ + field: 'value and {{missing}}' + }); }); it('should interpolate text body', () => { @@ -37,12 +86,12 @@ describe('interpolation utils', () => { mode: 'text', text: 'Hello {{name}}' }; - const result = interpolateBody(body, { name: 'World' }); + const result = interpolateObject(body, { name: 'World' }); expect(result.text).toBe('Hello World'); }); it('should return null when body is null', () => { - expect(interpolateBody(null, { a: 1 })).toBeNull(); + expect(interpolateObject(null, { a: 1 })).toBeNull(); }); }); }); 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..746d2b74f 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,9 @@ 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 { getAllVariables, getTreePathFromCollectionToItem, mergeHeaders } from 'utils/collections'; +import { interpolateObject } from './interpolation'; import { get } from 'lodash'; +import interpolateVars from 'bruno/src/ipc/network/interpolate-vars'; const generateSnippet = ({ language, item, collection, shouldInterpolate = false }) => { try { @@ -11,7 +12,11 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false const variables = getAllVariables(collection, item); - const request = item.request; + let request = item.request; + + if (shouldInterpolate) { + request = interpolateObject(request, variables); + } // Get the request tree path and merge headers const requestTreePath = getTreePathFromCollectionToItem(collection, item); @@ -24,14 +29,6 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false headers = [...headers, ...authHeaders]; } - // Interpolate headers and body if needed - if (shouldInterpolate) { - headers = interpolateHeaders(headers, variables); - if (request.body) { - request.body = interpolateBody(request.body, variables); - } - } - // Build HAR request const harRequest = buildHarRequest({ request, @@ -40,9 +37,8 @@ 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 result; + return snippet.convert(language.target, language.client); } 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 0946ce2bc..1133b5a77 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(() => []) }; @@ -146,11 +150,8 @@ describe('Snippet Generator - Simple Tests', () => { shouldInterpolate: true }); - 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}'`); + 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}'`); }); it('should handle GET requests', () => { @@ -203,11 +204,8 @@ 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/{{endpoint}} -H "Content-Type: application/json" -d '${expectedBody}'`); + 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}'`); }); it('should handle complex nested JSON body', () => { @@ -269,7 +267,7 @@ describe('Snippet Generator - Simple Tests', () => { } }, null, 2); - expect(result).toBe(`curl -X POST https://api.example.com/{{endpoint}} -H "Content-Type: application/json" -d '${expectedComplexBody}'`); + expect(result).toBe(`curl -X POST https://api.example.com/data -H "Content-Type: application/json" -d '${expectedComplexBody}'`); }); it('should handle errors gracefully', () => { @@ -369,13 +367,9 @@ 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/{{endpoint}} -H "Content-Type: application/json" -d '${expectedInterpolatedBody}'`); + expect(result).toBe(`curl -X POST https://api.test.com/users -H "Content-Type: application/json" -d '${expectedInterpolatedBody}'`); }); it('should NOT interpolate when shouldInterpolate is false', () => { @@ -424,7 +418,61 @@ 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', () => { + const item = { + request: { + method: 'GET', + url: 'https://api.example.com', + auth: { + mode: 'basic', + basic: { + username: '{{user}}', + password: '{{pass}}' + } + } + } + }; + + const collection = { + root: { + request: { + vars: { + req: [ + { name: 'user', value: 'admin', enabled: true }, + { name: 'pass', value: 'secret123', enabled: true } + ] + } + } + } + }; + + 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' + }) + ); }); }); From acf576872c99540c4f3d1a5a4d1f75993485437c Mon Sep 17 00:00:00 2001 From: sanjai Date: Wed, 14 Jan 2026 16:34:45 +0530 Subject: [PATCH 2/2] enhance: snippet generator to support header interpolation refactor: improve snippet generation and update test cases updated to minimise changes fix: remove exclusive test flag refactor: enhance interpolation utilities refactor: expand interpolation utilities for auth, headers, body, and params refactor: simplify request handling in snippet generation by removing lodash dependency and clarifying auth header processing fix: tests refactor: integrate interpolateObject utility for enhanced interpolation across auth, headers, body, and params refactor: streamline body interpolation by removing lodash dependency and returning updated body structure refactor: enhance body interpolation logic and streamline auth header processing in snippet generation refactor: simplify getAuthHeaders function by removing unnecessary parameters for improved clarity refactor: replace interpolateObject with interpolate for body --- .../GenerateCodeItem/utils/interpolation.js | 122 ++++++++---- .../utils/interpolation.spec.js | 186 +++++++++++------- .../utils/snippet-generator.js | 32 +-- .../utils/snippet-generator.spec.js | 41 ++-- .../bruno-app/src/utils/codegenerator/auth.js | 23 +-- 5 files changed, 250 insertions(+), 154 deletions(-) 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');