From fabba4d29615472b41f6ac42a17efcd98c3319e6 Mon Sep 17 00:00:00 2001 From: Abhishek S Lal Date: Sat, 4 Apr 2026 14:57:50 +0530 Subject: [PATCH] fix: resolve process.env variables in global environment level (#7600) * feat: enhance environment variable resolution in EnvironmentVariablesTable - Added logic to populate process environment variables from the active workspace when no collection is selected, allowing for proper resolution of {{process.env.X}}. - Updated workspace actions to map scratch collections to their respective workspaces, ensuring that environment variables can be resolved correctly. This improves the user experience by providing access to workspace-specific environment variables in the environment variables table. * refactor: improve state selection and add test IDs for better testing - Refactored the state selection logic in EnvironmentVariablesTable for clarity. - Added data-testid attributes to various components including CollapsibleSection, DotEnvFileDetails, DotEnvRawView, and EnvironmentList to enhance testability. - This change aims to streamline component interactions and facilitate easier testing. * feat: add test IDs to EnvironmentList for improved testability - Introduced data-testid attributes to the EnvironmentList component, enhancing the ability to target elements in tests. - This update includes test IDs for the CollapsibleSection, create .env file button, and the input field for the .env name, facilitating better integration with testing frameworks. * refactor: simplify global environment test setup - Removed unnecessary timeout setting and afterEach cleanup logic from the global environment process.env resolution test. - This change streamlines the test structure, focusing on the core functionality being tested. --- .../EnvironmentVariablesTable/index.js | 10 +++ .../Environments/CollapsibleSection/index.js | 5 +- .../Environments/DotEnvFileDetails/index.js | 1 + .../DotEnvFileEditor/DotEnvRawView.js | 2 +- .../EnvironmentListContent/index.js | 2 +- .../EnvironmentList/index.js | 3 + .../EnvironmentList/index.js | 3 + .../ReduxStore/slices/workspaces/actions.js | 5 ++ .../fixtures/collection/bruno.json | 5 ++ .../fixtures/collection/echo-post.bru | 17 +++++ .../global-env-process-env-resolution.spec.ts | 63 +++++++++++++++++++ .../init-user-data/collection-security.json | 10 +++ .../init-user-data/global-environments.json | 19 ++++++ .../init-user-data/preferences.json | 12 ++++ 14 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 tests/environments/global-env-process-env-resolution/fixtures/collection/bruno.json create mode 100644 tests/environments/global-env-process-env-resolution/fixtures/collection/echo-post.bru create mode 100644 tests/environments/global-env-process-env-resolution/global-env-process-env-resolution.spec.ts create mode 100644 tests/environments/global-env-process-env-resolution/init-user-data/collection-security.json create mode 100644 tests/environments/global-env-process-env-resolution/init-user-data/global-environments.json create mode 100644 tests/environments/global-env-process-env-resolution/init-user-data/preferences.json diff --git a/packages/bruno-app/src/components/EnvironmentVariablesTable/index.js b/packages/bruno-app/src/components/EnvironmentVariablesTable/index.js index 60cdf6279..7a88aefaa 100644 --- a/packages/bruno-app/src/components/EnvironmentVariablesTable/index.js +++ b/packages/bruno-app/src/components/EnvironmentVariablesTable/index.js @@ -45,6 +45,10 @@ const EnvironmentVariablesTable = ({ }) => { const { storedTheme } = useTheme(); const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments); + const activeWorkspace = useSelector((state) => { + const uid = state.workspaces?.activeWorkspaceUid; + return state.workspaces?.workspaces?.find((w) => w.uid === uid); + }); const dispatch = useDispatch(); const tabs = useSelector((state) => state.tabs.tabs); @@ -138,6 +142,12 @@ const EnvironmentVariablesTable = ({ _collection.globalEnvironmentVariables = globalEnvironmentVariables; } + // When collection is null (global/workspace environments), populate process env + // variables from the active workspace so that {{process.env.X}} can resolve + if (!collection && activeWorkspace?.processEnvVariables) { + _collection.workspaceProcessEnvVariables = activeWorkspace.processEnvVariables; + } + const initialValues = useMemo(() => { const vars = environment.variables || []; return [ diff --git a/packages/bruno-app/src/components/Environments/CollapsibleSection/index.js b/packages/bruno-app/src/components/Environments/CollapsibleSection/index.js index 169720b48..05316366d 100644 --- a/packages/bruno-app/src/components/Environments/CollapsibleSection/index.js +++ b/packages/bruno-app/src/components/Environments/CollapsibleSection/index.js @@ -8,11 +8,12 @@ const CollapsibleSection = ({ onToggle, badge, actions, - children + children, + testId }) => { return ( -
+
onViewModeChange?.('raw')} aria-pressed={viewMode === 'raw'} + data-testid="dotenv-view-raw" > Raw diff --git a/packages/bruno-app/src/components/Environments/DotEnvFileEditor/DotEnvRawView.js b/packages/bruno-app/src/components/Environments/DotEnvFileEditor/DotEnvRawView.js index 2570c7378..0041d0e22 100644 --- a/packages/bruno-app/src/components/Environments/DotEnvFileEditor/DotEnvRawView.js +++ b/packages/bruno-app/src/components/Environments/DotEnvFileEditor/DotEnvRawView.js @@ -13,7 +13,7 @@ const DotEnvRawView = ({ }) => { return ( <> -
+
- diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js index 744968fd6..231a301c6 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js @@ -736,6 +736,7 @@ const EnvironmentList = ({ setDotEnvExpanded(!dotEnvExpanded)} badge={dotEnvFiles.length} @@ -744,6 +745,7 @@ const EnvironmentList = ({ className="btn-action" onClick={handleCreateDotEnvInlineClick} title="Create .env file" + data-testid="create-dotenv-file" > @@ -768,6 +770,7 @@ const EnvironmentList = ({ ref={dotEnvInputRef} type="text" className="environment-name-input" + data-testid="dotenv-name-input" value={newDotEnvName} onChange={handleDotEnvNameChange} onKeyDown={handleDotEnvNameKeyDown} diff --git a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js index da240e855..757f9e6ef 100644 --- a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js +++ b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js @@ -731,6 +731,7 @@ const EnvironmentList = ({ setDotEnvExpanded(!dotEnvExpanded)} badge={dotEnvFiles.length} @@ -739,6 +740,7 @@ const EnvironmentList = ({ className="btn-action" onClick={handleCreateDotEnvInlineClick} title="Create .env file" + data-testid="create-dotenv-file" > @@ -763,6 +765,7 @@ const EnvironmentList = ({ ref={dotEnvInputRef} type="text" className="environment-name-input" + data-testid="dotenv-name-input" value={newDotEnvName} onChange={handleDotEnvNameChange} onKeyDown={handleDotEnvNameKeyDown} diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js index e3dcc9c59..e9ab09820 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js @@ -1001,6 +1001,11 @@ export const mountScratchCollection = (workspaceUid) => { brunoConfig }); + // Map scratch collection to workspace so getProcessEnvVars can resolve workspace .env values + if (workspace.pathname) { + await ipcRenderer.invoke('renderer:set-collection-workspace', scratchCollectionUid, workspace.pathname); + } + await dispatch(openScratchCollectionEvent(scratchCollectionUid, tempDirectoryPath, brunoConfig)); dispatch(setWorkspaceScratchCollection({ diff --git a/tests/environments/global-env-process-env-resolution/fixtures/collection/bruno.json b/tests/environments/global-env-process-env-resolution/fixtures/collection/bruno.json new file mode 100644 index 000000000..18f5d859b --- /dev/null +++ b/tests/environments/global-env-process-env-resolution/fixtures/collection/bruno.json @@ -0,0 +1,5 @@ +{ + "version": "1", + "name": "process-env-global-test", + "type": "collection" +} diff --git a/tests/environments/global-env-process-env-resolution/fixtures/collection/echo-post.bru b/tests/environments/global-env-process-env-resolution/fixtures/collection/echo-post.bru new file mode 100644 index 000000000..5183f3d31 --- /dev/null +++ b/tests/environments/global-env-process-env-resolution/fixtures/collection/echo-post.bru @@ -0,0 +1,17 @@ +meta { + name: echo-post + type: http + seq: 1 +} + +post { + url: https://echo.usebruno.com + body: json + auth: none +} + +body:json { + { + "value": "{{testVar}}" + } +} diff --git a/tests/environments/global-env-process-env-resolution/global-env-process-env-resolution.spec.ts b/tests/environments/global-env-process-env-resolution/global-env-process-env-resolution.spec.ts new file mode 100644 index 000000000..e9f543951 --- /dev/null +++ b/tests/environments/global-env-process-env-resolution/global-env-process-env-resolution.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '../../../playwright'; +import { + openCollection, + openEnvironmentSelector, + openRequest, + sendRequest, + expectResponseContains +} from '../../utils/page'; + +test.describe('Global Environment process.env Resolution', () => { + test('should resolve process.env variables referenced in global environment', async ({ + pageWithUserData: page + }) => { + await test.step('Open collection', async () => { + await openCollection(page, 'process-env-global-test'); + }); + + await test.step('Create .env file with variable via UI', async () => { + // Open global environment configuration + await openEnvironmentSelector(page, 'global'); + await page.getByTestId('configure-env').click(); + + // Expand the .env Files section + const dotEnvSection = page.getByTestId('dotenv-files-section'); + await dotEnvSection.waitFor({ state: 'visible' }); + await dotEnvSection.click(); + + // Click + to create a new .env file + await page.getByTestId('create-dotenv-file').click(); + + // Accept the default name (.env) and press Enter + await page.getByTestId('dotenv-name-input').press('Enter'); + await expect(page.getByText('.env file created!')).toBeVisible(); + + // Switch to Raw mode to type the variable + await page.getByTestId('dotenv-view-raw').click(); + + // Type the variable into the raw editor + const rawEditor = page.getByTestId('dotenv-raw-editor').locator('.CodeMirror'); + await rawEditor.click(); + await page.keyboard.type('MY_SECRET=hello-from-dotenv'); + + // Save the .env file + await page.getByTestId('save-dotenv-raw').click(); + }); + + await test.step('Verify global environment is active', async () => { + await expect(page.locator('.current-environment')).toContainText('ProcessEnv Test'); + }); + + await test.step('Open request', async () => { + await openRequest(page, 'process-env-global-test', 'echo-post'); + }); + + await test.step('Send request and verify process.env resolved', async () => { + await sendRequest(page, 200); + }); + + await test.step('Verify response contains resolved value from .env file', async () => { + await expectResponseContains(page, ['hello-from-dotenv']); + }); + }); +}); diff --git a/tests/environments/global-env-process-env-resolution/init-user-data/collection-security.json b/tests/environments/global-env-process-env-resolution/init-user-data/collection-security.json new file mode 100644 index 000000000..89dc2bfff --- /dev/null +++ b/tests/environments/global-env-process-env-resolution/init-user-data/collection-security.json @@ -0,0 +1,10 @@ +{ + "collections": [ + { + "path": "{{collectionPath}}", + "securityConfig": { + "jsSandboxMode": "safe" + } + } + ] +} diff --git a/tests/environments/global-env-process-env-resolution/init-user-data/global-environments.json b/tests/environments/global-env-process-env-resolution/init-user-data/global-environments.json new file mode 100644 index 000000000..6e75d1b64 --- /dev/null +++ b/tests/environments/global-env-process-env-resolution/init-user-data/global-environments.json @@ -0,0 +1,19 @@ +{ + "environments": [ + { + "uid": "pEnvTestGlobalEnvUi01", + "name": "ProcessEnv Test", + "variables": [ + { + "uid": "pEnvTestVarUid0000001", + "name": "testVar", + "value": "{{process.env.MY_SECRET}}", + "type": "text", + "secret": false, + "enabled": true + } + ] + } + ], + "activeGlobalEnvironmentUid": "pEnvTestGlobalEnvUi01" +} diff --git a/tests/environments/global-env-process-env-resolution/init-user-data/preferences.json b/tests/environments/global-env-process-env-resolution/init-user-data/preferences.json new file mode 100644 index 000000000..872cf5312 --- /dev/null +++ b/tests/environments/global-env-process-env-resolution/init-user-data/preferences.json @@ -0,0 +1,12 @@ +{ + "maximized": false, + "lastOpenedCollections": [ + "{{collectionPath}}" + ], + "preferences": { + "onboarding": { + "hasLaunchedBefore": true, + "hasSeenWelcomeModal": true + } + } +}