mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-18 13:30:48 +00:00
tests(feishu): inject client runtime seam
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { FeishuConfig, ResolvedFeishuAccount } from "./types.js";
|
||||
|
||||
const clientCtorMock = vi.hoisted(() => vi.fn());
|
||||
const wsClientCtorMock = vi.hoisted(() =>
|
||||
vi.fn(function wsClientCtor() {
|
||||
return { connected: true };
|
||||
@@ -22,22 +23,6 @@ const mockBaseHttpInstance = vi.hoisted(() => ({
|
||||
head: vi.fn().mockResolvedValue({}),
|
||||
options: vi.fn().mockResolvedValue({}),
|
||||
}));
|
||||
|
||||
vi.mock("@larksuiteoapi/node-sdk", () => ({
|
||||
AppType: { SelfBuild: "self" },
|
||||
Domain: { Feishu: "https://open.feishu.cn", Lark: "https://open.larksuite.com" },
|
||||
LoggerLevel: { info: "info" },
|
||||
Client: vi.fn(),
|
||||
WSClient: wsClientCtorMock,
|
||||
EventDispatcher: vi.fn(),
|
||||
defaultHttpInstance: mockBaseHttpInstance,
|
||||
}));
|
||||
|
||||
vi.mock("https-proxy-agent", () => ({
|
||||
HttpsProxyAgent: httpsProxyAgentCtorMock,
|
||||
}));
|
||||
|
||||
import { Client as LarkClient } from "@larksuiteoapi/node-sdk";
|
||||
import {
|
||||
createFeishuClient,
|
||||
createFeishuWSClient,
|
||||
@@ -45,6 +30,7 @@ import {
|
||||
FEISHU_HTTP_TIMEOUT_MS,
|
||||
FEISHU_HTTP_TIMEOUT_MAX_MS,
|
||||
FEISHU_HTTP_TIMEOUT_ENV_VAR,
|
||||
setFeishuClientRuntimeForTest,
|
||||
} from "./client.js";
|
||||
|
||||
const proxyEnvKeys = ["https_proxy", "HTTPS_PROXY", "http_proxy", "HTTP_PROXY"] as const;
|
||||
@@ -78,6 +64,21 @@ beforeEach(() => {
|
||||
delete process.env[key];
|
||||
}
|
||||
vi.clearAllMocks();
|
||||
setFeishuClientRuntimeForTest({
|
||||
sdk: {
|
||||
AppType: { SelfBuild: "self" } as never,
|
||||
Domain: {
|
||||
Feishu: "https://open.feishu.cn",
|
||||
Lark: "https://open.larksuite.com",
|
||||
} as never,
|
||||
LoggerLevel: { info: "info" } as never,
|
||||
Client: clientCtorMock as never,
|
||||
WSClient: wsClientCtorMock as never,
|
||||
EventDispatcher: vi.fn() as never,
|
||||
defaultHttpInstance: mockBaseHttpInstance as never,
|
||||
},
|
||||
HttpsProxyAgent: httpsProxyAgentCtorMock as never,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -94,6 +95,7 @@ afterEach(() => {
|
||||
} else {
|
||||
process.env[FEISHU_HTTP_TIMEOUT_ENV_VAR] = priorFeishuTimeoutEnv;
|
||||
}
|
||||
setFeishuClientRuntimeForTest();
|
||||
});
|
||||
|
||||
describe("createFeishuClient HTTP timeout", () => {
|
||||
@@ -102,7 +104,7 @@ describe("createFeishuClient HTTP timeout", () => {
|
||||
});
|
||||
|
||||
const getLastClientHttpInstance = () => {
|
||||
const calls = (LarkClient as unknown as ReturnType<typeof vi.fn>).mock.calls;
|
||||
const calls = clientCtorMock.mock.calls;
|
||||
const lastCall = calls[calls.length - 1]?.[0] as
|
||||
| { httpInstance?: { get: (...args: unknown[]) => Promise<unknown> } }
|
||||
| undefined;
|
||||
@@ -122,7 +124,7 @@ describe("createFeishuClient HTTP 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 calls = (LarkClient as unknown as ReturnType<typeof vi.fn>).mock.calls;
|
||||
const calls = clientCtorMock.mock.calls;
|
||||
const lastCall = calls[calls.length - 1][0] as { httpInstance?: unknown };
|
||||
expect(lastCall.httpInstance).toBeDefined();
|
||||
});
|
||||
@@ -130,7 +132,7 @@ describe("createFeishuClient HTTP timeout", () => {
|
||||
it("injects default timeout into HTTP request options", async () => {
|
||||
createFeishuClient({ appId: "app_2", appSecret: "secret_2", accountId: "timeout-inject" }); // pragma: allowlist secret
|
||||
|
||||
const calls = (LarkClient as unknown as ReturnType<typeof vi.fn>).mock.calls;
|
||||
const calls = clientCtorMock.mock.calls;
|
||||
const lastCall = calls[calls.length - 1][0] as {
|
||||
httpInstance: { post: (...args: unknown[]) => Promise<unknown> };
|
||||
};
|
||||
@@ -152,7 +154,7 @@ describe("createFeishuClient HTTP timeout", () => {
|
||||
it("allows explicit timeout override per-request", async () => {
|
||||
createFeishuClient({ appId: "app_3", appSecret: "secret_3", accountId: "timeout-override" }); // pragma: allowlist secret
|
||||
|
||||
const calls = (LarkClient as unknown as ReturnType<typeof vi.fn>).mock.calls;
|
||||
const calls = clientCtorMock.mock.calls;
|
||||
const lastCall = calls[calls.length - 1][0] as {
|
||||
httpInstance: { get: (...args: unknown[]) => Promise<unknown> };
|
||||
};
|
||||
@@ -241,7 +243,7 @@ describe("createFeishuClient HTTP timeout", () => {
|
||||
config: { httpTimeoutMs: 45_000 },
|
||||
});
|
||||
|
||||
const calls = (LarkClient as unknown as ReturnType<typeof vi.fn>).mock.calls;
|
||||
const calls = clientCtorMock.mock.calls;
|
||||
expect(calls.length).toBe(2);
|
||||
|
||||
const lastCall = calls[calls.length - 1][0] as {
|
||||
|
||||
@@ -2,6 +2,30 @@ import * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import type { FeishuConfig, FeishuDomain, ResolvedFeishuAccount } from "./types.js";
|
||||
|
||||
type FeishuClientSdk = Pick<
|
||||
typeof Lark,
|
||||
| "AppType"
|
||||
| "Client"
|
||||
| "defaultHttpInstance"
|
||||
| "Domain"
|
||||
| "EventDispatcher"
|
||||
| "LoggerLevel"
|
||||
| "WSClient"
|
||||
>;
|
||||
|
||||
const defaultFeishuClientSdk: FeishuClientSdk = {
|
||||
AppType: Lark.AppType,
|
||||
Client: Lark.Client,
|
||||
defaultHttpInstance: Lark.defaultHttpInstance,
|
||||
Domain: Lark.Domain,
|
||||
EventDispatcher: Lark.EventDispatcher,
|
||||
LoggerLevel: Lark.LoggerLevel,
|
||||
WSClient: Lark.WSClient,
|
||||
};
|
||||
|
||||
let feishuClientSdk: FeishuClientSdk = defaultFeishuClientSdk;
|
||||
let httpsProxyAgentCtor: typeof HttpsProxyAgent = HttpsProxyAgent;
|
||||
|
||||
/** Default HTTP timeout for Feishu API requests (30 seconds). */
|
||||
export const FEISHU_HTTP_TIMEOUT_MS = 30_000;
|
||||
export const FEISHU_HTTP_TIMEOUT_MAX_MS = 300_000;
|
||||
@@ -14,7 +38,7 @@ function getWsProxyAgent(): HttpsProxyAgent<string> | undefined {
|
||||
process.env.http_proxy ||
|
||||
process.env.HTTP_PROXY;
|
||||
if (!proxyUrl) return undefined;
|
||||
return new HttpsProxyAgent(proxyUrl);
|
||||
return new httpsProxyAgentCtor(proxyUrl);
|
||||
}
|
||||
|
||||
// Multi-account client cache
|
||||
@@ -28,10 +52,10 @@ const clientCache = new Map<
|
||||
|
||||
function resolveDomain(domain: FeishuDomain | undefined): Lark.Domain | string {
|
||||
if (domain === "lark") {
|
||||
return Lark.Domain.Lark;
|
||||
return feishuClientSdk.Domain.Lark;
|
||||
}
|
||||
if (domain === "feishu" || !domain) {
|
||||
return Lark.Domain.Feishu;
|
||||
return feishuClientSdk.Domain.Feishu;
|
||||
}
|
||||
return domain.replace(/\/+$/, ""); // Custom URL for private deployment
|
||||
}
|
||||
@@ -42,7 +66,8 @@ function resolveDomain(domain: FeishuDomain | undefined): Lark.Domain | string {
|
||||
* (e.g. when the Feishu API is slow, causing per-chat queue deadlocks).
|
||||
*/
|
||||
function createTimeoutHttpInstance(defaultTimeoutMs: number): Lark.HttpInstance {
|
||||
const base: Lark.HttpInstance = Lark.defaultHttpInstance as unknown as Lark.HttpInstance;
|
||||
const base: Lark.HttpInstance =
|
||||
feishuClientSdk.defaultHttpInstance as unknown as Lark.HttpInstance;
|
||||
|
||||
function injectTimeout<D>(opts?: Lark.HttpRequestOptions<D>): Lark.HttpRequestOptions<D> {
|
||||
return { timeout: defaultTimeoutMs, ...opts } as Lark.HttpRequestOptions<D>;
|
||||
@@ -129,10 +154,10 @@ export function createFeishuClient(creds: FeishuClientCredentials): Lark.Client
|
||||
}
|
||||
|
||||
// Create new client with timeout-aware HTTP instance
|
||||
const client = new Lark.Client({
|
||||
const client = new feishuClientSdk.Client({
|
||||
appId,
|
||||
appSecret,
|
||||
appType: Lark.AppType.SelfBuild,
|
||||
appType: feishuClientSdk.AppType.SelfBuild,
|
||||
domain: resolveDomain(domain),
|
||||
httpInstance: createTimeoutHttpInstance(defaultHttpTimeoutMs),
|
||||
});
|
||||
@@ -158,11 +183,11 @@ export function createFeishuWSClient(account: ResolvedFeishuAccount): Lark.WSCli
|
||||
}
|
||||
|
||||
const agent = getWsProxyAgent();
|
||||
return new Lark.WSClient({
|
||||
return new feishuClientSdk.WSClient({
|
||||
appId,
|
||||
appSecret,
|
||||
domain: resolveDomain(domain),
|
||||
loggerLevel: Lark.LoggerLevel.info,
|
||||
loggerLevel: feishuClientSdk.LoggerLevel.info,
|
||||
...(agent ? { agent } : {}),
|
||||
});
|
||||
}
|
||||
@@ -171,7 +196,7 @@ export function createFeishuWSClient(account: ResolvedFeishuAccount): Lark.WSCli
|
||||
* Create an event dispatcher for an account.
|
||||
*/
|
||||
export function createEventDispatcher(account: ResolvedFeishuAccount): Lark.EventDispatcher {
|
||||
return new Lark.EventDispatcher({
|
||||
return new feishuClientSdk.EventDispatcher({
|
||||
encryptKey: account.encryptKey,
|
||||
verificationToken: account.verificationToken,
|
||||
});
|
||||
@@ -194,3 +219,13 @@ export function clearClientCache(accountId?: string): void {
|
||||
clientCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export function setFeishuClientRuntimeForTest(overrides?: {
|
||||
sdk?: Partial<FeishuClientSdk>;
|
||||
HttpsProxyAgent?: typeof HttpsProxyAgent;
|
||||
}): void {
|
||||
feishuClientSdk = overrides?.sdk
|
||||
? { ...defaultFeishuClientSdk, ...overrides.sdk }
|
||||
: defaultFeishuClientSdk;
|
||||
httpsProxyAgentCtor = overrides?.HttpsProxyAgent ?? HttpsProxyAgent;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const createFeishuClientMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./client.js", () => ({
|
||||
createFeishuClient: createFeishuClientMock,
|
||||
const clientCtorMock = vi.hoisted(() => vi.fn());
|
||||
const mockBaseHttpInstance = vi.hoisted(() => ({
|
||||
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({}),
|
||||
}));
|
||||
|
||||
import { clearClientCache, setFeishuClientRuntimeForTest } from "./client.js";
|
||||
import { FEISHU_PROBE_REQUEST_TIMEOUT_MS, probeFeishu, clearProbeCache } from "./probe.js";
|
||||
|
||||
const DEFAULT_CREDS = { appId: "cli_123", appSecret: "secret" } as const; // pragma: allowlist secret
|
||||
@@ -28,9 +35,15 @@ function makeRequestFn(response: Record<string, unknown>) {
|
||||
return vi.fn().mockResolvedValue(response);
|
||||
}
|
||||
|
||||
function installClientCtor(requestFn: unknown) {
|
||||
clientCtorMock.mockImplementation(function MockFeishuClient(this: { request: unknown }) {
|
||||
this.request = requestFn;
|
||||
} as never);
|
||||
}
|
||||
|
||||
function setupClient(response: Record<string, unknown>) {
|
||||
const requestFn = makeRequestFn(response);
|
||||
createFeishuClientMock.mockReturnValue({ request: requestFn });
|
||||
installClientCtor(requestFn);
|
||||
return requestFn;
|
||||
}
|
||||
|
||||
@@ -60,7 +73,7 @@ async function expectErrorResultCached(params: {
|
||||
expectedError: string;
|
||||
ttlMs: number;
|
||||
}) {
|
||||
createFeishuClientMock.mockReturnValue({ request: params.requestFn });
|
||||
installClientCtor(params.requestFn);
|
||||
|
||||
const first = await probeFeishu(DEFAULT_CREDS);
|
||||
const second = await probeFeishu(DEFAULT_CREDS);
|
||||
@@ -95,11 +108,25 @@ async function readSequentialDefaultProbePair() {
|
||||
describe("probeFeishu", () => {
|
||||
beforeEach(() => {
|
||||
clearProbeCache();
|
||||
vi.restoreAllMocks();
|
||||
clearClientCache();
|
||||
vi.clearAllMocks();
|
||||
setFeishuClientRuntimeForTest({
|
||||
sdk: {
|
||||
AppType: { SelfBuild: "self" } as never,
|
||||
Domain: {
|
||||
Feishu: "https://open.feishu.cn",
|
||||
Lark: "https://open.larksuite.com",
|
||||
} as never,
|
||||
Client: clientCtorMock as never,
|
||||
defaultHttpInstance: mockBaseHttpInstance as never,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearProbeCache();
|
||||
clearClientCache();
|
||||
setFeishuClientRuntimeForTest();
|
||||
});
|
||||
|
||||
it("returns error when credentials are missing", async () => {
|
||||
@@ -141,7 +168,7 @@ describe("probeFeishu", () => {
|
||||
it("returns timeout error when request exceeds timeout", async () => {
|
||||
await withFakeTimers(async () => {
|
||||
const requestFn = vi.fn().mockImplementation(() => new Promise(() => {}));
|
||||
createFeishuClientMock.mockReturnValue({ request: requestFn });
|
||||
installClientCtor(requestFn);
|
||||
|
||||
const promise = probeFeishu(DEFAULT_CREDS, { timeoutMs: 1_000 });
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
@@ -152,7 +179,6 @@ describe("probeFeishu", () => {
|
||||
});
|
||||
|
||||
it("returns aborted when abort signal is already aborted", async () => {
|
||||
createFeishuClientMock.mockClear();
|
||||
const abortController = new AbortController();
|
||||
abortController.abort();
|
||||
|
||||
@@ -162,7 +188,7 @@ describe("probeFeishu", () => {
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({ ok: false, error: "probe aborted" });
|
||||
expect(createFeishuClientMock).not.toHaveBeenCalled();
|
||||
expect(clientCtorMock).not.toHaveBeenCalled();
|
||||
});
|
||||
it("returns cached result on subsequent calls within TTL", async () => {
|
||||
const requestFn = setupSuccessClient();
|
||||
|
||||
Reference in New Issue
Block a user