From fc985771eae268cc875d7db2348ecd0aa4f853c5 Mon Sep 17 00:00:00 2001 From: Nivedin <53208152+nivedin@users.noreply.github.com> Date: Tue, 11 Nov 2025 20:25:44 +0530 Subject: [PATCH] fix: capture environment before request run (#5560) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com> --- .../src/helpers/RequestRunner.ts | 270 ++++++++++++++---- .../test-runner/test-runner.service.ts | 18 +- 2 files changed, 230 insertions(+), 58 deletions(-) diff --git a/packages/hoppscotch-common/src/helpers/RequestRunner.ts b/packages/hoppscotch-common/src/helpers/RequestRunner.ts index dc1a25ff..063f9b67 100644 --- a/packages/hoppscotch-common/src/helpers/RequestRunner.ts +++ b/packages/hoppscotch-common/src/helpers/RequestRunner.ts @@ -33,6 +33,7 @@ import { getCurrentEnvironment, getEnvironment, getGlobalVariables, + SelectedEnvironmentIndex, setGlobalEnvVariables, updateEnvironment, } from "~/newstore/environments" @@ -81,6 +82,56 @@ const EXPERIMENTAL_SCRIPTING_SANDBOX = useSetting( "EXPERIMENTAL_SCRIPTING_SANDBOX" ) +export type InitialEnvironmentState = { + initialGlobalEnvs: Environment["variables"] + initialEnvID: string + initialSelectedEnvs: Environment["variables"] + initialEnvironmentIndex: SelectedEnvironmentIndex + initialEnvs: TestResult["envs"] & { + temp: Environment["variables"] + } + initialEnvsForComparison: TestResult["envs"] +} + +/** + * Captures the initial environment state before request execution + * So that we can compare and update environment variables after test script execution + * because the current environment can change during the request execution. + * @returns Object containing all initial environment states needed for comparison and updates + */ +export const captureInitialEnvironmentState = (): InitialEnvironmentState => { + // Capture initial environment state before request execution + const initialGlobalEnvs = resolveEnvVars( + "Global", + cloneDeep(getGlobalVariables()) + ) + const { id: initialEnvID, variables: initialEnvVariables } = + getCurrentEnvironment() + + const initialSelectedEnvs = resolveEnvVars(initialEnvID, initialEnvVariables) + + // Capture initial environment index for later use in updateEnvsAfterTestScript + const initialEnvironmentIndex = cloneDeep( + environmentsStore.value.selectedEnvironmentIndex + ) + + // Capture the initial script environment state (the environment passed to scripts) + const initialEnvs = getCombinedEnvVariables() + const initialEnvsForComparison: TestResult["envs"] = { + global: initialEnvs.global, + selected: initialEnvs.selected, + } + + return { + initialGlobalEnvs, + initialEnvID, + initialSelectedEnvs, + initialEnvironmentIndex, + initialEnvs, + initialEnvsForComparison, + } +} + export const getTestableBody = ( res: HoppRESTResponse & { type: "success" | "fail" } ) => { @@ -143,20 +194,16 @@ export const executedResponses$ = new Subject< * and secret environment service. * @param envs The environment variables to update * @param type Whether the environment variables are global or selected + * @param initialEnvID The initial environment ID to use for updates * @returns the updated environment variables */ const updateEnvironments = ( - envs: Environment["variables"] & - { - secret: true - currentValue: string - initialValue: string - key: string - }[], - type: "global" | "selected" + envs: Environment["variables"], + type: "global" | "selected", + initialEnvID?: string ) => { - const currentEnvID = - type === "selected" ? getCurrentEnvironment().id : "Global" + const envID = + type === "selected" ? initialEnvID || getCurrentEnvironment().id : "Global" const updatedSecretEnvironments: SecretVariable[] = [] const nonSecretVariables: Variable[] = [] @@ -172,12 +219,11 @@ const updateEnvironments = ( initialValue: e.initialValue ?? "", }) - // create a new object with cleared values for secret variables - // so that these values don't get saved in the environment + // For secret variables, keep the initialValue but clear currentValue for storage return { key: e.key, secret: e.secret, - initialValue: e.secret ? "" : (e.initialValue ?? ""), + initialValue: e.initialValue ?? "", currentValue: "", } } @@ -188,27 +234,26 @@ const updateEnvironments = ( varIndex: index, currentValue: e.currentValue ?? "", }) - // set the current value as empty string - // so that it doesn't get saved in the environment + + // For non-secret variables, preserve both initialValue and currentValue return { key: e.key, - secret: e.secret, + secret: e.secret ?? false, initialValue: e.initialValue ?? "", - currentValue: "", + currentValue: e.currentValue ?? "", } }) ) - if (currentEnvID) { + + if (envID) { secretEnvironmentService.addSecretEnvironment( - currentEnvID, + envID, updatedSecretEnvironments ) - currentEnvironmentValueService.addEnvironment( - currentEnvID, - nonSecretVariables - ) + currentEnvironmentValueService.addEnvironment(envID, nonSecretVariables) } + return updatedEnv } @@ -439,9 +484,18 @@ export function runRESTRequest$( headers: requestHeaders, } + const { + initialGlobalEnvs, + initialEnvID, + initialSelectedEnvs, + initialEnvironmentIndex, + initialEnvs, + initialEnvsForComparison, + } = captureInitialEnvironmentState() + const res = delegatePreRequestScriptRunner( resolvedRequest, - getCombinedEnvVariables(), + initialEnvs, cookieJarEntries ).then(async (preRequestScriptResult) => { if (cancelCalled) return E.left("cancellation" as const) @@ -541,9 +595,24 @@ export function runRESTRequest$( ) as E.Right tab.value.document.testResults = translateToSandboxTestResults( - combinedResult.right + combinedResult.right, + initialGlobalEnvs, + initialSelectedEnvs ) - updateEnvsAfterTestScript(combinedResult) + + // Check if scripts actually modified environment variables + if ( + hasEnvironmentChanges( + initialEnvsForComparison, // Initial environment when request started + postRequestScriptResult.right.envs // Final script environment after test script execution + ) + ) { + updateEnvsAfterTestScript( + combinedResult, + initialEnvironmentIndex, + initialEnvID + ) + } const updatedCookies = postRequestScriptResult.right.updatedCookies @@ -594,9 +663,12 @@ export function runRESTRequest$( return [cancel, res] } -function updateEnvsAfterTestScript(runResult: E.Right) { +function updateEnvsAfterTestScript( + runResult: E.Right, + initialEnvironmentIndex: SelectedEnvironmentIndex, + initialEnvID?: string +) { const globalEnvVariables = updateEnvironments( - // @ts-expect-error Typescript can't figure out this inference for some reason runResult.right.envs.global, "global" ) @@ -605,38 +677,87 @@ function updateEnvsAfterTestScript(runResult: E.Right) { v: 2, variables: globalEnvVariables, }) + const selectedEnvVariables = updateEnvironments( - // @ts-expect-error Typescript can't figure out this inference for some reason cloneDeep(runResult.right.envs.selected), - "selected" + "selected", + initialEnvID ) - if (environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV") { + + if (initialEnvironmentIndex.type === "MY_ENV") { const env = getEnvironment({ type: "MY_ENV", - index: environmentsStore.value.selectedEnvironmentIndex.index, + index: initialEnvironmentIndex.index, }) - updateEnvironment(environmentsStore.value.selectedEnvironmentIndex.index, { + updateEnvironment(initialEnvironmentIndex.index, { name: env.name, v: 2, id: "id" in env ? env.id : "", variables: selectedEnvVariables, }) - } else if ( - environmentsStore.value.selectedEnvironmentIndex.type === "TEAM_ENV" - ) { + } else if (initialEnvironmentIndex.type === "TEAM_ENV") { const env = getEnvironment({ type: "TEAM_ENV", }) pipe( updateTeamEnvironment( JSON.stringify(selectedEnvVariables), - environmentsStore.value.selectedEnvironmentIndex.teamEnvID, + initialEnvironmentIndex.teamEnvID, env.name ) )() } } +/** + * Checks if there are any changes between two environment states by comparing + * the initial environment state with the final environment state. + * @param initialEnvs The environment state at the start + * @param finalEnvs The environment state after changes + * @returns true if there are any environment changes, false otherwise + */ +const hasEnvironmentChanges = ( + initialEnvs: TestResult["envs"], + finalEnvs: TestResult["envs"] +): boolean => { + // Check global environment changes + const globalAdditions = getAddedEnvVariables( + initialEnvs.global, + finalEnvs.global + ) + const globalDeletions = getRemovedEnvVariables( + initialEnvs.global, + finalEnvs.global + ) + const globalUpdations = getUpdatedEnvVariables( + initialEnvs.global, + finalEnvs.global + ) + + // Check selected environment changes + const selectedAdditions = getAddedEnvVariables( + initialEnvs.selected, + finalEnvs.selected + ) + const selectedDeletions = getRemovedEnvVariables( + initialEnvs.selected, + finalEnvs.selected + ) + const selectedUpdations = getUpdatedEnvVariables( + initialEnvs.selected, + finalEnvs.selected + ) + + return ( + globalAdditions.length > 0 || + globalDeletions.length > 0 || + globalUpdations.length > 0 || + selectedAdditions.length > 0 || + selectedDeletions.length > 0 || + selectedUpdations.length > 0 + ) +} + const getCookieJarEntries = () => { // Exclusive to the Desktop App if (!platform.platformFeatureFlags.cookiesEnabled) { @@ -654,13 +775,16 @@ const getCookieJarEntries = () => { * Run the test runner request * @param request The request to run * @param persistEnv Whether to persist the environment variables after running the test script + * @param inheritedVariables The inherited collection variables from the collection/folder + * @param initialEnvironmentState The initial environment state before collection run execution * @returns The response and the test result */ export function runTestRunnerRequest( request: HoppRESTRequest, persistEnv = true, - inheritedVariables: HoppCollectionVariable[] = [] + inheritedVariables: HoppCollectionVariable[] = [], + initialEnvironmentState: InitialEnvironmentState ): Promise< | E.Left<"script_fail"> | E.Right<{ @@ -672,9 +796,18 @@ export function runTestRunnerRequest( > { const cookieJarEntries = getCookieJarEntries() + const { + initialGlobalEnvs, + initialEnvID, + initialSelectedEnvs, + initialEnvironmentIndex, + initialEnvs, + initialEnvsForComparison, + } = initialEnvironmentState + return delegatePreRequestScriptRunner( request, - getCombinedEnvVariables(), + initialEnvs, cookieJarEntries ).then(async (preRequestScriptResult) => { if (E.isLeft(preRequestScriptResult)) { @@ -747,12 +880,26 @@ export function runTestRunnerRequest( ], } - const sandboxTestResult = - translateToSandboxTestResults(combinedResult) + const sandboxTestResult = translateToSandboxTestResults( + combinedResult, + initialGlobalEnvs, + initialSelectedEnvs + ) // Update the environment variables after running the test script when persistEnv is true. else store the updated environment variables in the store as a temporary variable. if (persistEnv) { - updateEnvsAfterTestScript(postRequestScriptResult) + if ( + hasEnvironmentChanges( + initialEnvsForComparison, // Initial script environment when requests started + postRequestScriptResult.right.envs // Final script environment after test script execution + ) + ) { + updateEnvsAfterTestScript( + postRequestScriptResult, + initialEnvironmentIndex, + initialEnvID + ) + } } else { // Combine global and selected environment changes const allChanges = [ @@ -865,7 +1012,9 @@ const resolveEnvVars = ( }) function translateToSandboxTestResults( - testDesc: SandboxTestResult + testDesc: SandboxTestResult, + initialGlobalEnvs: Environment["variables"], + initialSelectedEnvs: Environment["variables"] ): HoppTestResult { const translateChildTests = (child: TestDescriptor): HoppTestData => { return { @@ -875,11 +1024,6 @@ function translateToSandboxTestResults( } } - const globals = resolveEnvVars("Global", cloneDeep(getGlobalVariables())) - const { id: currentEnvID, variables: currentEnvVariables } = - getCurrentEnvironment() - const envVars = resolveEnvVars(currentEnvID, currentEnvVariables) - return { description: "", expectResults: testDesc.tests.expectResults, @@ -887,14 +1031,32 @@ function translateToSandboxTestResults( scriptError: false, envDiff: { global: { - additions: getAddedEnvVariables(globals, testDesc.envs.global), - deletions: getRemovedEnvVariables(globals, testDesc.envs.global), - updations: getUpdatedEnvVariables(globals, testDesc.envs.global), + additions: getAddedEnvVariables( + initialGlobalEnvs, + testDesc.envs.global + ), + deletions: getRemovedEnvVariables( + initialGlobalEnvs, + testDesc.envs.global + ), + updations: getUpdatedEnvVariables( + initialGlobalEnvs, + testDesc.envs.global + ), }, selected: { - additions: getAddedEnvVariables(envVars, testDesc.envs.selected), - deletions: getRemovedEnvVariables(envVars, testDesc.envs.selected), - updations: getUpdatedEnvVariables(envVars, testDesc.envs.selected), + additions: getAddedEnvVariables( + initialSelectedEnvs, + testDesc.envs.selected + ), + deletions: getRemovedEnvVariables( + initialSelectedEnvs, + testDesc.envs.selected + ), + updations: getUpdatedEnvVariables( + initialSelectedEnvs, + testDesc.envs.selected + ), }, }, consoleEntries: testDesc.consoleEntries, diff --git a/packages/hoppscotch-common/src/services/test-runner/test-runner.service.ts b/packages/hoppscotch-common/src/services/test-runner/test-runner.service.ts index cd9642fd..9e62aadc 100644 --- a/packages/hoppscotch-common/src/services/test-runner/test-runner.service.ts +++ b/packages/hoppscotch-common/src/services/test-runner/test-runner.service.ts @@ -8,7 +8,11 @@ import { Service } from "dioc" import * as E from "fp-ts/Either" import { cloneDeep } from "lodash-es" import { Ref } from "vue" -import { runTestRunnerRequest } from "~/helpers/RequestRunner" +import { + captureInitialEnvironmentState, + InitialEnvironmentState, + runTestRunnerRequest, +} from "~/helpers/RequestRunner" import { HoppTestRunnerDocument, TestRunnerConfig, @@ -178,13 +182,17 @@ export class TestRunnerService extends Service { headers: [...inheritedHeaders, ...request.headers], } + // Capture the initial environment state for a test run so that it remains consistent and unchanged when current environment changes + const initialEnvironmentState = captureInitialEnvironmentState() + await this.runTestRequest( tab, finalRequest, collection, options, currentPath, - inheritedVariables + inheritedVariables, + initialEnvironmentState ) if (options.delay && options.delay > 0) { @@ -275,7 +283,8 @@ export class TestRunnerService extends Service { collection: HoppCollection, options: TestRunnerOptions, path: number[], - inheritedVariables: HoppCollectionVariable[] = [] + inheritedVariables: HoppCollectionVariable[] = [], + initialEnvironmentState: InitialEnvironmentState ) { if (options.stopRef?.value) { throw new Error("Test execution stopped") @@ -291,7 +300,8 @@ export class TestRunnerService extends Service { const results = await runTestRunnerRequest( request, options.keepVariableValues, - inheritedVariables + inheritedVariables, + initialEnvironmentState ) if (options.stopRef?.value) {