mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-08 06:28:33 +00:00
Merge pull request #6814 from sanjaikumar-bruno/fix/basic-auth-codegen
fix: Code Generation for Basic Auth
This commit is contained in:
@@ -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 = {}) => {
|
||||
|
||||
@@ -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: '<user>{{name}}</user>' };
|
||||
const result = interpolateBody(body, { name: 'Alice' });
|
||||
expect(result.xml).toBe('<user>Alice</user>');
|
||||
});
|
||||
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user