mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-27 22:54:07 +00:00
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
This commit is contained in:
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user