mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 00:30:42 +00:00
272 lines
8.6 KiB
TypeScript
272 lines
8.6 KiB
TypeScript
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("@slack/web-api", () => {
|
|
const WebClient = vi.fn(function WebClientMock(
|
|
this: Record<string, unknown>,
|
|
token: string,
|
|
options?: Record<string, unknown>,
|
|
) {
|
|
this.token = token;
|
|
this.options = options;
|
|
});
|
|
return { WebClient };
|
|
});
|
|
|
|
let createSlackWebClient: typeof import("./client.js").createSlackWebClient;
|
|
let createSlackWriteClient: typeof import("./client.js").createSlackWriteClient;
|
|
let createSlackTokenCacheKey: typeof import("./client.js").createSlackTokenCacheKey;
|
|
let getSlackWriteClient: typeof import("./client.js").getSlackWriteClient;
|
|
let clearSlackWriteClientCacheForTest: typeof import("./client.js").clearSlackWriteClientCacheForTest;
|
|
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<typeof vi.fn>;
|
|
|
|
beforeAll(async () => {
|
|
const slackWebApi = await import("@slack/web-api");
|
|
({
|
|
createSlackWebClient,
|
|
createSlackWriteClient,
|
|
createSlackTokenCacheKey,
|
|
getSlackWriteClient,
|
|
clearSlackWriteClientCacheForTest,
|
|
resolveSlackWebClientOptions,
|
|
resolveSlackWriteClientOptions,
|
|
SLACK_DEFAULT_RETRY_OPTIONS,
|
|
SLACK_WRITE_RETRY_OPTIONS,
|
|
} = await import("./client.js"));
|
|
WebClient = slackWebApi.WebClient as unknown as ReturnType<typeof vi.fn>;
|
|
});
|
|
|
|
beforeEach(() => {
|
|
WebClient.mockClear();
|
|
clearSlackWriteClientCacheForTest();
|
|
});
|
|
|
|
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("serializes write client requests by default", () => {
|
|
const options = resolveSlackWriteClientOptions();
|
|
|
|
expect(options.maxRequestConcurrency).toBe(1);
|
|
});
|
|
|
|
it("respects explicit write client concurrency overrides", () => {
|
|
const options = resolveSlackWriteClientOptions({ maxRequestConcurrency: 5 });
|
|
|
|
expect(options.maxRequestConcurrency).toBe(5);
|
|
});
|
|
|
|
it("passes no-retry config into the write client by default", () => {
|
|
createSlackWriteClient("xoxb-test", { timeout: 4321 });
|
|
|
|
expect(WebClient).toHaveBeenCalledWith(
|
|
"xoxb-test",
|
|
expect.objectContaining({
|
|
maxRequestConcurrency: 1,
|
|
timeout: 4321,
|
|
retryConfig: SLACK_WRITE_RETRY_OPTIONS,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("reuses default write clients per token", () => {
|
|
const first = getSlackWriteClient("xoxb-test");
|
|
const second = getSlackWriteClient("xoxb-test");
|
|
|
|
expect(second).toBe(first);
|
|
expect(WebClient).toHaveBeenCalledTimes(1);
|
|
expect(WebClient).toHaveBeenCalledWith(
|
|
"xoxb-test",
|
|
expect.objectContaining({
|
|
maxRequestConcurrency: 1,
|
|
retryConfig: SLACK_WRITE_RETRY_OPTIONS,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("keeps default write clients separated by token", () => {
|
|
const first = getSlackWriteClient("xoxb-one");
|
|
const second = getSlackWriteClient("xoxb-two");
|
|
|
|
expect(second).not.toBe(first);
|
|
expect(WebClient).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("builds stable non-secret token cache keys", () => {
|
|
const token = "xoxb-sensitive-token";
|
|
const first = createSlackTokenCacheKey(token);
|
|
const second = createSlackTokenCacheKey(token);
|
|
|
|
expect(first).toBe(second);
|
|
expect(first).toMatch(/^sha256:/);
|
|
expect(first).not.toContain(token);
|
|
expect(createSlackTokenCacheKey("xoxb-other-token")).not.toBe(first);
|
|
});
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|