diff --git a/apps/web/lib/setupGlobalAgentProxy.test.ts b/apps/web/lib/setupGlobalAgentProxy.test.ts new file mode 100644 index 0000000000..6311cdf560 --- /dev/null +++ b/apps/web/lib/setupGlobalAgentProxy.test.ts @@ -0,0 +1,235 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { logger } from "@formbricks/logger"; +import { env } from "./env"; +// Note: We don't mock global-agent because the code uses dynamic require("global" + "-agent") +// which can't be easily mocked. The real module will run in tests, which is acceptable. + +// Import after mocks are set up +import { setupGlobalAgentProxy } from "./setupGlobalAgentProxy"; + +// Mock logger +vi.mock("@formbricks/logger", () => ({ + logger: { + info: vi.fn(), + error: vi.fn(), + }, +})); + +// Mock env +vi.mock("./env", () => ({ + env: { + USE_GLOBAL_AGENT_PROXY: undefined, + GLOBAL_AGENT_NO_PROXY: undefined, + NO_PROXY: undefined, + }, +})); + +describe("setupGlobalAgentProxy", () => { + const originalWindow = globalThis.window; + const originalProcess = globalThis.process; + const originalGlobalAgentInitialized = globalThis.__FORMBRICKS_GLOBAL_AGENT_INITIALIZED; + + beforeEach(() => { + vi.clearAllMocks(); + // Reset global state + delete (globalThis as any).window; + delete (globalThis as any).__FORMBRICKS_GLOBAL_AGENT_INITIALIZED; + // Reset process to valid Node.js + globalThis.process = { + release: { name: "node" }, + versions: { node: "20.0.0" }, + env: {}, + } as any; + // Reset env mocks + vi.mocked(env).USE_GLOBAL_AGENT_PROXY = undefined; + vi.mocked(env).GLOBAL_AGENT_NO_PROXY = undefined; + vi.mocked(env).NO_PROXY = undefined; + }); + + afterEach(() => { + // Restore original values + if (originalWindow !== undefined) { + globalThis.window = originalWindow; + } + if (originalProcess !== undefined) { + globalThis.process = originalProcess; + } + if (originalGlobalAgentInitialized !== undefined) { + globalThis.__FORMBRICKS_GLOBAL_AGENT_INITIALIZED = originalGlobalAgentInitialized; + } else { + delete (globalThis as any).__FORMBRICKS_GLOBAL_AGENT_INITIALIZED; + } + }); + + describe("browser environment", () => { + test("should return early if window is defined", () => { + globalThis.window = {} as any; + + setupGlobalAgentProxy(); + + expect(logger.info).not.toHaveBeenCalled(); + }); + }); + + describe("non-Node environment", () => { + test("should return early if process is undefined", () => { + delete (globalThis as any).process; + + setupGlobalAgentProxy(); + + expect(logger.info).not.toHaveBeenCalled(); + }); + + test("should return early if process.release.name is not 'node'", () => { + globalThis.process = { + release: { name: "deno" }, + versions: { node: "20.0.0" }, + env: {}, + } as any; + + setupGlobalAgentProxy(); + + expect(logger.info).not.toHaveBeenCalled(); + }); + + test("should return early if process.versions.node is undefined", () => { + globalThis.process = { + release: { name: "node" }, + versions: {}, + env: {}, + } as any; + + setupGlobalAgentProxy(); + + expect(logger.info).not.toHaveBeenCalled(); + }); + }); + + describe("idempotent initialization", () => { + test("should return early if already initialized", () => { + globalThis.__FORMBRICKS_GLOBAL_AGENT_INITIALIZED = true; + vi.mocked(env).USE_GLOBAL_AGENT_PROXY = "1"; + + setupGlobalAgentProxy(); + + expect(logger.info).not.toHaveBeenCalled(); + }); + }); + + describe("opt-in flag", () => { + test("should return early if USE_GLOBAL_AGENT_PROXY is not '1'", () => { + vi.mocked(env).USE_GLOBAL_AGENT_PROXY = "0"; + + setupGlobalAgentProxy(); + + expect(logger.info).not.toHaveBeenCalled(); + }); + + test("should return early if USE_GLOBAL_AGENT_PROXY is undefined", () => { + vi.mocked(env).USE_GLOBAL_AGENT_PROXY = undefined; + + setupGlobalAgentProxy(); + + expect(logger.info).not.toHaveBeenCalled(); + }); + }); + + describe("NO_PROXY resolution", () => { + test("should use GLOBAL_AGENT_NO_PROXY when set", () => { + vi.mocked(env).USE_GLOBAL_AGENT_PROXY = "1"; + vi.mocked(env).GLOBAL_AGENT_NO_PROXY = "keycloak.local,adfs.local"; + + setupGlobalAgentProxy(); + + expect(process.env.GLOBAL_AGENT_NO_PROXY).toBe("keycloak.local,adfs.local"); + // Verify bootstrap was attempted (either success or error logged) + const infoCalled = vi + .mocked(logger.info) + .mock.calls.some( + (call) => call[0] === "Enabled global-agent proxy support for outbound HTTP requests" + ); + const errorCalled = vi.mocked(logger.error).mock.calls.length > 0; + expect(infoCalled || errorCalled).toBe(true); + }); + + test("should use NO_PROXY when GLOBAL_AGENT_NO_PROXY is not set", () => { + vi.mocked(env).USE_GLOBAL_AGENT_PROXY = "1"; + vi.mocked(env).NO_PROXY = "auth.service.company.local"; + + setupGlobalAgentProxy(); + + expect(process.env.GLOBAL_AGENT_NO_PROXY).toBe("auth.service.company.local"); + // Verify bootstrap was attempted + const infoCalled = vi + .mocked(logger.info) + .mock.calls.some( + (call) => call[0] === "Enabled global-agent proxy support for outbound HTTP requests" + ); + const errorCalled = vi.mocked(logger.error).mock.calls.length > 0; + expect(infoCalled || errorCalled).toBe(true); + }); + + test("should prefer GLOBAL_AGENT_NO_PROXY over NO_PROXY", () => { + vi.mocked(env).USE_GLOBAL_AGENT_PROXY = "1"; + vi.mocked(env).GLOBAL_AGENT_NO_PROXY = "keycloak.local"; + vi.mocked(env).NO_PROXY = "auth.service.company.local"; + + setupGlobalAgentProxy(); + + expect(process.env.GLOBAL_AGENT_NO_PROXY).toBe("keycloak.local"); + // Verify bootstrap was attempted + const infoCalled = vi.mocked(logger.info).mock.calls.length > 0; + const errorCalled = vi.mocked(logger.error).mock.calls.length > 0; + expect(infoCalled || errorCalled).toBe(true); + }); + + test("should not set GLOBAL_AGENT_NO_PROXY when neither is set", () => { + vi.mocked(env).USE_GLOBAL_AGENT_PROXY = "1"; + // eslint-disable-next-line turbo/no-undeclared-env-vars + const originalNoProxy = process.env.GLOBAL_AGENT_NO_PROXY; + // eslint-disable-next-line turbo/no-undeclared-env-vars + delete process.env.GLOBAL_AGENT_NO_PROXY; + + setupGlobalAgentProxy(); + + // eslint-disable-next-line turbo/no-undeclared-env-vars + expect(process.env.GLOBAL_AGENT_NO_PROXY).toBeUndefined(); + // Verify bootstrap was attempted + const infoCalled = vi + .mocked(logger.info) + .mock.calls.some( + (call) => call[0] === "Enabled global-agent proxy support for outbound HTTP requests" + ); + const errorCalled = vi.mocked(logger.error).mock.calls.length > 0; + expect(infoCalled || errorCalled).toBe(true); + + // Restore + if (originalNoProxy !== undefined) { + // eslint-disable-next-line turbo/no-undeclared-env-vars + process.env.GLOBAL_AGENT_NO_PROXY = originalNoProxy; + } + }); + }); + + describe("successful initialization", () => { + test("should attempt to bootstrap global-agent when enabled", () => { + vi.mocked(env).USE_GLOBAL_AGENT_PROXY = "1"; + + setupGlobalAgentProxy(); + + // Verify bootstrap was attempted (either success or error) + const infoCalled = vi + .mocked(logger.info) + .mock.calls.some( + (call) => call[0] === "Enabled global-agent proxy support for outbound HTTP requests" + ); + const errorCalled = vi.mocked(logger.error).mock.calls.length > 0; + expect(infoCalled || errorCalled).toBe(true); + + // If successful, flag should be set + if (infoCalled) { + expect(globalThis.__FORMBRICKS_GLOBAL_AGENT_INITIALIZED).toBe(true); + } + }); + }); +}); diff --git a/apps/web/lib/setupGlobalAgentProxy.ts b/apps/web/lib/setupGlobalAgentProxy.ts index e4e6bf2d0f..6e1340ace1 100644 --- a/apps/web/lib/setupGlobalAgentProxy.ts +++ b/apps/web/lib/setupGlobalAgentProxy.ts @@ -40,15 +40,17 @@ export const setupGlobalAgentProxy = (): void => { process.env.GLOBAL_AGENT_NO_PROXY = noProxy; } + // Mark as initialized before attempting bootstrap to avoid repeated attempts + globalThis.__FORMBRICKS_GLOBAL_AGENT_INITIALIZED = true; + try { // Dynamic require prevents bundling into edge/serverless builds // Using string concatenation to prevent webpack from statically analyzing the require // eslint-disable-next-line @typescript-eslint/no-var-requires, turbo/no-undeclared-env-vars const { bootstrap } = require("global" + "-agent"); bootstrap(); - globalThis.__FORMBRICKS_GLOBAL_AGENT_INITIALIZED = true; logger.info("Enabled global-agent proxy support for outbound HTTP requests"); } catch (error) { - logger.error("Failed to enable global-agent proxy support", error); + logger.error(error, "Failed to enable global-agent proxy support"); } };