fix: variable name validation in brunoVarInfo (#6203)

* fix: variable name validation in brunoVarInfo
This commit is contained in:
Pooja
2025-11-26 00:50:57 +05:30
committed by GitHub
parent c2d000e805
commit b0405b1e1a
3 changed files with 133 additions and 8 deletions

View File

@@ -297,7 +297,7 @@ const GlobalStyle = createGlobalStyle`
padding: 0.125rem 0.375rem;
background: #D977061A;
border-radius: 0.25rem;
font-size: 0.875rem;
font-size: 0.75rem;
color: #D97706;
letter-spacing: 0.03125rem;
}
@@ -465,6 +465,13 @@ const GlobalStyle = createGlobalStyle`
margin-top: 0.25rem;
}
.CodeMirror-brunoVarInfo .var-warning-note {
font-size: 0.75rem;
color: #ef4444;
margin-top: 0.375rem;
line-height: 1.25rem;
}
.CodeMirror-hint-active {
background: #08f !important;
color: #fff !important;

View File

@@ -13,6 +13,7 @@ import store from 'providers/ReduxStore';
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
import { MaskedEditor } from 'utils/common/masked-editor';
import { setupAutoComplete } from 'utils/codemirror/autocomplete';
import { variableNameRegex } from 'utils/common/regex';
let CodeMirror;
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
@@ -232,14 +233,14 @@ export const renderVarInfo = (token, options) => {
const isReadOnly = scopeInfo.type === 'process.env' || scopeInfo.type === 'runtime' || scopeInfo.type === 'undefined';
// Get raw value from scope
const rawValue = scopeInfo?.value || '';
const rawValue = scopeInfo.value || '';
// Check if variable should be masked:
const isSecret = scopeInfo.type !== 'undefined' ? isVariableSecret(scopeInfo) : false;
const hasSecretReferences = containsSecretVariableReferences(rawValue, collection, item);
const shouldMaskValue = isSecret || hasSecretReferences;
const isMasked = options?.variables?.maskedEnvVariables?.includes(variableName);
const isMasked = options.variables?.maskedEnvVariables?.includes(variableName);
const into = document.createElement('div');
into.className = 'bruno-var-info-container';
@@ -264,6 +265,20 @@ export const renderVarInfo = (token, options) => {
header.appendChild(scopeBadge);
into.appendChild(header);
// Check if variable name is valid (only for non-process.env variables)
const isValidVariableName = scopeInfo.type === 'process.env' || variableNameRegex.test(variableName);
// Show warning if variable name is invalid
if (!isValidVariableName) {
const warningNote = document.createElement('div');
warningNote.className = 'var-warning-note';
warningNote.textContent = 'Invalid variable name! Variables must only contain alpha-numeric characters, "-", "_", "."';
into.appendChild(warningNote);
// Don't show value or any other content for invalid variable names
return into;
}
// Value container with icons
const valueContainer = document.createElement('div');
valueContainer.className = 'var-value-container';
@@ -327,6 +342,17 @@ export const renderVarInfo = (token, options) => {
let originalValue = rawValue;
let isEditing = false;
cmEditor.setOption('extraKeys', {
'Enter': (cm) => {
// Enter: save and blur
cm.getInputField().blur();
},
'Shift-Enter': (cm) => {
// Shift+Enter: insert new line
cm.replaceSelection('\n', 'end');
}
});
// Dynamically adjust editor height as content changes
cmEditor.on('change', () => {
if (isEditing) {
@@ -495,17 +521,17 @@ export const renderVarInfo = (token, options) => {
valueContainer.appendChild(iconsContainer);
// Read-only note
if (scopeInfo?.type === 'process.env') {
if (scopeInfo.type === 'process.env') {
const readOnlyNote = document.createElement('div');
readOnlyNote.className = 'var-readonly-note';
readOnlyNote.textContent = 'read-only';
into.appendChild(readOnlyNote);
} else if (scopeInfo?.type === 'runtime') {
} else if (scopeInfo.type === 'runtime') {
const readOnlyNote = document.createElement('div');
readOnlyNote.className = 'var-readonly-note';
readOnlyNote.textContent = 'Set by scripts (read-only)';
into.appendChild(readOnlyNote);
} else if (scopeInfo?.type === 'undefined') {
} else if (scopeInfo.type === 'undefined') {
const readOnlyNote = document.createElement('div');
readOnlyNote.className = 'var-readonly-note';
readOnlyNote.textContent = 'No active environment';
@@ -555,7 +581,7 @@ if (!SERVER_RENDERED) {
const target = e.target || e.srcElement;
// Prevent new tooltips if one is already active
if (target.nodeName !== 'SPAN' || state.hoverTimeout !== undefined || activePopup !== null) {
if (target.nodeName !== 'SPAN' || state.hoverTimeout !== undefined) {
return;
}
// Show popover for both valid and invalid variables
@@ -599,8 +625,51 @@ if (!SERVER_RENDERED) {
const state = cm.state.brunoVarInfo;
const options = state.options;
const token = cm.getTokenAt(pos, true);
let token = cm.getTokenAt(pos, true);
if (token) {
const line = cm.getLine(pos.line);
// Find the opening {{ before the cursor
let start = token.start;
while (start > 0 && !line.substring(start - 2, start).includes('{{')) {
// Stop if we encounter }} - we've gone past the start of our variable
if (line.substring(start - 2, start) === '}}') {
break;
}
start--;
}
if (line.substring(start - 2, start) === '{{') {
start = start - 2;
}
// Find the closing }} after the cursor
let end = token.end;
while (end < line.length && !line.substring(end, end + 2).includes('}}')) {
// Stop if we encounter {{ - we've gone past the end of our variable
if (line.substring(end, end + 2) === '{{') {
break;
}
end++;
}
if (line.substring(end, end + 2) === '}}') {
end = end + 2;
}
// Extract the full variable string including {{ and }}
const fullVariableString = line.substring(start, end);
// Only use the expanded string if it looks like a complete variable
if (fullVariableString.startsWith('{{') && fullVariableString.endsWith('}}')) {
token = {
...token,
string: fullVariableString,
start: start,
end: end
};
}
const brunoVarInfo = renderVarInfo(token, options);
if (brunoVarInfo) {
showPopup(cm, box, brunoVarInfo);

View File

@@ -415,4 +415,53 @@ test.describe('Variable Tooltip', () => {
expect(varValueContent).toContain('secret-key-123');
});
});
test('should handle invalid variable names with warning', async ({ page, createTmpDir }) => {
const collectionName = 'invalid-var-test';
await test.step('Setup collection and request', async () => {
await createCollection(page, collectionName, await createTmpDir('invalid-var-collection'), {
openWithSandboxMode: 'safe'
});
// Create request using utility method
await createRequest(page, 'Invalid Var Test', collectionName);
// Set the URL
await page.locator('.collection-item-name').filter({ hasText: 'Invalid Var Test' }).click();
const urlEditor = page.locator('#request-url .CodeMirror');
await urlEditor.click();
await page.keyboard.type('https://api.example.com');
await page.keyboard.press('Control+s');
});
await test.step('Test invalid variable name with space', async () => {
await page.getByRole('tab', { name: 'Body' }).click();
// Select JSON body mode
await page.locator('.body-mode-selector').click();
await page.locator('.dropdown-item').filter({ hasText: 'JSON' }).click();
const bodyEditor = page.locator('.CodeMirror').last();
await bodyEditor.click();
await bodyEditor.evaluate((el: any) => {
const cm = el.CodeMirror;
cm.setValue('{\n "userId": "{{user id}}"\n}');
});
await page.keyboard.press('Control+s');
// Hover over the invalid variable
await page.mouse.move(0, 0);
const invalidVar = bodyEditor.locator('.cm-variable-invalid, .cm-variable-valid').filter({ hasText: 'user id' }).first();
await invalidVar.hover();
// Verify tooltip shows warning and hides input
const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();
await expect(tooltip).toBeVisible();
await expect(tooltip.locator('.var-name')).toContainText('user id');
await expect(tooltip.locator('.var-warning-note')).toBeVisible();
await expect(tooltip.locator('.var-value-editable-display')).not.toBeVisible();
});
});
});