mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-28 15:14:06 +00:00
fix: variable name validation in brunoVarInfo (#6203)
* fix: variable name validation in brunoVarInfo
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user