mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-25 13:45:52 +00:00
Merge pull request #6104 from bijin-bruno/feature/prompt-vars-extended
This commit is contained in:
@@ -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;
|
||||
@@ -0,0 +1,56 @@
|
||||
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({});
|
||||
|
||||
const handleChange = (prompt, value) => {
|
||||
setValues((prev) => ({ ...prev, [prompt]: value }));
|
||||
};
|
||||
|
||||
if (!prompts?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
size="lg"
|
||||
title={title}
|
||||
confirmText="Continue"
|
||||
cancelText="Cancel"
|
||||
handleConfirm={() => onSubmit(values)}
|
||||
handleCancel={onCancel}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -237,6 +237,9 @@ const GlobalStyle = createGlobalStyle`
|
||||
.cm-variable-invalid {
|
||||
color: ${(props) => props.theme.codemirror.variable.invalid};
|
||||
}
|
||||
.cm-variable-prompt {
|
||||
color: ${(props) => props.theme.codemirror.variable.prompt};
|
||||
}
|
||||
}
|
||||
.CodeMirror-brunoVarInfo {
|
||||
color: ${(props) => props.theme.codemirror.variable.info.color};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Provider } from 'react-redux';
|
||||
import { AppProvider } from 'providers/App';
|
||||
import { ToastProvider } from 'providers/Toaster';
|
||||
import { HotkeysProvider } from 'providers/Hotkeys';
|
||||
import { PromptVariablesProvider } from 'providers/PromptVariables';
|
||||
|
||||
import ReduxStore from 'providers/ReduxStore';
|
||||
import ThemeProvider from 'providers/Theme/index';
|
||||
@@ -44,11 +45,13 @@ function Main({ children }) {
|
||||
<Provider store={ReduxStore}>
|
||||
<ThemeProvider>
|
||||
<ToastProvider>
|
||||
<AppProvider>
|
||||
<HotkeysProvider>
|
||||
{children}
|
||||
</HotkeysProvider>
|
||||
</AppProvider>
|
||||
<PromptVariablesProvider>
|
||||
<AppProvider>
|
||||
<HotkeysProvider>
|
||||
{children}
|
||||
</HotkeysProvider>
|
||||
</AppProvider>
|
||||
</PromptVariablesProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
@@ -57,5 +60,3 @@ function Main({ children }) {
|
||||
}
|
||||
|
||||
export default Main;
|
||||
|
||||
|
||||
|
||||
52
packages/bruno-app/src/providers/PromptVariables/index.js
Normal file
52
packages/bruno-app/src/providers/PromptVariables/index.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import PromptVariablesModal from 'components/RequestPane/PromptVariables/PromptVariablesModal';
|
||||
import React, { createContext, useCallback, useState } from 'react';
|
||||
|
||||
const PromptVariablesContext = createContext();
|
||||
|
||||
export function PromptVariablesProvider({ children }) {
|
||||
const [modalState, setModalState] = useState({ open: false, prompts: [], resolve: null, reject: null });
|
||||
|
||||
const prompt = useCallback((prompts) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setModalState({ open: true, prompts, resolve, reject });
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Expose globally for non-component code (e.g., Redux thunks)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.promptForVariables = async (prompts) => {
|
||||
try {
|
||||
return await prompt(prompts);
|
||||
} catch (err) {
|
||||
if (err !== 'cancelled') console.error('window.promptForVariables encountered an error:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleSubmit = (values) => {
|
||||
modalState.resolve(values);
|
||||
setModalState({ open: false, prompts: [], resolve: null, reject: null });
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
modalState.reject('cancelled');
|
||||
setModalState({ open: false, prompts: [], resolve: null, reject: null });
|
||||
};
|
||||
|
||||
return (
|
||||
<PromptVariablesContext.Provider value={{ prompt }}>
|
||||
{children}
|
||||
{modalState.open && (
|
||||
<PromptVariablesModal
|
||||
title="Input Required"
|
||||
prompts={modalState.prompts}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
</PromptVariablesContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default PromptVariablesProvider;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { collectionSchema, environmentSchema, itemSchema } from '@usebruno/schema';
|
||||
import { parseQueryParams } from '@usebruno/common/utils';
|
||||
import { parseQueryParams, extractPromptVariables } from '@usebruno/common/utils';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import filter from 'lodash/filter';
|
||||
import find from 'lodash/find';
|
||||
@@ -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';
|
||||
import { saveGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
@@ -380,6 +384,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;
|
||||
@@ -395,9 +469,26 @@ 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;
|
||||
|
||||
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(
|
||||
updateResponsePaneScrollPosition({
|
||||
uid: state.tabs.activeTabUid,
|
||||
@@ -413,13 +504,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';
|
||||
@@ -1369,7 +1453,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'));
|
||||
}
|
||||
@@ -1386,6 +1470,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', {
|
||||
|
||||
@@ -288,6 +288,7 @@ const darkTheme = {
|
||||
variable: {
|
||||
valid: 'rgb(11 178 126)',
|
||||
invalid: '#f06f57',
|
||||
prompt: '#3D8DF5',
|
||||
info: {
|
||||
color: '#FFFFFF',
|
||||
bg: '#343434',
|
||||
|
||||
@@ -289,6 +289,7 @@ const lightTheme = {
|
||||
variable: {
|
||||
valid: '#047857',
|
||||
invalid: 'rgb(185, 28, 28)',
|
||||
prompt: '#186ADE',
|
||||
info: {
|
||||
color: '#343434',
|
||||
bg: '#FFFFFF',
|
||||
|
||||
@@ -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']));
|
||||
});
|
||||
});
|
||||
@@ -30,6 +30,12 @@ export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPa
|
||||
while ((ch = stream.next()) != null) {
|
||||
if (ch === '}' && stream.peek() === '}') {
|
||||
stream.eat('}');
|
||||
|
||||
// Prompt variable: starts with '?'
|
||||
if (word.startsWith('?')) {
|
||||
return `variable-prompt`;
|
||||
}
|
||||
|
||||
// Check if it's a mock variable (starts with $) and exists in mockDataFunctions
|
||||
const isMockVariable = word.startsWith('$') && mockDataFunctions.hasOwnProperty(word.substring(1));
|
||||
const found = isMockVariable || pathFoundInVariables(word, variables);
|
||||
|
||||
@@ -25,12 +25,65 @@ const { NtlmClient } = require('axios-ntlm');
|
||||
const { addDigestInterceptor } = require('@usebruno/requests');
|
||||
const { getCACertificates } = require('@usebruno/requests');
|
||||
const { getOAuth2Token } = require('../utils/oauth2');
|
||||
const { encodeUrl, buildFormUrlEncodedPayload } = require('@usebruno/common').utils;
|
||||
const { encodeUrl, buildFormUrlEncodedPayload, extractPromptVariables } = require('@usebruno/common').utils;
|
||||
|
||||
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,
|
||||
@@ -74,6 +127,39 @@ const runSingleRequest = async function (
|
||||
|
||||
request = await prepareRequest(item, collection);
|
||||
|
||||
// Detect prompt variables before proceeding
|
||||
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. \nPrompts: ${promptVars.join(', ')}`;
|
||||
console.log(chalk.yellow(stripExtension(relativeItemPathname) + ' Skipped:') + chalk.dim(` (${errorMsg})`));
|
||||
return {
|
||||
test: {
|
||||
filename: relativeItemPathname
|
||||
},
|
||||
request: {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: request.headers,
|
||||
data: request.data
|
||||
},
|
||||
response: {
|
||||
status: 'skipped',
|
||||
statusText: errorMsg,
|
||||
data: null,
|
||||
responseTime: 0
|
||||
},
|
||||
error: null,
|
||||
status: 'skipped',
|
||||
skipped: true,
|
||||
assertionResults: [],
|
||||
testResults: [],
|
||||
preRequestTestResults: [],
|
||||
postResponseTestResults: [],
|
||||
shouldStopRunnerExecution
|
||||
};
|
||||
}
|
||||
|
||||
request.__bruno__executionMode = 'cli';
|
||||
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
@@ -172,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 {
|
||||
|
||||
@@ -11,3 +11,8 @@ export {
|
||||
export {
|
||||
patternHasher
|
||||
} from './template-hasher';
|
||||
|
||||
export {
|
||||
extractPromptVariables,
|
||||
extractPromptVariablesFromString
|
||||
} from './prompt-variables';
|
||||
|
||||
54
packages/bruno-common/src/utils/prompt-variables.spec.ts
Normal file
54
packages/bruno-common/src/utils/prompt-variables.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from '@jest/globals';
|
||||
|
||||
import { extractPromptVariables, extractPromptVariablesFromString } from './prompt-variables';
|
||||
|
||||
describe('prompt variable utils', () => {
|
||||
describe('extractPromptVariablesFromString', () => {
|
||||
it('should extract prompt variables', () => {
|
||||
expect(extractPromptVariablesFromString('Hello {{?world}}')).toEqual(['world']);
|
||||
expect(extractPromptVariablesFromString('No prompts here')).toEqual([]);
|
||||
expect(extractPromptVariablesFromString('Multiple {{?prompts}} in {{?one}} string')).toEqual(['prompts', 'one']);
|
||||
});
|
||||
|
||||
it('should deduplicate prompt variables', () => {
|
||||
// Strings
|
||||
expect(extractPromptVariables('{{?world}} prompt here Hello {{?world}}')).toEqual(['world']);
|
||||
expect(extractPromptVariables('Multiple {{?prompts}} in {{?one}} string plus another {{?one}}')).toEqual(['prompts', 'one']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractPromptVariables', () => {
|
||||
it('should extract prompt variables from strings', () => {
|
||||
expect(extractPromptVariables('Hello {{?world}}')).toEqual(['world']);
|
||||
expect(extractPromptVariables('No prompts here')).toEqual([]);
|
||||
expect(extractPromptVariables('Multiple {{?prompts}} in {{?one}} string')).toEqual(['prompts', 'one']);
|
||||
});
|
||||
|
||||
it('should extract prompt variables from objects', () => {
|
||||
expect(extractPromptVariables({ text: 'Hello {{?world}}' })).toEqual(['world']);
|
||||
expect(extractPromptVariables({ noPrompt: 'No prompt here' })).toEqual([]);
|
||||
expect(extractPromptVariables({ prompt1: 'Hello {{?world}}', prompt2: 'Another {{?test}}' })).toEqual(['world', 'test']);
|
||||
});
|
||||
|
||||
it('should extract prompt variables from arrays', () => {
|
||||
// Strings
|
||||
expect(extractPromptVariables(['No prompts here', 'Hello {{?world}}'])).toEqual(['world']);
|
||||
expect(extractPromptVariables(['Multiple {{?prompts}} in {{?one}} string', 'Another {{?test}} string'])).toEqual(['prompts', 'one', 'test']);
|
||||
|
||||
// Objects
|
||||
expect(extractPromptVariables([{ prompt: 'Hello {{?world}}', noprompt: 'No prompt here' }, { noprompt: '' }])).toEqual(['world']);
|
||||
|
||||
// Nested arrays
|
||||
expect(extractPromptVariables(['Prompt {{?here}}', ['Hello {{?world}}', 'Another {{?test}} string']])).toEqual(['here', 'world', 'test']);
|
||||
|
||||
// Mixed data types
|
||||
expect(extractPromptVariables([{ text: 'Multiple {{?prompts}} in {{?one}} string', noPrompt: 'No prompt here' }, ['Another {{?test}} string', { prompt: '{{?nested}}', no: 'prompt' }]])).toEqual(['prompts', 'one', 'test', 'nested']);
|
||||
});
|
||||
|
||||
it('should deduplicate prompt variables', () => {
|
||||
// Strings
|
||||
expect(extractPromptVariables(['{{?world}} prompt here', 'Hello {{?world}}'])).toEqual(['world']);
|
||||
expect(extractPromptVariables(['Multiple {{?prompts}} in {{?one}} string', 'Another {{?one}} string'])).toEqual(['prompts', 'one']);
|
||||
});
|
||||
});
|
||||
});
|
||||
45
packages/bruno-common/src/utils/prompt-variables.ts
Normal file
45
packages/bruno-common/src/utils/prompt-variables.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Extract prompt variables matching {{?<Prompt Text>}} from a string.
|
||||
* @param {string} str - The input string.
|
||||
* @returns {string[]} - An array of extracted prompt variables.
|
||||
*/
|
||||
export const extractPromptVariablesFromString = (str: string): string[] => {
|
||||
const regex = /{{\?([^}]+)}}/g;
|
||||
const prompts = new Set<string>();
|
||||
let match;
|
||||
while ((match = regex.exec(str)) !== null) {
|
||||
prompts.add(match[1].trim());
|
||||
}
|
||||
return Array.from(prompts);
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract prompt variables from an object.
|
||||
* @param {*} obj - The input object.
|
||||
* @returns {string[]} - An array of extracted prompt variables.
|
||||
*/
|
||||
export function extractPromptVariables(obj: any): string[] {
|
||||
const prompts = new Set<string>();
|
||||
try {
|
||||
if (typeof obj === 'string') {
|
||||
// Extract prompt variables from strings
|
||||
const extracted = extractPromptVariablesFromString(obj);
|
||||
extracted.forEach((prompt) => prompts.add(prompt));
|
||||
} else if (Array.isArray(obj)) {
|
||||
// Recursively extract from array elements
|
||||
for (const item of obj) {
|
||||
const extracted = extractPromptVariables(item);
|
||||
extracted.forEach((prompt) => prompts.add(prompt));
|
||||
}
|
||||
} else if (typeof obj === 'object' && obj !== null) {
|
||||
// Recursively extract from object properties
|
||||
for (const key in obj) {
|
||||
const extracted = extractPromptVariables(obj[key]);
|
||||
extracted.forEach((prompt) => prompts.add(prompt));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error extracting prompt variables:', error);
|
||||
}
|
||||
return Array.from(prompts);
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ const { each, get, extend, cloneDeep, merge } = require('lodash');
|
||||
const { NtlmClient } = require('axios-ntlm');
|
||||
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
|
||||
const { encodeUrl } = require('@usebruno/common').utils;
|
||||
const { extractPromptVariables } = require('@usebruno/common').utils;
|
||||
const { interpolateString } = require('./interpolate-string');
|
||||
const { resolveAwsV4Credentials, addAwsV4Interceptor } = require('./awsv4auth-helper');
|
||||
const { addDigestInterceptor } = require('@usebruno/requests');
|
||||
@@ -144,6 +145,7 @@ const configureRequest = async (
|
||||
|
||||
request.maxRedirects = 0;
|
||||
|
||||
const { promptVariables = {} } = collection;
|
||||
let { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;
|
||||
let axiosInstance = makeAxiosInstance({
|
||||
proxyMode,
|
||||
@@ -164,7 +166,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) {
|
||||
@@ -180,7 +182,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') {
|
||||
@@ -196,7 +198,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) {
|
||||
@@ -212,7 +214,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) {
|
||||
@@ -415,7 +417,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 });
|
||||
@@ -456,7 +459,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);
|
||||
@@ -965,6 +968,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;
|
||||
@@ -1148,9 +1195,30 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
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', {
|
||||
type: 'runner-request-skipped',
|
||||
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. \n Promps: ${promptVars.join(', ')}`,
|
||||
data: null,
|
||||
responseTime: 0,
|
||||
headers: null
|
||||
},
|
||||
...eventData
|
||||
});
|
||||
|
||||
currentRequestIndex++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
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