mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
feat: enhance support for prompt variables
This commit is contained in:
@@ -96,9 +96,6 @@ const StyledWrapper = styled.div`
|
||||
.cm-variable-invalid {
|
||||
color: red;
|
||||
}
|
||||
.cm-variable-prompt {
|
||||
color: dodgerblue;
|
||||
}
|
||||
|
||||
.CodeMirror-search-hint {
|
||||
display: inline;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
padding: 0 0.2rem;
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
|
||||
export default function PromptVariablesModal({ title = 'Input Required', prompts, onSubmit, onCancel }) {
|
||||
const [values, setValues] = useState({});
|
||||
@@ -16,25 +18,38 @@ export default function PromptVariablesModal({ title = 'Input Required', prompts
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
size="md"
|
||||
size="lg"
|
||||
title={title}
|
||||
confirmText="Continue"
|
||||
cancelText="Cancel"
|
||||
handleConfirm={() => onSubmit(values)}
|
||||
handleCancel={onCancel}
|
||||
>
|
||||
{prompts.map((prompt) => (
|
||||
<div key={prompt} style={{ marginBottom: 16 }}>
|
||||
<label style={{ display: 'block', fontWeight: 500, marginBottom: 8 }}>{prompt}:</label>
|
||||
<input
|
||||
type="text"
|
||||
style={{ width: '100%', color: '#333', padding: '8px', borderRadius: '4px', border: '1px solid #ccc' }}
|
||||
value={values[prompt] || ''}
|
||||
onChange={(e) => handleChange(prompt, e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<StyledWrapper data-testid="prompt-variables-modal-content">
|
||||
<div className="space-y-5 mt-2">
|
||||
{prompts.map((prompt, index) => (
|
||||
<div key={prompt} data-testid="prompt-variable-input-container">
|
||||
<label htmlFor={`prompt-${index}`} className="block font-semibold">
|
||||
{prompt}
|
||||
</label>
|
||||
<input
|
||||
id={`prompt-${index}`}
|
||||
type="text"
|
||||
data-testid={`prompt-variable-input-${index}`}
|
||||
className="textbox mt-2 w-full"
|
||||
placeholder="Enter value"
|
||||
value={values[prompt] || ''}
|
||||
onChange={(e) => handleChange(prompt, e.target.value)}
|
||||
autoFocus={index === 0}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
|
||||
@@ -50,9 +50,6 @@ const StyledWrapper = styled.div`
|
||||
.cm-variable-invalid {
|
||||
color: red;
|
||||
}
|
||||
.cm-variable-prompt {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.CodeMirror-search-hint {
|
||||
display: inline;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { interpolateUrl, interpolateUrlPathParams } from 'utils/url/index';
|
||||
import { getLanguages } from 'utils/codegenerator/targets';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getAllVariables, getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
import { resolveInheritedAuth } from './utils/auth-utils';
|
||||
import { resolveInheritedAuth } from 'utils/auth';
|
||||
|
||||
const TEMPLATE_VAR_PATTERN = /\{\{([^}]+)\}\}/;
|
||||
|
||||
|
||||
@@ -1,47 +1,9 @@
|
||||
import { buildHarRequest } from 'utils/codegenerator/har';
|
||||
import { getAuthHeaders } from 'utils/codegenerator/auth';
|
||||
import { getAllVariables, getTreePathFromCollectionToItem } from 'utils/collections/index';
|
||||
import { getAllVariables, getTreePathFromCollectionToItem, mergeHeaders } from 'utils/collections/index';
|
||||
import { interpolateHeaders, interpolateBody } from './interpolation';
|
||||
import { get } from 'lodash';
|
||||
|
||||
// Merge headers from collection, folders, and request
|
||||
const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
let headers = new Map();
|
||||
|
||||
// Add collection headers first
|
||||
const collectionHeaders = collection?.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
|
||||
collectionHeaders.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header);
|
||||
}
|
||||
});
|
||||
|
||||
// Add folder headers next, traversing from root to leaf
|
||||
if (requestTreePath && requestTreePath.length > 0) {
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
const folderHeaders = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'root.request.headers', []);
|
||||
folderHeaders.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add request headers last (they take precedence)
|
||||
const requestHeaders = request.headers || [];
|
||||
requestHeaders.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert Map back to array
|
||||
return Array.from(headers.values());
|
||||
};
|
||||
|
||||
const generateSnippet = ({ language, item, collection, shouldInterpolate = false }) => {
|
||||
try {
|
||||
// Get HTTPSnippet dynamically so mocks can be applied in tests
|
||||
@@ -88,6 +50,5 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
|
||||
};
|
||||
|
||||
export {
|
||||
generateSnippet,
|
||||
mergeHeaders
|
||||
generateSnippet
|
||||
};
|
||||
@@ -45,19 +45,24 @@ jest.mock('utils/codegenerator/auth', () => ({
|
||||
getAuthHeaders: jest.fn(() => [])
|
||||
}));
|
||||
|
||||
jest.mock('utils/collections/index', () => ({
|
||||
getAllVariables: jest.fn((collection) => ({
|
||||
...collection?.globalEnvironmentVariables,
|
||||
...collection?.runtimeVariables,
|
||||
...collection?.processEnvVariables,
|
||||
baseUrl: 'https://api.example.com',
|
||||
apiKey: 'secret-key-123',
|
||||
userId: '12345'
|
||||
})),
|
||||
getTreePathFromCollectionToItem: jest.fn(() => [])
|
||||
}));
|
||||
jest.mock('utils/collections/index', () => {
|
||||
const actual = jest.requireActual('utils/collections/index');
|
||||
|
||||
import { generateSnippet, mergeHeaders } from './snippet-generator';
|
||||
return {
|
||||
...actual,
|
||||
getAllVariables: jest.fn((collection) => ({
|
||||
...collection?.globalEnvironmentVariables,
|
||||
...collection?.runtimeVariables,
|
||||
...collection?.processEnvVariables,
|
||||
baseUrl: 'https://api.example.com',
|
||||
apiKey: 'secret-key-123',
|
||||
userId: '12345'
|
||||
})),
|
||||
getTreePathFromCollectionToItem: jest.fn(() => [])
|
||||
};
|
||||
});
|
||||
|
||||
import { generateSnippet } from './snippet-generator';
|
||||
|
||||
describe('Snippet Generator - Simple Tests', () => {
|
||||
|
||||
@@ -424,41 +429,6 @@ describe('Snippet Generator - Simple Tests', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeHeaders', () => {
|
||||
it('should include headers from collection, folder and request (with correct precedence)', () => {
|
||||
const collection = {
|
||||
root: {
|
||||
request: {
|
||||
headers: [
|
||||
{ name: 'X-Collection', value: 'c', enabled: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const folder = {
|
||||
type: 'folder',
|
||||
root: {
|
||||
request: {
|
||||
headers: [
|
||||
{ name: 'X-Folder', value: 'f', enabled: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const request = {
|
||||
headers: [
|
||||
{ name: 'X-Request', value: 'r', enabled: true }
|
||||
]
|
||||
};
|
||||
|
||||
const headers = mergeHeaders(collection, request, [folder]);
|
||||
const names = headers.map((h) => h.name);
|
||||
expect(names).toEqual(expect.arrayContaining(['X-Collection', 'X-Folder', 'X-Request']));
|
||||
});
|
||||
});
|
||||
|
||||
// Snippet should include inherited headers
|
||||
describe('generateSnippet – header inclusion in output', () => {
|
||||
it('should include collection and folder headers in generated snippet', () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import PromptVariablesModal from 'components/RequestPane/PromptVariables/PromptVariablesModal';
|
||||
import React, { createContext, useCallback, useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
const PromptVariablesContext = createContext();
|
||||
|
||||
@@ -9,13 +8,7 @@ export function PromptVariablesProvider({ children }) {
|
||||
|
||||
const prompt = useCallback((prompts) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
setModalState({ open: true, prompts, resolve, reject });
|
||||
} catch (err) {
|
||||
console.error('PromptVariablesProvider: Error opening prompt modal:', err);
|
||||
toast.error('Prompt variable(s) detected, but prompt modal is not available. Please ensure PromptVariableProvider is mounted.');
|
||||
reject(err);
|
||||
}
|
||||
setModalState({ open: true, prompts, resolve, reject });
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -32,41 +25,28 @@ export function PromptVariablesProvider({ children }) {
|
||||
}
|
||||
|
||||
const handleSubmit = (values) => {
|
||||
try {
|
||||
modalState.resolve(values);
|
||||
} catch (err) {
|
||||
console.error('PromptVariablesProvider: Error resolving prompt values:', err);
|
||||
}
|
||||
modalState.resolve(values);
|
||||
setModalState({ open: false, prompts: [], resolve: null, reject: null });
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
try {
|
||||
modalState.reject('cancelled');
|
||||
} catch (err) {
|
||||
console.error('PromptVariablesProvider: Error rejecting prompt:', err);
|
||||
}
|
||||
modalState.reject('cancelled');
|
||||
setModalState({ open: false, prompts: [], resolve: null, reject: null });
|
||||
};
|
||||
|
||||
try {
|
||||
return (
|
||||
<PromptVariablesContext.Provider value={{ prompt }}>
|
||||
{children}
|
||||
{modalState.open && (
|
||||
<PromptVariablesModal
|
||||
title="Input Required"
|
||||
prompts={modalState.prompts}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
</PromptVariablesContext.Provider>
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('PromptVariablesProvider: Error rendering provider or modal:', err);
|
||||
return children;
|
||||
}
|
||||
return (
|
||||
<PromptVariablesContext.Provider value={{ prompt }}>
|
||||
{children}
|
||||
{modalState.open && (
|
||||
<PromptVariablesModal
|
||||
title="Input Required"
|
||||
prompts={modalState.prompts}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
</PromptVariablesContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default PromptVariablesProvider;
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
isItemAFolder,
|
||||
refreshUidsInItem,
|
||||
isItemARequest,
|
||||
getAllVariables,
|
||||
transformRequestToSaveToFilesystem,
|
||||
transformCollectionRootToSave
|
||||
} from 'utils/collections';
|
||||
@@ -52,7 +53,7 @@ import {
|
||||
import { each } from 'lodash';
|
||||
import { closeAllCollectionTabs, updateResponsePaneScrollPosition } from 'providers/ReduxStore/slices/tabs';
|
||||
import { resolveRequestFilename } from 'utils/common/platform';
|
||||
import { parsePathParams, splitOnFirst } from 'utils/url/index';
|
||||
import { interpolateUrl, parsePathParams, splitOnFirst } from 'utils/url/index';
|
||||
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
|
||||
import {
|
||||
getGlobalEnvironmentVariables,
|
||||
@@ -62,11 +63,14 @@ import {
|
||||
resetSequencesInFolder,
|
||||
getReorderedItemsInSourceDirectory,
|
||||
calculateDraggedItemNewPathname,
|
||||
transformFolderRootToSave
|
||||
transformFolderRootToSave,
|
||||
getTreePathFromCollectionToItem,
|
||||
mergeHeaders
|
||||
} from 'utils/collections/index';
|
||||
import { sanitizeName } from 'utils/common/regex';
|
||||
import { buildPersistedEnvVariables } from 'utils/environments';
|
||||
import { safeParseJSON, safeStringifyJSON } from 'utils/common/index';
|
||||
import { resolveInheritedAuth } from 'utils/auth';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { updateSettingsSelectedTab } from './index';
|
||||
|
||||
@@ -379,6 +383,76 @@ export const wsConnectOnly = (item, collectionUid) => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract prompt variables from a request, collection, and environment variables.
|
||||
* Tries to respect the hierarchy of the variables and avoid unnecessary prompts as much as possible
|
||||
*
|
||||
* @param {*} item
|
||||
* @param {*} collection
|
||||
* @returns {Promise<Object>} A promise that resolves with the prompt variables or null if no prompt variables are found
|
||||
*/
|
||||
const extractPromptVariablesForRequest = async (item, collection) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// Ensure window contains promptForVariables function
|
||||
if (typeof window === 'undefined' || typeof window.promptForVariables !== 'function') {
|
||||
console.error('Failed to initialize prompt variables: window.promptForVariables is not available. '
|
||||
+ 'This may indicate an initialization issue with the app environment.');
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
const prompts = [];
|
||||
const request = item.draft?.request ?? item.request ?? {};
|
||||
const allVariables = getAllVariables(collection, item);
|
||||
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
// Get active headers from collection, folders, and request by priority order
|
||||
const headers = mergeHeaders(collection, request, requestTreePath);
|
||||
// Get request auth or inherited auth
|
||||
const resolvedAuthRequest = resolveInheritedAuth(item, collection);
|
||||
|
||||
for (let clientCert of clientCertConfig) {
|
||||
const domain = interpolateUrl({ url: clientCert?.domain, variables: allVariables });
|
||||
|
||||
if (domain) {
|
||||
const hostRegex = '^(https:\\/\\/|grpc:\\/\\/|grpcs:\\/\\/)?' + domain.replaceAll('.', '\\.').replaceAll('*', '.*');
|
||||
const requestUrl = interpolateUrl({ url: request.url, variables: allVariables });
|
||||
if (requestUrl.match(hostRegex)) {
|
||||
prompts.push(...extractPromptVariables(clientCert));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to extract unique prompt variables from anywhere in the request and environment variables.
|
||||
prompts.push(...extractPromptVariables(allVariables));
|
||||
prompts.push(...extractPromptVariables(request.body?.[request.body.mode]));
|
||||
prompts.push(...extractPromptVariables(headers));
|
||||
prompts.push(...extractPromptVariables(request.params));
|
||||
prompts.push(...extractPromptVariables(resolvedAuthRequest.auth));
|
||||
|
||||
// Remove duplicates
|
||||
const uniquePrompts = Array.from(new Set(prompts));
|
||||
|
||||
// If no prompt variables are found, return null
|
||||
if (!uniquePrompts?.length) {
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
try {
|
||||
// Prompt user for values if any prompt variables are found
|
||||
const userValues = await window.promptForVariables(uniquePrompts);
|
||||
const promptVariables = {};
|
||||
// Populate runtimeVariables with user input for prompt variables
|
||||
for (const prompt of uniquePrompts) {
|
||||
promptVariables[`?${prompt}`] = userValues[prompt] ?? '';
|
||||
}
|
||||
|
||||
return resolve(promptVariables);
|
||||
} catch (error) {
|
||||
return reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;
|
||||
@@ -394,30 +468,24 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
|
||||
const itemCopy = cloneDeep(item);
|
||||
|
||||
// add selected global env variables to the collection object
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({
|
||||
globalEnvironments,
|
||||
activeGlobalEnvironmentUid
|
||||
});
|
||||
collectionCopy.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
|
||||
const requestUid = uuid();
|
||||
itemCopy.requestUid = requestUid;
|
||||
|
||||
// Ensure window contains promptForVariables function
|
||||
if (typeof window.promptForVariables === 'function') {
|
||||
// Attempt to extract unique prompt variables from anywhere in the requestExpand commentComment on line R260ResolvedCode has comments. Press enter to view.
|
||||
const uniquePrompts = extractPromptVariables(itemCopy.draft?.request ?? itemCopy.request);
|
||||
|
||||
if (uniquePrompts?.length > 0) {
|
||||
try {
|
||||
// Prompt user for values if any prompt variables are found
|
||||
let userValues = await window.promptForVariables(uniquePrompts);
|
||||
|
||||
// Populate runtimeVariables with user input for prompt variables
|
||||
for (const prompt of uniquePrompts) {
|
||||
collectionCopy.runtimeVariables[`?${prompt}`] = userValues[prompt] ?? '';
|
||||
}
|
||||
} catch (error) {
|
||||
if (error === 'cancelled') {
|
||||
return resolve(); // Resolve without error if user cancels prompt
|
||||
}
|
||||
reject(error);
|
||||
}
|
||||
try {
|
||||
const promptVariables = await extractPromptVariablesForRequest(itemCopy, collectionCopy);
|
||||
collectionCopy.promptVariables = promptVariables ?? {};
|
||||
} catch (error) {
|
||||
if (error === 'cancelled') {
|
||||
return resolve(); // Resolve without error if user cancels prompt
|
||||
}
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
await dispatch(
|
||||
@@ -435,13 +503,6 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
|
||||
})
|
||||
);
|
||||
|
||||
// add selected global env variables to the collection object
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({
|
||||
globalEnvironments,
|
||||
activeGlobalEnvironmentUid
|
||||
});
|
||||
collectionCopy.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);
|
||||
const isGrpcRequest = itemCopy.type === 'grpc-request';
|
||||
const isWsRequest = itemCopy.type === 'ws-request';
|
||||
@@ -1379,7 +1440,7 @@ export const loadGrpcMethodsFromReflection = (item, collectionUid, url) => async
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (!collection) {
|
||||
return reject(new Error('Collection not found'));
|
||||
}
|
||||
@@ -1396,6 +1457,18 @@ export const loadGrpcMethodsFromReflection = (item, collectionUid, url) => async
|
||||
const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);
|
||||
const runtimeVariables = collectionCopy.runtimeVariables;
|
||||
|
||||
try {
|
||||
const promptVariables = await extractPromptVariablesForRequest(itemCopy, collectionCopy);
|
||||
if (promptVariables) {
|
||||
collectionCopy.promptVariables = promptVariables;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error === 'cancelled') {
|
||||
return resolve(); // Resolve without error if user cancels prompt
|
||||
}
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
ipcRenderer
|
||||
.invoke('grpc:load-methods-reflection', {
|
||||
|
||||
@@ -287,7 +287,7 @@ const darkTheme = {
|
||||
variable: {
|
||||
valid: 'rgb(11 178 126)',
|
||||
invalid: '#f06f57',
|
||||
prompt: 'dodgerblue',
|
||||
prompt: '#3D8DF5',
|
||||
info: {
|
||||
color: '#ce9178',
|
||||
bg: 'rgb(48,48,49)',
|
||||
|
||||
@@ -288,7 +288,7 @@ const lightTheme = {
|
||||
variable: {
|
||||
valid: '#047857',
|
||||
invalid: 'rgb(185, 28, 28)',
|
||||
prompt: 'dodgerblue',
|
||||
prompt: '#186ADE',
|
||||
info: {
|
||||
color: 'rgb(52, 52, 52)',
|
||||
bg: 'white',
|
||||
|
||||
@@ -40,4 +40,4 @@ export const resolveInheritedAuth = (item, collection) => {
|
||||
...mergedRequest,
|
||||
auth: effectiveAuth
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { resolveInheritedAuth } from './auth-utils';
|
||||
import { resolveInheritedAuth } from './index';
|
||||
|
||||
jest.mock('utils/collections/index', () => ({
|
||||
getTreePathFromCollectionToItem: (collection, item) => {
|
||||
@@ -76,4 +76,4 @@ describe('auth-utils.resolveInheritedAuth', () => {
|
||||
expect(resolved.auth.mode).toBe('basic');
|
||||
expect(resolved.auth.basic.username).toBe('override');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1161,11 +1161,12 @@ export const getAllVariables = (collection, item) => {
|
||||
const pathParams = getPathParams(item);
|
||||
const { globalEnvironmentVariables = {} } = collection;
|
||||
|
||||
const { processEnvVariables = {}, runtimeVariables = {} } = collection;
|
||||
const { processEnvVariables = {}, runtimeVariables = {}, promptVariables = {} } = collection;
|
||||
const mergedVariables = {
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...runtimeVariables
|
||||
...runtimeVariables,
|
||||
...promptVariables
|
||||
};
|
||||
|
||||
const mergedVariablesGlobal = {
|
||||
@@ -1174,6 +1175,7 @@ export const getAllVariables = (collection, item) => {
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...runtimeVariables,
|
||||
...promptVariables
|
||||
}
|
||||
|
||||
const maskedEnvVariables = getEnvironmentVariablesMasked(collection) || [];
|
||||
@@ -1194,6 +1196,7 @@ export const getAllVariables = (collection, item) => {
|
||||
...requestVariables,
|
||||
...oauth2CredentialVariables,
|
||||
...runtimeVariables,
|
||||
...promptVariables,
|
||||
pathParams: {
|
||||
...pathParams
|
||||
},
|
||||
@@ -1206,6 +1209,44 @@ export const getAllVariables = (collection, item) => {
|
||||
};
|
||||
};
|
||||
|
||||
// Merge headers from collection, folders, and request
|
||||
export const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
let headers = new Map();
|
||||
|
||||
// Add collection headers first
|
||||
const collectionHeaders = collection?.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
|
||||
collectionHeaders.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header);
|
||||
}
|
||||
});
|
||||
|
||||
// Add folder headers next, traversing from root to leaf
|
||||
if (requestTreePath && requestTreePath.length > 0) {
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
const folderHeaders = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'root.request.headers', []);
|
||||
folderHeaders.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add request headers last (they take precedence)
|
||||
const requestHeaders = request.headers || [];
|
||||
requestHeaders.forEach((header) => {
|
||||
if (header.enabled) {
|
||||
headers.set(header.name, header);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert Map back to array
|
||||
return Array.from(headers.values());
|
||||
};
|
||||
|
||||
export const maskInputValue = (value) => {
|
||||
if (!value || typeof value !== 'string') {
|
||||
return '';
|
||||
|
||||
37
packages/bruno-app/src/utils/collections/index.spec.js
Normal file
37
packages/bruno-app/src/utils/collections/index.spec.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
import { mergeHeaders } from './index';
|
||||
|
||||
describe('mergeHeaders', () => {
|
||||
it('should include headers from collection, folder and request (with correct precedence)', () => {
|
||||
const collection = {
|
||||
root: {
|
||||
request: {
|
||||
headers: [
|
||||
{ name: 'X-Collection', value: 'c', enabled: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const folder = {
|
||||
type: 'folder',
|
||||
root: {
|
||||
request: {
|
||||
headers: [
|
||||
{ name: 'X-Folder', value: 'f', enabled: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const request = {
|
||||
headers: [
|
||||
{ name: 'X-Request', value: 'r', enabled: true }
|
||||
]
|
||||
};
|
||||
|
||||
const headers = mergeHeaders(collection, request, [folder]);
|
||||
const names = headers.map((h) => h.name);
|
||||
expect(names).toEqual(expect.arrayContaining(['X-Collection', 'X-Folder', 'X-Request']));
|
||||
});
|
||||
});
|
||||
@@ -31,6 +31,59 @@ const onConsoleLog = (type, args) => {
|
||||
console[type](...args);
|
||||
};
|
||||
|
||||
const getCACertHostRegex = (domain) => {
|
||||
return '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*');
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract prompt variables from a request
|
||||
* Tries to respect the hierarchy of the variables and avoid unnecessary prompts as much as possible
|
||||
* Note: TO BE CALLED ONLY AFTER THE PREPARE REQUEST
|
||||
*
|
||||
* @param {*} request - request object built by prepareRequest
|
||||
* @returns {string[]} An array of extracted prompt variables
|
||||
*/
|
||||
const extractPromptVariablesForRequest = ({ request, collection, envVariables, runtimeVariables, processEnvVars, brunoConfig }) => {
|
||||
const { vars, collectionVariables, folderVariables, requestVariables, ...requestObj } = request;
|
||||
|
||||
const allVariables = {
|
||||
...envVariables,
|
||||
...collectionVariables,
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...runtimeVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const prompts = extractPromptVariables(requestObj);
|
||||
prompts.push(...extractPromptVariables(allVariables));
|
||||
|
||||
const interpolationOptions = {
|
||||
envVars: envVariables,
|
||||
runtimeVariables,
|
||||
processEnvVars
|
||||
};
|
||||
|
||||
// client certificate config
|
||||
const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);
|
||||
for (let clientCert of clientCertConfig) {
|
||||
const domain = interpolateString(clientCert?.domain, interpolationOptions);
|
||||
if (domain) {
|
||||
const hostRegex = getCACertHostRegex(domain);
|
||||
if (request.url.match(hostRegex)) {
|
||||
prompts.push(...extractPromptVariables(clientCert));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return unique prompt variables
|
||||
return Array.from(new Set(prompts));
|
||||
};
|
||||
|
||||
const runSingleRequest = async function (
|
||||
item,
|
||||
collectionPath,
|
||||
@@ -75,10 +128,11 @@ const runSingleRequest = async function (
|
||||
request = await prepareRequest(item, collection);
|
||||
|
||||
// Detect prompt variables before proceeding
|
||||
const promptVars = extractPromptVariables(request);
|
||||
const promptVars = extractPromptVariablesForRequest({ request, collection, envVariables, runtimeVariables, processEnvVars, brunoConfig });
|
||||
|
||||
if (promptVars.length > 0) {
|
||||
const errorMsg = 'Prompt variables detected in request. CLI execution is not supported for requests with prompt variables.';
|
||||
console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${errorMsg})`));
|
||||
const errorMsg = `Prompt variables detected in request. CLI execution is not supported for requests with prompt variables. \nPrompts: ${promptVars.join(', ')}`;
|
||||
console.log(chalk.yellow(stripExtension(relativeItemPathname) + ' Skipped:') + chalk.dim(` (${errorMsg})`));
|
||||
return {
|
||||
test: {
|
||||
filename: relativeItemPathname
|
||||
@@ -204,7 +258,7 @@ const runSingleRequest = async function (
|
||||
const domain = interpolateString(clientCert?.domain, interpolationOptions);
|
||||
const type = clientCert?.type || 'cert';
|
||||
if (domain) {
|
||||
const hostRegex = '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*');
|
||||
const hostRegex = getCACertHostRegex(domain);
|
||||
if (request.url.match(hostRegex)) {
|
||||
if (type === 'cert') {
|
||||
try {
|
||||
|
||||
@@ -41,11 +41,13 @@ const getCertsAndProxyConfig = async ({
|
||||
httpsAgentRequestFields['caCertificatesCount'] = caCertificatesCount;
|
||||
httpsAgentRequestFields['ca'] = caCertificates || [];
|
||||
|
||||
const { promptVariables } = collection;
|
||||
const brunoConfig = getBrunoConfig(collectionUid, collection);
|
||||
const interpolationOptions = {
|
||||
globalEnvironmentVariables,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
promptVariables,
|
||||
processEnvVars
|
||||
};
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@ const configureRequest = async (
|
||||
|
||||
request.maxRedirects = 0;
|
||||
|
||||
const { promptVariables = {} } = collection;
|
||||
let { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;
|
||||
let axiosInstance = makeAxiosInstance({
|
||||
proxyMode,
|
||||
@@ -134,7 +135,7 @@ const configureRequest = async (
|
||||
let credentials, credentialsId, oauth2Url, debugInfo;
|
||||
switch (grantType) {
|
||||
case 'authorization_code':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars, promptVariables);
|
||||
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid, certsAndProxyConfig }));
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
if (tokenPlacement == 'header' && credentials?.access_token) {
|
||||
@@ -150,7 +151,7 @@ const configureRequest = async (
|
||||
}
|
||||
break;
|
||||
case 'implicit':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars, promptVariables);
|
||||
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingImplicitGrant({ request: requestCopy, collectionUid }));
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
if (tokenPlacement == 'header') {
|
||||
@@ -166,7 +167,7 @@ const configureRequest = async (
|
||||
}
|
||||
break;
|
||||
case 'client_credentials':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars, promptVariables);
|
||||
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid, certsAndProxyConfig }));
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
if (tokenPlacement == 'header' && credentials?.access_token) {
|
||||
@@ -182,7 +183,7 @@ const configureRequest = async (
|
||||
}
|
||||
break;
|
||||
case 'password':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars, promptVariables);
|
||||
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid, certsAndProxyConfig }));
|
||||
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
if (tokenPlacement == 'header' && credentials?.access_token) {
|
||||
@@ -385,7 +386,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
) => {
|
||||
// run pre-request script
|
||||
let scriptResult;
|
||||
const collectionName = collection?.name
|
||||
const { promptVariables = {}, name: collectionName } = collection;
|
||||
|
||||
const requestScript = get(request, 'script.req');
|
||||
if (requestScript?.length) {
|
||||
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
|
||||
@@ -426,7 +428,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
|
||||
// interpolate variables inside request
|
||||
interpolateVars(request, envVars, runtimeVariables, processEnvVars);
|
||||
interpolateVars(request, envVars, runtimeVariables, processEnvVars, promptVariables);
|
||||
|
||||
if (request.settings?.encodeUrl) {
|
||||
request.url = encodeUrl(request.url);
|
||||
@@ -913,6 +915,50 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract prompt variables from a request
|
||||
* Tries to respect the hierarchy of the variables and avoid unnecessary prompts as much as possible
|
||||
* Note: TO BE CALLED ONLY AFTER THE PREPARE REQUEST
|
||||
*
|
||||
* @param {*} request - request object built by prepareRequest
|
||||
* @returns {string[]} An array of extracted prompt variables
|
||||
*/
|
||||
const extractPromptVariablesForRequest = async ({ request, collection, envVars: collectionEnvironmentVars, runtimeVariables, processEnvVars }) => {
|
||||
const { globalEnvironmentVariables, collectionVariables, folderVariables, requestVariables, ...requestObj } = request;
|
||||
|
||||
const allVariables = {
|
||||
...globalEnvironmentVariables,
|
||||
...collectionEnvironmentVars,
|
||||
...collectionVariables,
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...runtimeVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const { interpolationOptions, ...certsAndProxyConfig } = await getCertsAndProxyConfig({
|
||||
collectionUid: collection.uid,
|
||||
collection,
|
||||
request,
|
||||
envVars: collectionEnvironmentVars,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
collectionPath: collection.pathname,
|
||||
globalEnvironmentVariables
|
||||
});
|
||||
|
||||
const prompts = extractPromptVariables(requestObj);
|
||||
prompts.push(...extractPromptVariables(allVariables));
|
||||
prompts.push(...extractPromptVariables(certsAndProxyConfig));
|
||||
|
||||
// return unique prompt variables
|
||||
return Array.from(new Set(prompts));
|
||||
};
|
||||
|
||||
// handler for sending http request
|
||||
ipcMain.handle('send-http-request', async (event, item, collection, environment, runtimeVariables) => {
|
||||
const collectionUid = collection.uid;
|
||||
@@ -1067,7 +1113,12 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
const promptVars = extractPromptVariables(request);
|
||||
const request = await prepareRequest(item, collection, abortController);
|
||||
request.__bruno__executionMode = 'runner';
|
||||
|
||||
const requestUid = uuid();
|
||||
|
||||
const promptVars = await extractPromptVariablesForRequest({ request, collection, envVars, runtimeVariables, processEnvVars });
|
||||
|
||||
if (promptVars.length > 0) {
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
@@ -1075,7 +1126,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
error: 'Request has been skipped due to containing prompt variables',
|
||||
responseReceived: {
|
||||
status: 'skipped',
|
||||
statusText: 'Prompt variables detected in request. Runner execution is not supported for requests with prompt variables.',
|
||||
statusText: `Prompt variables detected in request. Runner execution is not supported for requests with prompt variables. \n Promps: ${promptVars.join(', ')}`,
|
||||
data: null,
|
||||
responseTime: 0,
|
||||
headers: null
|
||||
@@ -1088,11 +1139,6 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
const request = await prepareRequest(item, collection, abortController);
|
||||
request.__bruno__executionMode = 'runner';
|
||||
|
||||
const requestUid = uuid();
|
||||
|
||||
try {
|
||||
let preRequestScriptResult;
|
||||
let preRequestError = null;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const { forOwn, cloneDeep } = require('lodash');
|
||||
const { interpolate } = require('@usebruno/common');
|
||||
|
||||
const interpolateString = (str, { globalEnvironmentVariables, envVars, runtimeVariables, processEnvVars }) => {
|
||||
const interpolateString = (str, { globalEnvironmentVariables, envVars, runtimeVariables, processEnvVars, promptVariables }) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ const interpolateString = (str, { globalEnvironmentVariables, envVars, runtimeVa
|
||||
processEnvVars = processEnvVars || {};
|
||||
runtimeVariables = runtimeVariables || {};
|
||||
globalEnvironmentVariables = globalEnvironmentVariables || {};
|
||||
promptVariables = promptVariables || {};
|
||||
|
||||
// we clone envVars because we don't want to modify the original object
|
||||
envVars = envVars ? cloneDeep(envVars) : {};
|
||||
@@ -30,6 +31,7 @@ const interpolateString = (str, { globalEnvironmentVariables, envVars, runtimeVa
|
||||
...globalEnvironmentVariables,
|
||||
...envVars,
|
||||
...runtimeVariables,
|
||||
...promptVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
|
||||
@@ -18,7 +18,7 @@ const getRawQueryString = (url) => {
|
||||
return queryIndex !== -1 ? url.slice(queryIndex) : '';
|
||||
};
|
||||
|
||||
const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, processEnvVars = {}) => {
|
||||
const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, processEnvVars = {}, promptVariables = {}) => {
|
||||
const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};
|
||||
const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
@@ -52,6 +52,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
|
||||
...requestVariables,
|
||||
...oauth2CredentialVariables,
|
||||
...runtimeVariables,
|
||||
...promptVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
|
||||
@@ -18,6 +18,7 @@ const prepareGrpcRequest = async (item, collection, environment, runtimeVariable
|
||||
const collectionRoot = collection?.draft?.root ? get(collection, 'draft.root', {}) : get(collection, 'root', {});
|
||||
const headers = {};
|
||||
const url = request.url;
|
||||
const { promptVariables = {} } = collection;
|
||||
|
||||
const scriptFlow = collection?.brunoConfig?.scripts?.flow ?? 'sandwich';
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
@@ -28,6 +29,7 @@ const prepareGrpcRequest = async (item, collection, environment, runtimeVariable
|
||||
mergeVars(collection, request, requestTreePath);
|
||||
request.globalEnvironmentVariables = collection?.globalEnvironmentVariables;
|
||||
request.oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({ oauth2Credentials: collection?.oauth2Credentials });
|
||||
request.promptVariables = promptVariables;
|
||||
}
|
||||
|
||||
each(get(request, 'headers', []), (h) => {
|
||||
@@ -49,6 +51,7 @@ const prepareGrpcRequest = async (item, collection, environment, runtimeVariable
|
||||
processEnvVars,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
promptVariables,
|
||||
body: request.body,
|
||||
protoPath: request.protoPath,
|
||||
// Add variable properties for interpolation
|
||||
@@ -68,7 +71,7 @@ const prepareGrpcRequest = async (item, collection, environment, runtimeVariable
|
||||
let credentials, credentialsId, oauth2Url, debugInfo;
|
||||
switch (grantType) {
|
||||
case 'authorization_code':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars, promptVariables);
|
||||
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid: collection.uid, certsAndProxyConfig }));
|
||||
grpcRequest.oauth2Credentials = { credentials, url: oauth2Url, collectionUid: collection.uid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
if (tokenPlacement == 'header') {
|
||||
@@ -82,7 +85,7 @@ const prepareGrpcRequest = async (item, collection, environment, runtimeVariable
|
||||
}
|
||||
break;
|
||||
case 'client_credentials':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars, promptVariables);
|
||||
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid: collection.uid, certsAndProxyConfig }));
|
||||
grpcRequest.oauth2Credentials = { credentials, url: oauth2Url, collectionUid: collection.uid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
if (tokenPlacement == 'header') {
|
||||
@@ -96,7 +99,7 @@ const prepareGrpcRequest = async (item, collection, environment, runtimeVariable
|
||||
}
|
||||
break;
|
||||
case 'password':
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);
|
||||
interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars, promptVariables);
|
||||
({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid: collection.uid, certsAndProxyConfig }));
|
||||
grpcRequest.oauth2Credentials = { credentials, url: oauth2Url, collectionUid: collection.uid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
|
||||
if (tokenPlacement == 'header') {
|
||||
@@ -112,7 +115,7 @@ const prepareGrpcRequest = async (item, collection, environment, runtimeVariable
|
||||
}
|
||||
}
|
||||
|
||||
interpolateVars(grpcRequest, envVars, runtimeVariables, processEnvVars);
|
||||
interpolateVars(grpcRequest, envVars, runtimeVariables, processEnvVars, promptVariables);
|
||||
processHeaders(grpcRequest.headers);
|
||||
|
||||
return grpcRequest;
|
||||
|
||||
@@ -327,6 +327,7 @@ const prepareRequest = async (item, collection = {}, abortController) => {
|
||||
mergeAuth(collection, request, requestTreePath);
|
||||
request.globalEnvironmentVariables = collection?.globalEnvironmentVariables;
|
||||
request.oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({ oauth2Credentials: collection?.oauth2Credentials });
|
||||
request.promptVariables = collection?.promptVariables || {};
|
||||
}
|
||||
|
||||
|
||||
@@ -463,6 +464,7 @@ const prepareRequest = async (item, collection = {}, abortController) => {
|
||||
axiosRequest.collectionVariables = request.collectionVariables;
|
||||
axiosRequest.folderVariables = request.folderVariables;
|
||||
axiosRequest.requestVariables = request.requestVariables;
|
||||
axiosRequest.promptVariables = request.promptVariables;
|
||||
axiosRequest.globalEnvironmentVariables = request.globalEnvironmentVariables;
|
||||
axiosRequest.oauth2CredentialVariables = request.oauth2CredentialVariables;
|
||||
axiosRequest.assertions = request.assertions;
|
||||
|
||||
@@ -3,13 +3,13 @@ const { configureRequest } = require('../../src/ipc/network/index');
|
||||
describe('index: configureRequest', () => {
|
||||
it("Should add 'http://' to the URL if no protocol is specified", async () => {
|
||||
const request = { method: 'GET', url: 'test-domain', body: {} };
|
||||
await configureRequest(null, null, request, null, null, null, null);
|
||||
await configureRequest(null, {}, request, null, null, null, null);
|
||||
expect(request.url).toEqual('http://test-domain');
|
||||
});
|
||||
|
||||
it("Should NOT add 'http://' to the URL if a protocol is specified", async () => {
|
||||
const request = { method: 'GET', url: 'ftp://test-domain', body: {} };
|
||||
await configureRequest(null, null, request, null, null, null, null);
|
||||
await configureRequest(null, {}, request, null, null, null, null);
|
||||
expect(request.url).toEqual('ftp://test-domain');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,9 +7,10 @@ const { jar: createCookieJar } = require('@usebruno/requests').cookies;
|
||||
const variableNameRegex = /^[\w-.]*$/;
|
||||
|
||||
class Bru {
|
||||
constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName) {
|
||||
constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables) {
|
||||
this.envVariables = envVariables || {};
|
||||
this.runtimeVariables = runtimeVariables || {};
|
||||
this.promptVariables = promptVariables || {};
|
||||
this.processEnvVars = cloneDeep(processEnvVars || {});
|
||||
this.collectionVariables = collectionVariables || {};
|
||||
this.folderVariables = folderVariables || {};
|
||||
@@ -134,6 +135,7 @@ class Bru {
|
||||
...this.requestVariables,
|
||||
...this.oauth2CredentialVariables,
|
||||
...this.runtimeVariables,
|
||||
...this.promptVariables,
|
||||
process: {
|
||||
env: {
|
||||
...this.processEnvVars
|
||||
|
||||
@@ -255,6 +255,7 @@ class AssertRuntime {
|
||||
return [];
|
||||
}
|
||||
|
||||
const promptVariables = request?.promptVariables || {};
|
||||
const bru = new Bru(
|
||||
envVariables,
|
||||
runtimeVariables,
|
||||
@@ -263,7 +264,10 @@ class AssertRuntime {
|
||||
collectionVariables,
|
||||
folderVariables,
|
||||
requestVariables,
|
||||
globalEnvironmentVariables
|
||||
globalEnvironmentVariables,
|
||||
{},
|
||||
undefined,
|
||||
promptVariables
|
||||
);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = createResponseParser(response);
|
||||
|
||||
@@ -61,8 +61,9 @@ class ScriptRuntime {
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const promptVariables = request?.promptVariables || {};
|
||||
const assertionResults = request?.assertionResults || [];
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName);
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
||||
const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
|
||||
@@ -234,8 +235,9 @@ class ScriptRuntime {
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const assertionResults = request?.assertionResults || [];
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName);
|
||||
const promptVariables = request?.promptVariables || {};
|
||||
const assertionResults = request?.assertionResults || {};
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
||||
|
||||
@@ -61,8 +61,9 @@ class TestRuntime {
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const promptVariables = request?.promptVariables || {};
|
||||
const assertionResults = request?.assertionResults || [];
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, {}, collectionName);
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, {}, collectionName, promptVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
||||
|
||||
@@ -35,7 +35,8 @@ class VarsRuntime {
|
||||
return;
|
||||
}
|
||||
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, undefined, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables);
|
||||
const promptVariables = request?.promptVariables || {};
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, undefined, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, undefined, promptVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = createResponseParser(response);
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "prompt-variables-interpolation",
|
||||
"type": "collection",
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
],
|
||||
"size": 0.0008153915405273438,
|
||||
"filesCount": 4,
|
||||
"clientCertificates": {
|
||||
"enabled": true,
|
||||
"certs": [
|
||||
{
|
||||
"domain": "localhost:8081",
|
||||
"type": "pfx",
|
||||
"pfxFilePath": "../client.pfx",
|
||||
"passphrase": "{{?Enter Client CA Password}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
auth {
|
||||
mode: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: auth_unsername
|
||||
password: {{?Enter Collection Auth Password}}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
collectionVar: {{?Enter Collection Variable}}
|
||||
~collectionVarDisabled: {{?Should Not Prompt collectionVarDisabled}}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
vars {
|
||||
collectionEnvVar: {{?Enter Collection Env Variable}}
|
||||
~collectionEnvVarDisabled: {{?Should Not Prompt collectionEnvVarDisabled}}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
meta {
|
||||
name: http-folder
|
||||
}
|
||||
|
||||
headers {
|
||||
folderHeaderVar: {{?Enter Folder Header Variable}}
|
||||
~folderHeaderVarDisabled: {{?Should Not Prompt folderHeaderVarDisabled}}
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: basic
|
||||
}
|
||||
|
||||
auth:basic {
|
||||
username: auth_username
|
||||
password: {{?Enter Folder Auth Password}}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
folderVar: {{?Enter Folder Variable}}
|
||||
~folderVarDisabled: {{?Should Not Prompt folderVarDisabled}}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
meta {
|
||||
name: http-request-without-ca
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: http://localhost:8081/api/echo/json?query={{?Enter Query Variable}}
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
params:query {
|
||||
query: {{?Enter Query Variable}}
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
~x-disabled-header: {{?Should Not Prompt request x-disabled-header}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"body": "{{?Enter Body Variable}}",
|
||||
"bodyNumber": {{?Enter Number Variable}},
|
||||
"bodyBoolean": {{?Enter Boolean Variable}},
|
||||
"repeat-1": "{{?Enter Body Variable}}",
|
||||
"requestVar": "{{requestVar}}",
|
||||
"folderVar": "{{folderVar}}",
|
||||
"collectionVar": "{{collectionVar}}",
|
||||
"collectionEnvVar": "{{collectionEnvVar}}",
|
||||
"globalEnvVar": "{{globalEnvVar}}",
|
||||
"folderHeader": "{{folderHeader}}"
|
||||
}
|
||||
}
|
||||
|
||||
body:form-urlencoded {
|
||||
formurlencoded: {{?Should Not Prompt body mode form-urlencoded}}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
requestVar: {{?Enter Request Variable}}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
meta {
|
||||
name: https-request-with-ca
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: https://localhost:8081/api/echo/json
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"body": "test"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { closeAllCollections } from '../../utils/page';
|
||||
|
||||
test.describe('Prompt Variables Interpolation', () => {
|
||||
test.afterAll(async ({ pageWithUserData: page }) => {
|
||||
// cleanup: close all collections
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
// without client certificate - no HTTPS
|
||||
test('Verifying if the prompt variables are prompted correctly for the http request - without client certificate', async ({ pageWithUserData: page }) => {
|
||||
let promptVariablesModal;
|
||||
let promptInputs;
|
||||
|
||||
await test.step('Open collection and navigate to the http request with prompt variables', async () => {
|
||||
// Open collection and accept sandbox mode
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'prompt-variables-interpolation' }).click();
|
||||
|
||||
// Navigate to the request
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'http-folder' }).click();
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'http-request-without-ca' }).click();
|
||||
});
|
||||
|
||||
await test.step('Send the request and verify the prompt variables modal is visible', async () => {
|
||||
// Send the request
|
||||
await page.getByTestId('send-arrow-icon').click();
|
||||
|
||||
promptVariablesModal = page.getByRole('dialog').filter({ has: page.locator('.bruno-modal-header-title').getByText('Input Required') });
|
||||
await promptVariablesModal.waitFor({ state: 'visible' });
|
||||
});
|
||||
|
||||
await test.step('Verify duplicate prompt variables are not allowed', async () => {
|
||||
// Enter the prompt variables
|
||||
promptInputs = promptVariablesModal.getByTestId('prompt-variable-input-container');
|
||||
await expect(promptInputs).toHaveCount(11);
|
||||
});
|
||||
|
||||
await test.step('Verify disabled / non selected modes prompt variables are not prompted', async () => {
|
||||
// verify that any prompt added to the inactive fields starting with label "Should Not Prompt" are not displayed
|
||||
// eg: 1. Headers - disabled or hierarchical overrides should not be displayed
|
||||
// 2. Vars - respects hierarchical overrides, eg: request var > folder var > collection var > global env var
|
||||
// 3. Body - only prompts from selected body mode should be displayed eg: json
|
||||
// 4. Auth - only prompts from selected mode should be displayed eg: basic, respects hierarchical overrides
|
||||
// 5. Client Cert - only prompts from current domain config should be displayed
|
||||
await expect(promptInputs.filter({ hasText: 'Should Not Prompt', exact: true })).toHaveCount(0);
|
||||
});
|
||||
|
||||
await test.step('Fill the prompt variables and send the request', async () => {
|
||||
await promptInputs.filter({ hasText: 'Enter Query Variable' }).locator('input').fill('queryPromptValue');
|
||||
await promptInputs.filter({ hasText: 'Enter Body Variable' }).locator('input').fill('bodyPromptValue');
|
||||
await promptInputs.filter({ hasText: 'Enter Number Variable' }).locator('input').fill('123');
|
||||
await promptInputs.filter({ hasText: 'Enter Boolean Variable' }).locator('input').fill('true');
|
||||
await promptInputs.filter({ hasText: 'Enter Request Variable' }).locator('input').fill('requestVarPromptValue');
|
||||
await promptInputs.filter({ hasText: 'Enter Folder Variable' }).locator('input').fill('folderVarPromptValue');
|
||||
await promptInputs.filter({ hasText: 'Enter Collection Variable' }).locator('input').fill('collectionVarPromptValue');
|
||||
await promptInputs.filter({ hasText: 'Enter Collection Env Variable' }).locator('input').fill('collectionEnvVarPromptValue');
|
||||
await promptInputs.filter({ hasText: 'Enter Global Env Variable' }).locator('input').fill('globalEnvVarPromptValue');
|
||||
await promptInputs.filter({ hasText: 'Enter Folder Auth Password' }).locator('input').fill('folderAuthPasswordValue');
|
||||
await promptInputs.filter({ hasText: 'Enter Folder Header Variable' }).locator('input').fill('folderHeaderVarPromptValue');
|
||||
|
||||
// Submit the prompt variables
|
||||
await promptVariablesModal.getByRole('button', { name: 'Continue' }).click();
|
||||
});
|
||||
|
||||
await test.step('Verify the request is sent with the correct variables', async () => {
|
||||
// Verify the response status code
|
||||
await expect(page.getByTestId('response-status-code')).toHaveText(/200/);
|
||||
await expect(page.locator('.response-pane').locator('.CodeMirror-line').getByText('"folderVar": "folderVarPromptValue"').first()).toBeVisible();
|
||||
await expect(page.locator('.response-pane').locator('.CodeMirror-line').getByText('"collectionVar": "collectionVarPromptValue"').first()).toBeVisible();
|
||||
await expect(page.locator('.response-pane').locator('.CodeMirror-line').getByText('"collectionEnvVar": "collectionEnvVarPromptValue"').first()).toBeVisible();
|
||||
await expect(page.locator('.response-pane').locator('.CodeMirror-line').getByText('"globalEnvVar": "globalEnvVarPromptValue"').first()).toBeVisible();
|
||||
await expect(page.locator('.response-pane').locator('.CodeMirror-line').getByText('"requestVar": "requestVarPromptValue"').first()).toBeVisible();
|
||||
await expect(page.locator('.response-pane').locator('.CodeMirror-line').getByText('"body": "bodyPromptValue"').first()).toBeVisible();
|
||||
await expect(page.locator('.response-pane').locator('.CodeMirror-line').getByText('"repeat-1": "bodyPromptValue"').first()).toBeVisible();
|
||||
await expect(page.locator('.response-pane').locator('.CodeMirror-line').getByText('"bodyNumber": 123').first()).toBeVisible();
|
||||
await expect(page.locator('.response-pane').locator('.CodeMirror-line').getByText('"bodyBoolean": true').first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// with client certificate - HTTPS
|
||||
test('Verifying if the prompt variables are prompted correctly for the http request - with client certificate', async ({ pageWithUserData: page }) => {
|
||||
let promptVariablesModal;
|
||||
let promptInputs;
|
||||
|
||||
await test.step('Open collection and navigate to the http request with prompt variables', async () => {
|
||||
// Open collection and accept sandbox mode
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'prompt-variables-interpolation' }).click();
|
||||
|
||||
// Navigate to the request
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'http-folder' }).click();
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'https-request-with-ca' }).click();
|
||||
});
|
||||
|
||||
await test.step('Send the request and verify the prompt variables modal is visible', async () => {
|
||||
// Send the request
|
||||
await page.getByTestId('send-arrow-icon').click();
|
||||
|
||||
promptVariablesModal = page.getByRole('dialog').filter({ has: page.locator('.bruno-modal-header-title').getByText('Input Required') });
|
||||
await promptVariablesModal.waitFor({ state: 'visible' });
|
||||
});
|
||||
|
||||
await test.step('Verify disabled / non selected modes prompt variables are not prompted', async () => {
|
||||
promptInputs = promptVariablesModal.getByTestId('prompt-variable-input-container');
|
||||
// verify that any prompt added to the inactive fields starting with label "Should Not Prompt" are not displayed
|
||||
// eg: 1. Headers - disabled or hierarchical overrides should not be displayed
|
||||
// 2. Vars - respects hierarchical overrides, eg: request var > folder var > collection var > global env var
|
||||
// 3. Body - only prompts from selected body mode should be displayed eg: json
|
||||
// 4. Auth - only prompts from selected mode should be displayed eg: basic, respects hierarchical overrides
|
||||
// 5. Client Cert - only prompts from current domain config should be displayed
|
||||
await expect(promptInputs.filter({ hasText: 'Should Not Prompt', exact: true })).toHaveCount(0);
|
||||
await expect(promptInputs.filter({ hasText: 'Enter Client CA Password', exact: true })).toHaveCount(1);
|
||||
});
|
||||
|
||||
await test.step('Fill the prompt variables and send the request', async () => {
|
||||
await promptInputs.filter({ hasText: 'Enter Client CA Password' }).locator('input').fill('clientCAPasswordValue');
|
||||
// leave the rest of the prompt variables empty
|
||||
|
||||
// Submit the prompt variables
|
||||
await promptVariablesModal.getByRole('button', { name: 'Continue' }).click();
|
||||
});
|
||||
|
||||
// @TODO: setup a valid certificate and server required to verify the request is sent with the correct variables
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"collections": [
|
||||
{
|
||||
"path": "{{projectRoot}}/tests/interpolation/prompt-variables/fixtures/collection",
|
||||
"securityConfig": {
|
||||
"jsSandboxMode": "safe"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"environments": [
|
||||
{
|
||||
"uid": "FlaexlO7lcH7UtEpWsVyz",
|
||||
"name": "E2E_Global",
|
||||
"variables": [
|
||||
{
|
||||
"uid": "lflBDSYBdHkUedYhBF4Ty",
|
||||
"name": "globalEnvVar",
|
||||
"value": "{{?Enter Global Env Variable}}",
|
||||
"type": "text",
|
||||
"secret": false,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"uid": "lflBDSYBdHkUedYhBF4Ty",
|
||||
"name": "globalEnvVarDisabled",
|
||||
"value": "{{?Should Not Prompt globalEnvVarDisabled}}",
|
||||
"type": "text",
|
||||
"secret": false,
|
||||
"enabled": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"activeGlobalEnvironmentUid": "FlaexlO7lcH7UtEpWsVyz"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"maximized": false,
|
||||
"lastOpenedCollections": [
|
||||
"{{projectRoot}}/tests/interpolation/prompt-variables/fixtures/collection"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"collections": [
|
||||
{
|
||||
"pathname": "{{projectRoot}}/tests/interpolation/prompt-variables/fixtures/collection",
|
||||
"selectedEnvironment": "local"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user