From b0405b1e1a28cfab36e0794bd560c0adf3282644 Mon Sep 17 00:00:00 2001 From: Pooja Date: Wed, 26 Nov 2025 00:50:57 +0530 Subject: [PATCH] fix: variable name validation in brunoVarInfo (#6203) * fix: variable name validation in brunoVarInfo --- packages/bruno-app/src/globalStyles.js | 9 +- .../src/utils/codemirror/brunoVarInfo.js | 83 +++++++++++++++++-- .../variable-tooltip/variable-tooltip.spec.ts | 49 +++++++++++ 3 files changed, 133 insertions(+), 8 deletions(-) diff --git a/packages/bruno-app/src/globalStyles.js b/packages/bruno-app/src/globalStyles.js index ce16e4016..3c55175cc 100644 --- a/packages/bruno-app/src/globalStyles.js +++ b/packages/bruno-app/src/globalStyles.js @@ -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; diff --git a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js index 2ae1788ec..22f75f5e8 100644 --- a/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js +++ b/packages/bruno-app/src/utils/codemirror/brunoVarInfo.js @@ -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); diff --git a/tests/variable-tooltip/variable-tooltip.spec.ts b/tests/variable-tooltip/variable-tooltip.spec.ts index d7dce1172..c89eabb47 100644 --- a/tests/variable-tooltip/variable-tooltip.spec.ts +++ b/tests/variable-tooltip/variable-tooltip.spec.ts @@ -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(); + }); + }); });