mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-26 10:41:16 +00:00
* fix(feishu): route sdk http through ambient proxy * fix(feishu): preserve request agents under proxy * fix(feishu): honor managed proxy agent override * fix(feishu): share ambient proxy transport * chore(feishu): move release note to pr --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
650 lines
21 KiB
TypeScript
650 lines
21 KiB
TypeScript
// Feishu tests cover client plugin behavior.
|
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { FEISHU_HTTP_TIMEOUT_MS } from "./client-timeout.js";
|
|
import { FeishuConfigSchema } from "./config-schema.js";
|
|
import type { ResolvedFeishuAccount } from "./types.js";
|
|
|
|
const FEISHU_HTTP_TIMEOUT_ENV_VAR = "OPENCLAW_FEISHU_HTTP_TIMEOUT_MS";
|
|
const FEISHU_HTTP_TIMEOUT_MAX_MS = 300_000;
|
|
|
|
type CreateFeishuClient = typeof import("./client.js").createFeishuClient;
|
|
type CreateFeishuWSClient = typeof import("./client.js").createFeishuWSClient;
|
|
type GetFeishuUserAgent = typeof import("./client.js").getFeishuUserAgent;
|
|
type ResetFeishuProxyAgentForTest = typeof import("./client.js").resetFeishuProxyAgentForTest;
|
|
|
|
const requestInterceptorState = vi.hoisted(() => {
|
|
let registered: ((req: unknown) => unknown) | undefined;
|
|
return {
|
|
get registered() {
|
|
return registered;
|
|
},
|
|
use: vi.fn((fn: (req: unknown) => unknown) => {
|
|
registered = fn;
|
|
}),
|
|
};
|
|
});
|
|
const clientCtorMock = vi.hoisted(() =>
|
|
vi.fn(function clientCtor() {
|
|
return { connected: true };
|
|
}),
|
|
);
|
|
const wsClientCtorMock = vi.hoisted(() =>
|
|
vi.fn(function wsClientCtor() {
|
|
return { connected: true };
|
|
}),
|
|
);
|
|
const proxyAgentInstance = vi.hoisted(() => ({ proxied: true, destroy: vi.fn() }));
|
|
const proxyAgentCtorMock = vi.hoisted(() => vi.fn(() => proxyAgentInstance));
|
|
const mockBaseHttpInstance = vi.hoisted(() => {
|
|
const requestInterceptors = { use: requestInterceptorState.use };
|
|
Object.defineProperty(requestInterceptors, "handlers", {
|
|
configurable: true,
|
|
get() {
|
|
throw new Error("Do not read axios private interceptor handlers");
|
|
},
|
|
set() {
|
|
throw new Error("Do not write axios private interceptor handlers");
|
|
},
|
|
});
|
|
return {
|
|
request: vi.fn().mockResolvedValue({}),
|
|
get: vi.fn().mockResolvedValue({}),
|
|
post: vi.fn().mockResolvedValue({}),
|
|
put: vi.fn().mockResolvedValue({}),
|
|
patch: vi.fn().mockResolvedValue({}),
|
|
delete: vi.fn().mockResolvedValue({}),
|
|
head: vi.fn().mockResolvedValue({}),
|
|
options: vi.fn().mockResolvedValue({}),
|
|
interceptors: {
|
|
request: requestInterceptors,
|
|
},
|
|
};
|
|
});
|
|
const proxyEnvKeys = [
|
|
"https_proxy",
|
|
"HTTPS_PROXY",
|
|
"http_proxy",
|
|
"HTTP_PROXY",
|
|
"OPENCLAW_PROXY_ACTIVE",
|
|
] as const;
|
|
type ProxyEnvKey = (typeof proxyEnvKeys)[number];
|
|
const registerFeishuDocToolsMock = vi.hoisted(() => vi.fn());
|
|
const registerFeishuChatToolsMock = vi.hoisted(() => vi.fn());
|
|
const registerFeishuWikiToolsMock = vi.hoisted(() => vi.fn());
|
|
const registerFeishuDriveToolsMock = vi.hoisted(() => vi.fn());
|
|
const registerFeishuPermToolsMock = vi.hoisted(() => vi.fn());
|
|
const registerFeishuBitableToolsMock = vi.hoisted(() => vi.fn());
|
|
const feishuPluginMock = vi.hoisted(() => ({ id: "feishu-test-plugin" }));
|
|
const setFeishuRuntimeMock = vi.hoisted(() => vi.fn());
|
|
const registerFeishuSubagentHooksMock = vi.hoisted(() => vi.fn());
|
|
|
|
let createFeishuClient: CreateFeishuClient;
|
|
let createFeishuWSClient: CreateFeishuWSClient;
|
|
let getFeishuUserAgent: GetFeishuUserAgent;
|
|
let resetFeishuProxyAgentForTest: ResetFeishuProxyAgentForTest;
|
|
|
|
let priorProxyEnv: Partial<Record<ProxyEnvKey, string | undefined>> = {};
|
|
let priorFeishuTimeoutEnv: string | undefined;
|
|
|
|
function setFeishuTestEnvValue(key: string, value: string | undefined): void {
|
|
if (value === undefined) {
|
|
Reflect.deleteProperty(process.env, key);
|
|
} else {
|
|
Reflect.set(process.env, key, value);
|
|
}
|
|
}
|
|
|
|
vi.mock("./channel.js", () => ({
|
|
feishuPlugin: feishuPluginMock,
|
|
}));
|
|
|
|
vi.mock("./docx.js", () => ({
|
|
registerFeishuDocTools: registerFeishuDocToolsMock,
|
|
}));
|
|
|
|
vi.mock("./chat.js", () => ({
|
|
registerFeishuChatTools: registerFeishuChatToolsMock,
|
|
}));
|
|
|
|
vi.mock("./wiki.js", () => ({
|
|
registerFeishuWikiTools: registerFeishuWikiToolsMock,
|
|
}));
|
|
|
|
vi.mock("./drive.js", () => ({
|
|
registerFeishuDriveTools: registerFeishuDriveToolsMock,
|
|
}));
|
|
|
|
vi.mock("./perm.js", () => ({
|
|
registerFeishuPermTools: registerFeishuPermToolsMock,
|
|
}));
|
|
|
|
vi.mock("./bitable.js", () => ({
|
|
registerFeishuBitableTools: registerFeishuBitableToolsMock,
|
|
}));
|
|
|
|
vi.mock("./runtime.js", () => ({
|
|
setFeishuRuntime: setFeishuRuntimeMock,
|
|
}));
|
|
|
|
vi.mock("./subagent-hooks.js", () => ({
|
|
registerFeishuSubagentHooks: registerFeishuSubagentHooksMock,
|
|
}));
|
|
|
|
const baseAccount: ResolvedFeishuAccount = {
|
|
accountId: "main",
|
|
selectionSource: "explicit",
|
|
enabled: true,
|
|
configured: true,
|
|
appId: "app_123",
|
|
appSecret: "secret_123", // pragma: allowlist secret
|
|
domain: "feishu",
|
|
config: FeishuConfigSchema.parse({}),
|
|
};
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null;
|
|
}
|
|
|
|
type HttpInstanceLike = {
|
|
request: (options?: Record<string, unknown>) => Promise<unknown>;
|
|
get: (url: string, options?: Record<string, unknown>) => Promise<unknown>;
|
|
post: (url: string, body?: unknown, options?: Record<string, unknown>) => Promise<unknown>;
|
|
};
|
|
|
|
function requireHttpInstance(value: unknown): HttpInstanceLike {
|
|
if (
|
|
isRecord(value) &&
|
|
typeof value.request === "function" &&
|
|
typeof value.get === "function" &&
|
|
typeof value.post === "function"
|
|
) {
|
|
return {
|
|
request: value.request as HttpInstanceLike["request"],
|
|
get: value.get as HttpInstanceLike["get"],
|
|
post: value.post as HttpInstanceLike["post"],
|
|
};
|
|
}
|
|
throw new Error("expected Feishu HTTP instance");
|
|
}
|
|
|
|
function readCallOptions(
|
|
mock: { mock: { calls: unknown[][] } },
|
|
index = -1,
|
|
): Record<string, unknown> {
|
|
const call = index < 0 ? mock.mock.calls.at(index)?.[0] : mock.mock.calls[index]?.[0];
|
|
return isRecord(call) ? call : {};
|
|
}
|
|
|
|
function firstWsClientOptions(): {
|
|
agent?: unknown;
|
|
wsConfig?: unknown;
|
|
onError?: unknown;
|
|
onReady?: unknown;
|
|
onReconnected?: unknown;
|
|
onReconnecting?: unknown;
|
|
httpInstance?: unknown;
|
|
} {
|
|
const options = readCallOptions(wsClientCtorMock, 0);
|
|
return {
|
|
agent: options.agent,
|
|
wsConfig: options.wsConfig,
|
|
onError: options.onError,
|
|
onReady: options.onReady,
|
|
onReconnected: options.onReconnected,
|
|
onReconnecting: options.onReconnecting,
|
|
httpInstance: options.httpInstance,
|
|
};
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
vi.doMock("@larksuiteoapi/node-sdk", () => ({
|
|
AppType: { SelfBuild: "self" },
|
|
Domain: { Feishu: "https://open.feishu.cn", Lark: "https://open.larksuite.com" },
|
|
LoggerLevel: { info: "info" },
|
|
Client: clientCtorMock,
|
|
WSClient: wsClientCtorMock,
|
|
EventDispatcher: vi.fn(),
|
|
defaultHttpInstance: mockBaseHttpInstance,
|
|
}));
|
|
vi.doMock("@openclaw/proxyline", () => ({
|
|
createAmbientNodeProxyAgent: proxyAgentCtorMock,
|
|
hasAmbientNodeProxyConfigured: vi.fn(() =>
|
|
Boolean(
|
|
process.env.HTTPS_PROXY ??
|
|
process.env.https_proxy ??
|
|
process.env.HTTP_PROXY ??
|
|
process.env.http_proxy,
|
|
),
|
|
),
|
|
}));
|
|
|
|
({ createFeishuClient, createFeishuWSClient, getFeishuUserAgent, resetFeishuProxyAgentForTest } =
|
|
await import("./client.js"));
|
|
});
|
|
|
|
beforeEach(() => {
|
|
priorProxyEnv = {};
|
|
priorFeishuTimeoutEnv = process.env[FEISHU_HTTP_TIMEOUT_ENV_VAR];
|
|
setFeishuTestEnvValue(FEISHU_HTTP_TIMEOUT_ENV_VAR, undefined);
|
|
for (const key of proxyEnvKeys) {
|
|
priorProxyEnv[key] = process.env[key];
|
|
setFeishuTestEnvValue(key, undefined);
|
|
}
|
|
resetFeishuProxyAgentForTest();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
for (const key of proxyEnvKeys) {
|
|
setFeishuTestEnvValue(key, priorProxyEnv[key]);
|
|
}
|
|
setFeishuTestEnvValue(FEISHU_HTTP_TIMEOUT_ENV_VAR, priorFeishuTimeoutEnv);
|
|
});
|
|
|
|
afterAll(() => {
|
|
resetFeishuProxyAgentForTest();
|
|
vi.doUnmock("./channel.js");
|
|
vi.doUnmock("./docx.js");
|
|
vi.doUnmock("./chat.js");
|
|
vi.doUnmock("./wiki.js");
|
|
vi.doUnmock("./drive.js");
|
|
vi.doUnmock("./perm.js");
|
|
vi.doUnmock("./bitable.js");
|
|
vi.doUnmock("./runtime.js");
|
|
vi.doUnmock("./subagent-hooks.js");
|
|
vi.doUnmock("@larksuiteoapi/node-sdk");
|
|
vi.doUnmock("@openclaw/proxyline");
|
|
vi.resetModules();
|
|
});
|
|
|
|
describe("Feishu default User-Agent interceptor", () => {
|
|
it("registers through the public interceptor API and overrides the SDK User-Agent", () => {
|
|
expect(requestInterceptorState.registered).toBeTypeOf("function");
|
|
|
|
const req = { headers: { "User-Agent": "oapi-node-sdk/1.0.0" } };
|
|
expect(requestInterceptorState.registered?.(req)).toBe(req);
|
|
|
|
expect(req.headers["User-Agent"]).toBe(getFeishuUserAgent());
|
|
});
|
|
|
|
it("sets the User-Agent on AxiosHeaders-like request headers", () => {
|
|
const headers = { set: vi.fn() };
|
|
const req = { headers };
|
|
|
|
expect(requestInterceptorState.registered?.(req)).toBe(req);
|
|
|
|
expect(headers.set).toHaveBeenCalledWith("User-Agent", getFeishuUserAgent());
|
|
});
|
|
});
|
|
|
|
describe("createFeishuClient HTTP timeout", () => {
|
|
const readLastClientHttpInstance = (): HttpInstanceLike =>
|
|
requireHttpInstance(readCallOptions(clientCtorMock).httpInstance);
|
|
|
|
const expectGetCallTimeout = async (timeout: number) => {
|
|
const httpInstance = readLastClientHttpInstance();
|
|
await httpInstance.get("https://example.com/api");
|
|
expect(mockBaseHttpInstance.get).toHaveBeenCalledWith("https://example.com/api", { timeout });
|
|
};
|
|
|
|
it("passes a custom httpInstance with default timeout to Lark.Client", () => {
|
|
createFeishuClient({ appId: "app_1", appSecret: "secret_1", accountId: "timeout-test" }); // pragma: allowlist secret
|
|
|
|
const httpInstance = readLastClientHttpInstance();
|
|
expect(typeof httpInstance.get).toBe("function");
|
|
expect(typeof httpInstance.post).toBe("function");
|
|
});
|
|
|
|
it("injects default timeout into HTTP request options", async () => {
|
|
createFeishuClient({ appId: "app_2", appSecret: "secret_2", accountId: "timeout-inject" }); // pragma: allowlist secret
|
|
|
|
const httpInstance = readLastClientHttpInstance();
|
|
|
|
await httpInstance.post(
|
|
"https://example.com/api",
|
|
{ data: 1 },
|
|
{ headers: { "X-Custom": "yes" } },
|
|
);
|
|
|
|
expect(mockBaseHttpInstance.post).toHaveBeenCalledWith(
|
|
"https://example.com/api",
|
|
{ data: 1 },
|
|
{ timeout: FEISHU_HTTP_TIMEOUT_MS, headers: { "X-Custom": "yes" } },
|
|
);
|
|
});
|
|
|
|
it("allows explicit timeout override per-request", async () => {
|
|
createFeishuClient({ appId: "app_3", appSecret: "secret_3", accountId: "timeout-override" }); // pragma: allowlist secret
|
|
|
|
const httpInstance = readLastClientHttpInstance();
|
|
|
|
await httpInstance.get("https://example.com/api", { timeout: 5_000 });
|
|
|
|
expect(mockBaseHttpInstance.get).toHaveBeenCalledWith("https://example.com/api", {
|
|
timeout: 5_000,
|
|
});
|
|
});
|
|
|
|
it("uses config-configured default timeout when provided", async () => {
|
|
createFeishuClient({
|
|
appId: "app_4",
|
|
appSecret: "secret_4", // pragma: allowlist secret
|
|
accountId: "timeout-config",
|
|
config: { httpTimeoutMs: 45_000 },
|
|
});
|
|
|
|
await expectGetCallTimeout(45_000);
|
|
});
|
|
|
|
it("falls back to default timeout when configured timeout is invalid", async () => {
|
|
createFeishuClient({
|
|
appId: "app_5",
|
|
appSecret: "secret_5", // pragma: allowlist secret
|
|
accountId: "timeout-config-invalid",
|
|
config: { httpTimeoutMs: -1 },
|
|
});
|
|
|
|
await expectGetCallTimeout(FEISHU_HTTP_TIMEOUT_MS);
|
|
});
|
|
|
|
it("uses env timeout override when provided and no direct timeout is set", async () => {
|
|
setFeishuTestEnvValue(FEISHU_HTTP_TIMEOUT_ENV_VAR, "60000");
|
|
|
|
createFeishuClient({
|
|
appId: "app_8",
|
|
appSecret: "secret_8", // pragma: allowlist secret
|
|
accountId: "timeout-env-override",
|
|
config: { httpTimeoutMs: 45_000 },
|
|
});
|
|
|
|
await expectGetCallTimeout(60_000);
|
|
});
|
|
|
|
it("ignores non-decimal env timeout overrides", async () => {
|
|
for (const value of ["0x10", "1e3", "10.5"]) {
|
|
setFeishuTestEnvValue(FEISHU_HTTP_TIMEOUT_ENV_VAR, value);
|
|
|
|
createFeishuClient({
|
|
appId: `app-${value}`,
|
|
appSecret: "secret-env-timeout", // pragma: allowlist secret
|
|
accountId: `timeout-env-invalid-${value}`,
|
|
});
|
|
|
|
await expectGetCallTimeout(FEISHU_HTTP_TIMEOUT_MS);
|
|
mockBaseHttpInstance.get.mockClear();
|
|
}
|
|
});
|
|
|
|
it("prefers direct timeout over env override", async () => {
|
|
setFeishuTestEnvValue(FEISHU_HTTP_TIMEOUT_ENV_VAR, "60000");
|
|
|
|
createFeishuClient({
|
|
appId: "app_10",
|
|
appSecret: "secret_10", // pragma: allowlist secret
|
|
accountId: "timeout-direct-override",
|
|
httpTimeoutMs: 120_000,
|
|
config: { httpTimeoutMs: 45_000 },
|
|
});
|
|
|
|
await expectGetCallTimeout(120_000);
|
|
});
|
|
|
|
it("clamps env timeout override to max bound", async () => {
|
|
setFeishuTestEnvValue(
|
|
FEISHU_HTTP_TIMEOUT_ENV_VAR,
|
|
String(FEISHU_HTTP_TIMEOUT_MAX_MS + 123_456),
|
|
);
|
|
|
|
createFeishuClient({
|
|
appId: "app_9",
|
|
appSecret: "secret_9", // pragma: allowlist secret
|
|
accountId: "timeout-env-clamp",
|
|
});
|
|
|
|
await expectGetCallTimeout(FEISHU_HTTP_TIMEOUT_MAX_MS);
|
|
});
|
|
|
|
it("recreates cached client when configured timeout changes", async () => {
|
|
createFeishuClient({
|
|
appId: "app_6",
|
|
appSecret: "secret_6", // pragma: allowlist secret
|
|
accountId: "timeout-cache-change",
|
|
config: { httpTimeoutMs: 30_000 },
|
|
});
|
|
createFeishuClient({
|
|
appId: "app_6",
|
|
appSecret: "secret_6", // pragma: allowlist secret
|
|
accountId: "timeout-cache-change",
|
|
config: { httpTimeoutMs: 45_000 },
|
|
});
|
|
|
|
expect(clientCtorMock.mock.calls.length).toBe(2);
|
|
const httpInstance = readLastClientHttpInstance();
|
|
await httpInstance.get("https://example.com/api");
|
|
|
|
expect(mockBaseHttpInstance.get).toHaveBeenCalledWith("https://example.com/api", {
|
|
timeout: 45_000,
|
|
});
|
|
});
|
|
|
|
it("uses OpenClaw's ambient proxy agent for Feishu HTTP API requests", async () => {
|
|
process.env.HTTPS_PROXY = "http://upper-https:8002";
|
|
|
|
createFeishuClient({
|
|
appId: "app_11",
|
|
appSecret: "secret_11", // pragma: allowlist secret
|
|
accountId: "http-proxy-agent",
|
|
});
|
|
|
|
const httpInstance = readLastClientHttpInstance();
|
|
await httpInstance.post("https://example.com/api", { data: 1 });
|
|
await httpInstance.get("https://example.com/other");
|
|
|
|
expect(proxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
|
expect(mockBaseHttpInstance.post).toHaveBeenCalledWith(
|
|
"https://example.com/api",
|
|
{ data: 1 },
|
|
{
|
|
timeout: FEISHU_HTTP_TIMEOUT_MS,
|
|
httpAgent: proxyAgentInstance,
|
|
httpsAgent: proxyAgentInstance,
|
|
proxy: false,
|
|
},
|
|
);
|
|
expect(mockBaseHttpInstance.get).toHaveBeenCalledWith("https://example.com/other", {
|
|
timeout: FEISHU_HTTP_TIMEOUT_MS,
|
|
httpAgent: proxyAgentInstance,
|
|
httpsAgent: proxyAgentInstance,
|
|
proxy: false,
|
|
});
|
|
});
|
|
|
|
it("preserves request-level agents while disabling axios ambient proxy handling", async () => {
|
|
process.env.HTTPS_PROXY = "http://upper-https:8002";
|
|
const callerHttpAgent = { caller: "http" };
|
|
const callerHttpsAgent = { caller: "https" };
|
|
|
|
createFeishuClient({
|
|
appId: "app_12",
|
|
appSecret: "secret_12", // pragma: allowlist secret
|
|
accountId: "http-proxy-preserve-agent",
|
|
});
|
|
|
|
const httpInstance = readLastClientHttpInstance();
|
|
await httpInstance.request({
|
|
url: "https://example.com/api",
|
|
httpAgent: callerHttpAgent,
|
|
httpsAgent: callerHttpsAgent,
|
|
});
|
|
|
|
expect(proxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
|
expect(mockBaseHttpInstance.request).toHaveBeenCalledWith({
|
|
url: "https://example.com/api",
|
|
timeout: FEISHU_HTTP_TIMEOUT_MS,
|
|
httpAgent: callerHttpAgent,
|
|
httpsAgent: callerHttpsAgent,
|
|
proxy: false,
|
|
});
|
|
});
|
|
|
|
it("overrides request-level agents while managed proxy mode is active", async () => {
|
|
process.env.HTTPS_PROXY = "http://upper-https:8002";
|
|
process.env.OPENCLAW_PROXY_ACTIVE = "1";
|
|
const callerHttpAgent = { caller: "http" };
|
|
const callerHttpsAgent = { caller: "https" };
|
|
|
|
createFeishuClient({
|
|
appId: "app_13",
|
|
appSecret: "secret_13", // pragma: allowlist secret
|
|
accountId: "http-proxy-managed-agent",
|
|
});
|
|
|
|
const httpInstance = readLastClientHttpInstance();
|
|
await httpInstance.request({
|
|
url: "https://example.com/api",
|
|
httpAgent: callerHttpAgent,
|
|
httpsAgent: callerHttpsAgent,
|
|
});
|
|
|
|
expect(proxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
|
expect(mockBaseHttpInstance.request).toHaveBeenCalledWith({
|
|
url: "https://example.com/api",
|
|
timeout: FEISHU_HTTP_TIMEOUT_MS,
|
|
httpAgent: proxyAgentInstance,
|
|
httpsAgent: proxyAgentInstance,
|
|
proxy: false,
|
|
});
|
|
});
|
|
|
|
it("falls back to Axios ambient proxy handling when agent creation fails", async () => {
|
|
process.env.HTTPS_PROXY = "http://upper-https:8002";
|
|
proxyAgentCtorMock.mockImplementationOnce(() => {
|
|
throw new Error("proxy agent failed");
|
|
});
|
|
|
|
createFeishuClient({
|
|
...baseAccount,
|
|
accountId: "http-proxy-fallback",
|
|
});
|
|
|
|
const httpInstance = readLastClientHttpInstance();
|
|
await httpInstance.request({ url: "https://example.com/api" });
|
|
|
|
expect(mockBaseHttpInstance.request).toHaveBeenCalledWith({
|
|
url: "https://example.com/api",
|
|
timeout: FEISHU_HTTP_TIMEOUT_MS,
|
|
});
|
|
});
|
|
|
|
it("fails closed when managed proxy agent creation fails", async () => {
|
|
process.env.HTTPS_PROXY = "http://upper-https:8002";
|
|
process.env.OPENCLAW_PROXY_ACTIVE = "1";
|
|
proxyAgentCtorMock.mockImplementationOnce(() => {
|
|
throw new Error("proxy agent failed");
|
|
});
|
|
|
|
createFeishuClient({
|
|
...baseAccount,
|
|
accountId: "http-proxy-managed-failure",
|
|
});
|
|
|
|
const httpInstance = readLastClientHttpInstance();
|
|
await expect(httpInstance.request({ url: "https://example.com/api" })).rejects.toThrow(
|
|
"Feishu managed proxy is active but no proxy agent could be created",
|
|
);
|
|
expect(mockBaseHttpInstance.request).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("createFeishuWSClient proxy handling", () => {
|
|
it("passes heartbeat wsConfig defaults to Lark.WSClient", async () => {
|
|
await createFeishuWSClient(baseAccount);
|
|
|
|
const options = firstWsClientOptions();
|
|
expect(options.wsConfig).toEqual({
|
|
pingTimeout: 3,
|
|
});
|
|
});
|
|
|
|
it("passes lifecycle callbacks while preserving heartbeat wsConfig defaults", async () => {
|
|
const onError = vi.fn();
|
|
const onReady = vi.fn();
|
|
const onReconnected = vi.fn();
|
|
const onReconnecting = vi.fn();
|
|
|
|
await createFeishuWSClient(baseAccount, {
|
|
onError,
|
|
onReady,
|
|
onReconnected,
|
|
onReconnecting,
|
|
});
|
|
|
|
const options = firstWsClientOptions();
|
|
expect(options.onError).toBe(onError);
|
|
expect(options.onReady).toBe(onReady);
|
|
expect(options.onReconnected).toBe(onReconnected);
|
|
expect(options.onReconnecting).toBe(onReconnecting);
|
|
expect(options.wsConfig).toEqual({
|
|
pingTimeout: 3,
|
|
});
|
|
});
|
|
|
|
it("passes the proxy-aware HTTP instance to Lark.WSClient bootstrap requests", async () => {
|
|
process.env.HTTPS_PROXY = "http://upper-https:8002";
|
|
|
|
await createFeishuWSClient(baseAccount);
|
|
|
|
const options = firstWsClientOptions();
|
|
const httpInstance = requireHttpInstance(options.httpInstance);
|
|
await httpInstance.request({ url: "https://open.feishu.cn/open-apis/ws/v1" });
|
|
|
|
expect(proxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
|
expect(options.agent).toBe(proxyAgentInstance);
|
|
expect(mockBaseHttpInstance.request).toHaveBeenCalledWith({
|
|
url: "https://open.feishu.cn/open-apis/ws/v1",
|
|
timeout: FEISHU_HTTP_TIMEOUT_MS,
|
|
httpAgent: proxyAgentInstance,
|
|
httpsAgent: proxyAgentInstance,
|
|
proxy: false,
|
|
});
|
|
});
|
|
|
|
it("does not set a ws proxy agent when proxy env is absent", async () => {
|
|
await createFeishuWSClient(baseAccount);
|
|
|
|
expect(proxyAgentCtorMock).not.toHaveBeenCalled();
|
|
const options = firstWsClientOptions();
|
|
expect(options.agent).toBeUndefined();
|
|
});
|
|
|
|
it("creates a ws proxy agent when lowercase https_proxy is set", async () => {
|
|
setFeishuTestEnvValue("https_proxy", "http://lower-https:8001");
|
|
|
|
await createFeishuWSClient(baseAccount);
|
|
|
|
expect(proxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
|
const options = firstWsClientOptions();
|
|
expect(options.agent).toBe(proxyAgentInstance);
|
|
});
|
|
|
|
it("creates a ws proxy agent when uppercase HTTPS_PROXY is set", async () => {
|
|
setFeishuTestEnvValue("HTTPS_PROXY", "http://upper-https:8002");
|
|
|
|
await createFeishuWSClient(baseAccount);
|
|
|
|
expect(proxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
|
const options = firstWsClientOptions();
|
|
expect(options.agent).toBe(proxyAgentInstance);
|
|
});
|
|
|
|
it("falls back to HTTP_PROXY for ws proxy agent creation", async () => {
|
|
setFeishuTestEnvValue("HTTP_PROXY", "http://upper-http:8999");
|
|
|
|
await createFeishuWSClient(baseAccount);
|
|
|
|
expect(proxyAgentCtorMock).toHaveBeenCalledTimes(1);
|
|
const options = firstWsClientOptions();
|
|
expect(options.agent).toBe(proxyAgentInstance);
|
|
});
|
|
});
|