mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
fix: validate environment variables in unsaved changes dialog (#7403)
This commit is contained in:
committed by
GitHub
parent
5db34dff11
commit
073b1ef036
@@ -22,6 +22,7 @@ import NewRequest from 'components/Sidebar/NewRequest/index';
|
||||
import GradientCloseButton from './GradientCloseButton';
|
||||
import { flattenItems } from 'utils/collections/index';
|
||||
import { closeWsConnection } from 'utils/network/index';
|
||||
import { getInvalidVariableNames } from 'utils/common/variables';
|
||||
import ExampleTab from '../ExampleTab';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -356,6 +357,11 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
window.addEventListener('dotenv-save-failed', onFailed, { once: true });
|
||||
window.dispatchEvent(new Event('dotenv-save'));
|
||||
} else if (draft?.environmentUid && draft?.variables) {
|
||||
const invalidNames = getInvalidVariableNames(draft.variables);
|
||||
if (invalidNames.length > 0) {
|
||||
toast.error(`Invalid variable name(s): ${invalidNames.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
dispatch(saveEnvironment(draft.variables, draft.environmentUid, collection.uid))
|
||||
.then(() => {
|
||||
dispatch(clearEnvironmentsDraft({ collectionUid: collection.uid }));
|
||||
@@ -402,6 +408,11 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
window.addEventListener('dotenv-save-failed', onFailed, { once: true });
|
||||
window.dispatchEvent(new Event('dotenv-save'));
|
||||
} else if (draft?.environmentUid && draft?.variables) {
|
||||
const invalidNames = getInvalidVariableNames(draft.variables);
|
||||
if (invalidNames.length > 0) {
|
||||
toast.error(`Invalid variable name(s): ${invalidNames.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
dispatch(saveGlobalEnvironment({ variables: draft.variables, environmentUid: draft.environmentUid }))
|
||||
.then(() => {
|
||||
dispatch(clearGlobalEnvironmentDraft());
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { findCollectionByUid, flattenItems, isItemARequest, hasRequestChanges, findEnvironmentInCollection } from 'utils/collections';
|
||||
import { pluralizeWord } from 'utils/common';
|
||||
import { getInvalidVariableNames } from 'utils/common/variables';
|
||||
import { completeQuitFlow } from 'providers/ReduxStore/slices/app';
|
||||
import { saveMultipleRequests, saveMultipleCollections, saveMultipleFolders, saveEnvironment, closeTabs } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveGlobalEnvironment, clearGlobalEnvironmentDraft } from 'providers/ReduxStore/slices/global-environments';
|
||||
@@ -13,6 +14,7 @@ import { deleteRequestDraft, deleteCollectionDraft, deleteFolderDraft, clearEnvi
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
import Button from 'ui/Button';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const SaveRequestsModal = ({ onClose, forceCloseTabs = false, tabUidsToClose = [] }) => {
|
||||
const MAX_UNSAVED_ITEMS_TO_SHOW = 5;
|
||||
@@ -166,14 +168,27 @@ const SaveRequestsModal = ({ onClose, forceCloseTabs = false, tabUidsToClose = [
|
||||
await dispatch(saveMultipleRequests(requestDrafts));
|
||||
}
|
||||
|
||||
// Save all collection environment drafts
|
||||
for (const draft of collectionEnvironmentDrafts) {
|
||||
await dispatch(saveEnvironment(draft.variables, draft.environmentUid, draft.collectionUid));
|
||||
// Save environment drafts, skipping any with invalid variable names
|
||||
const allEnvironmentDrafts = [...collectionEnvironmentDrafts, ...globalEnvironmentDrafts];
|
||||
let hasSkippedEnvs = false;
|
||||
|
||||
for (const draft of allEnvironmentDrafts) {
|
||||
const invalidNames = getInvalidVariableNames(draft.variables);
|
||||
if (invalidNames.length > 0) {
|
||||
hasSkippedEnvs = true;
|
||||
toast.error(`Cannot save "${draft.name}": invalid variable name(s) — ${invalidNames.join(', ')}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (draft.type === 'collection-environment') {
|
||||
await dispatch(saveEnvironment(draft.variables, draft.environmentUid, draft.collectionUid));
|
||||
} else {
|
||||
await dispatch(saveGlobalEnvironment({ variables: draft.variables, environmentUid: draft.environmentUid }));
|
||||
}
|
||||
}
|
||||
|
||||
// Save all global environment drafts
|
||||
for (const draft of globalEnvironmentDrafts) {
|
||||
await dispatch(saveGlobalEnvironment({ variables: draft.variables, environmentUid: draft.environmentUid }));
|
||||
if (hasSkippedEnvs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (forceCloseTabs) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
|
||||
import { sanitizeName, validateName } from './regex';
|
||||
import { hasInvalidVariableNames } from './variables';
|
||||
|
||||
describe('regex validators', () => {
|
||||
describe('sanitize name', () => {
|
||||
@@ -163,3 +164,85 @@ describe('sanitizeName and validateName', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasInvalidVariableNames', () => {
|
||||
describe('valid variable names', () => {
|
||||
it('should return false for alphanumeric names with underscores, hyphens, and dots', () => {
|
||||
const validVariables = [
|
||||
{ name: 'valid_name', value: 'test' },
|
||||
{ name: 'valid-name', value: 'test' },
|
||||
{ name: 'valid.name', value: 'test' },
|
||||
{ name: 'validName123', value: 'test' }
|
||||
];
|
||||
expect(hasInvalidVariableNames(validVariables)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty array (no variables to validate)', () => {
|
||||
expect(hasInvalidVariableNames([])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid variable names', () => {
|
||||
it('should return true for variable names with unicode letters (not in \\w)', () => {
|
||||
const variables = [{ name: 'válid_ñame', value: 'test' }];
|
||||
expect(hasInvalidVariableNames(variables)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for variable names containing spaces', () => {
|
||||
const variables = [{ name: 'invalid name', value: 'test' }];
|
||||
expect(hasInvalidVariableNames(variables)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for variable names with special characters', () => {
|
||||
const invalidVariables = [
|
||||
{ name: 'invalid@name', value: 'test' },
|
||||
{ name: 'invalid!name', value: 'test' },
|
||||
{ name: 'invalid#name', value: 'test' },
|
||||
{ name: 'invalid$name', value: 'test' }
|
||||
];
|
||||
invalidVariables.forEach((variable) => {
|
||||
expect(hasInvalidVariableNames([variable])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return true if any variable in array has invalid name', () => {
|
||||
const mixedVariables = [
|
||||
{ name: 'valid_name', value: 'test' },
|
||||
{ name: 'invalid name', value: 'test' },
|
||||
{ name: 'another_valid', value: 'test' }
|
||||
];
|
||||
expect(hasInvalidVariableNames(mixedVariables)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty/placeholder variable names (skipped)', () => {
|
||||
it('should skip variables with empty names (placeholder rows in UI)', () => {
|
||||
const variables = [
|
||||
{ name: '', value: 'test' },
|
||||
{ name: ' ', value: 'test' }
|
||||
];
|
||||
expect(hasInvalidVariableNames(variables)).toBe(false);
|
||||
});
|
||||
|
||||
it('should skip variables without name property', () => {
|
||||
const variables = [
|
||||
{ value: 'test' },
|
||||
{ name: null, value: 'test' }
|
||||
];
|
||||
expect(hasInvalidVariableNames(variables)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defensive handling of invalid inputs', () => {
|
||||
it('should return false for null or undefined (no variables to validate)', () => {
|
||||
expect(hasInvalidVariableNames(null)).toBe(false);
|
||||
expect(hasInvalidVariableNames(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for non-array inputs (defensive check)', () => {
|
||||
expect(hasInvalidVariableNames('string')).toBe(false);
|
||||
expect(hasInvalidVariableNames(123)).toBe(false);
|
||||
expect(hasInvalidVariableNames({})).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
21
packages/bruno-app/src/utils/common/variables.js
Normal file
21
packages/bruno-app/src/utils/common/variables.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { variableNameRegex } from './regex';
|
||||
|
||||
/**
|
||||
* Returns the list of invalid variable names from a variables array.
|
||||
* Skips empty/placeholder names (empty string or whitespace-only).
|
||||
*/
|
||||
export const getInvalidVariableNames = (variables) => {
|
||||
if (!variables || !Array.isArray(variables)) return [];
|
||||
return variables
|
||||
.filter((variable) => variable.name && variable.name.trim() !== '' && !variableNameRegex.test(variable.name))
|
||||
.map((variable) => variable.name);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether any variable in the array has an invalid name.
|
||||
* Uses variableNameRegex from regex.js — names may only contain
|
||||
* word characters (\\w), hyphens, and dots.
|
||||
*/
|
||||
export const hasInvalidVariableNames = (variables) => {
|
||||
return getInvalidVariableNames(variables).length > 0;
|
||||
};
|
||||
Reference in New Issue
Block a user