import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("@slack/web-api", () => { const WebClient = vi.fn(function WebClientMock( this: Record, token: string, options?: Record, ) { this.token = token; this.options = options; }); return { WebClient }; }); let createSlackWebClient: typeof import("./client.js").createSlackWebClient; let createSlackWriteClient: typeof import("./client.js").createSlackWriteClient; let resolveSlackWebClientOptions: typeof import("./client.js").resolveSlackWebClientOptions; let resolveSlackWriteClientOptions: typeof import("./client.js").resolveSlackWriteClientOptions; let SLACK_DEFAULT_RETRY_OPTIONS: typeof import("./client.js").SLACK_DEFAULT_RETRY_OPTIONS; let SLACK_WRITE_RETRY_OPTIONS: typeof import("./client.js").SLACK_WRITE_RETRY_OPTIONS; let WebClient: ReturnType; beforeAll(async () => { const slackWebApi = await import("@slack/web-api"); ({ createSlackWebClient, createSlackWriteClient, resolveSlackWebClientOptions, resolveSlackWriteClientOptions, SLACK_DEFAULT_RETRY_OPTIONS, SLACK_WRITE_RETRY_OPTIONS, } = await import("./client.js")); WebClient = slackWebApi.WebClient as unknown as ReturnType; }); beforeEach(() => { WebClient.mockClear(); }); describe("slack web client config", () => { it("applies the default retry config when none is provided", () => { const options = resolveSlackWebClientOptions(); expect(options.retryConfig).toEqual(SLACK_DEFAULT_RETRY_OPTIONS); }); it("respects explicit retry config overrides", () => { const customRetry = { retries: 0 }; const options = resolveSlackWebClientOptions({ retryConfig: customRetry }); expect(options.retryConfig).toBe(customRetry); }); it("passes merged options into WebClient", () => { createSlackWebClient("xoxb-test", { timeout: 1234 }); expect(WebClient).toHaveBeenCalledWith( "xoxb-test", expect.objectContaining({ timeout: 1234, retryConfig: SLACK_DEFAULT_RETRY_OPTIONS, }), ); }); it("applies the write retry config when none is provided", () => { const options = resolveSlackWriteClientOptions(); expect(options.retryConfig).toEqual(SLACK_WRITE_RETRY_OPTIONS); }); it("passes no-retry config into the write client by default", () => { createSlackWriteClient("xoxb-test", { timeout: 4321 }); expect(WebClient).toHaveBeenCalledWith( "xoxb-test", expect.objectContaining({ timeout: 4321, retryConfig: SLACK_WRITE_RETRY_OPTIONS, }), ); }); }); describe("slack proxy agent", () => { const originalEnv = { ...process.env }; const PROXY_KEYS = [ "HTTPS_PROXY", "HTTP_PROXY", "https_proxy", "http_proxy", "NO_PROXY", "no_proxy", ]; beforeEach(() => { for (const key of PROXY_KEYS) { delete process.env[key]; } }); afterEach(() => { for (const key of PROXY_KEYS) { if (originalEnv[key] !== undefined) { process.env[key] = originalEnv[key]; } else { delete process.env[key]; } } }); it("sets agent from HTTPS_PROXY env var", () => { process.env.HTTPS_PROXY = "http://proxy.example.com:3128"; const options = resolveSlackWebClientOptions(); expect(options.agent).toBeDefined(); expect(options.agent!.constructor.name).toBe("HttpsProxyAgent"); }); it("falls back to HTTP_PROXY when HTTPS_PROXY is not set", () => { process.env.HTTP_PROXY = "http://proxy.example.com:3128"; const options = resolveSlackWebClientOptions(); expect(options.agent).toBeDefined(); }); it("does not set agent when no proxy env var is configured", () => { const options = resolveSlackWebClientOptions(); expect(options.agent).toBeUndefined(); }); it("does not override an explicitly provided agent", () => { process.env.HTTPS_PROXY = "http://proxy.example.com:3128"; const customAgent = {} as never; const options = resolveSlackWebClientOptions({ agent: customAgent }); expect(options.agent).toBe(customAgent); }); it("prefers lowercase https_proxy over uppercase", () => { process.env.https_proxy = "http://lower.example.com:3128"; process.env.HTTPS_PROXY = "http://upper.example.com:3128"; const options = resolveSlackWebClientOptions(); expect(options.agent).toBeDefined(); // HttpsProxyAgent stores the proxy URL — verify it picked the lower-case one expect((options.agent as unknown as { proxy: { href: string } }).proxy.href).toContain( "lower.example.com", ); }); it("treats empty lowercase https_proxy as authoritative over uppercase", () => { process.env.https_proxy = ""; process.env.HTTPS_PROXY = "http://upper.example.com:3128"; const options = resolveSlackWebClientOptions(); expect(options.agent).toBeUndefined(); }); it("also applies proxy agent to write client options", () => { process.env.HTTPS_PROXY = "http://proxy.example.com:3128"; const options = resolveSlackWriteClientOptions(); expect(options.agent).toBeDefined(); expect(options.agent!.constructor.name).toBe("HttpsProxyAgent"); }); it("respects NO_PROXY excluding slack.com", () => { process.env.HTTPS_PROXY = "http://proxy.example.com:3128"; process.env.NO_PROXY = "localhost,slack.com,.internal.corp"; const options = resolveSlackWebClientOptions(); expect(options.agent).toBeUndefined(); }); it("respects no_proxy (lowercase) excluding .slack.com", () => { process.env.HTTPS_PROXY = "http://proxy.example.com:3128"; process.env.no_proxy = ".slack.com"; const options = resolveSlackWebClientOptions(); expect(options.agent).toBeUndefined(); }); it("respects space-separated no_proxy entries", () => { process.env.HTTPS_PROXY = "http://proxy.example.com:3128"; process.env.no_proxy = "localhost *.slack.com"; const options = resolveSlackWebClientOptions(); expect(options.agent).toBeUndefined(); }); it("respects NO_PROXY wildcard", () => { process.env.HTTPS_PROXY = "http://proxy.example.com:3128"; process.env.NO_PROXY = "*"; const options = resolveSlackWebClientOptions(); expect(options.agent).toBeUndefined(); }); it("does not skip proxy when NO_PROXY excludes unrelated hosts", () => { process.env.HTTPS_PROXY = "http://proxy.example.com:3128"; process.env.NO_PROXY = "localhost,.internal.corp"; const options = resolveSlackWebClientOptions(); expect(options.agent).toBeDefined(); }); it("degrades gracefully on malformed proxy URL", () => { process.env.HTTPS_PROXY = "not-a-valid-url://:::bad"; const options = resolveSlackWebClientOptions(); // Should not throw; falls back to no agent expect(options.agent).toBeUndefined(); }); });