feat: enhance support for prompt variables

This commit is contained in:
Bijin Bruno
2025-11-17 20:12:20 +05:30
parent d28f2f32e9
commit 48a09f6f50
40 changed files with 708 additions and 212 deletions

View File

@@ -96,9 +96,6 @@ const StyledWrapper = styled.div`
.cm-variable-invalid {
color: red;
}
.cm-variable-prompt {
color: dodgerblue;
}
.CodeMirror-search-hint {
display: inline;

View File

@@ -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;

View File

@@ -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>
);

View File

@@ -50,9 +50,6 @@ const StyledWrapper = styled.div`
.cm-variable-invalid {
color: red;
}
.cm-variable-prompt {
color: blue;
}
.CodeMirror-search-hint {
display: inline;

View File

@@ -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 = /\{\{([^}]+)\}\}/;

View File

@@ -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
};

View File

@@ -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', () => {

View File

@@ -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;

View File

@@ -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', {

View File

@@ -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)',

View File

@@ -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',

View File

@@ -40,4 +40,4 @@ export const resolveInheritedAuth = (item, collection) => {
...mergedRequest,
auth: effectiveAuth
};
};
};

View File

@@ -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');
});
});
});

View File

@@ -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 '';

View 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']));
});
});

View File

@@ -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 {

View File

@@ -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
};

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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');
});
});

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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}}"
}
]
}
}

View File

@@ -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}}
}

View File

@@ -0,0 +1,4 @@
vars {
collectionEnvVar: {{?Enter Collection Env Variable}}
~collectionEnvVarDisabled: {{?Should Not Prompt collectionEnvVarDisabled}}
}

View File

@@ -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}}
}

View File

@@ -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}}
}

View File

@@ -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"
}
}

View File

@@ -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
});
});

View File

@@ -0,0 +1,10 @@
{
"collections": [
{
"path": "{{projectRoot}}/tests/interpolation/prompt-variables/fixtures/collection",
"securityConfig": {
"jsSandboxMode": "safe"
}
}
]
}

View File

@@ -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"
}

View File

@@ -0,0 +1,6 @@
{
"maximized": false,
"lastOpenedCollections": [
"{{projectRoot}}/tests/interpolation/prompt-variables/fixtures/collection"
]
}

View File

@@ -0,0 +1,8 @@
{
"collections": [
{
"pathname": "{{projectRoot}}/tests/interpolation/prompt-variables/fixtures/collection",
"selectedEnvironment": "local"
}
]
}