mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-11 04:16:03 +00:00
* fix(telegram): bound getChat Bot API response reads * fix(telegram): repair getChat bound-read type and proof script lint --------- Co-authored-by: NIO <nocodet@mail.com>
235 lines
6.9 KiB
TypeScript
235 lines
6.9 KiB
TypeScript
// Telegram tests cover api fetch plugin behavior.
|
|
import { createRequire } from "node:module";
|
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { fetchTelegramChatId } from "./api-fetch.js";
|
|
|
|
const TELEGRAM_GETCHAT_JSON_CAP_BYTES = 4 * 1024 * 1024;
|
|
|
|
function getChatOkResponse(id: number | string): Response {
|
|
return new Response(JSON.stringify({ ok: true, result: { id } }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
function oversizedTelegramGetChatJsonResponse(onCancel: () => void): Response {
|
|
const response = new Response(
|
|
new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(new Uint8Array(TELEGRAM_GETCHAT_JSON_CAP_BYTES + 1));
|
|
},
|
|
cancel() {
|
|
onCancel();
|
|
},
|
|
}),
|
|
{ headers: { "content-type": "application/json" }, status: 200 },
|
|
);
|
|
Object.defineProperty(response, "json", {
|
|
value: async () => {
|
|
throw new Error("unbounded json reader was used");
|
|
},
|
|
});
|
|
return response;
|
|
}
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const EnvHttpProxyAgent = require("undici/lib/dispatcher/env-http-proxy-agent.js") as {
|
|
new (opts?: Record<string, unknown>): Record<PropertyKey, unknown>;
|
|
};
|
|
const { kHttpsProxyAgent, kNoProxyAgent } = require("undici/lib/core/symbols.js") as {
|
|
kHttpsProxyAgent: symbol;
|
|
kNoProxyAgent: symbol;
|
|
};
|
|
const proxyMocks = vi.hoisted(() => {
|
|
const undiciFetch = vi.fn();
|
|
const proxyAgentSpy = vi.fn();
|
|
const setGlobalDispatcher = vi.fn();
|
|
class ProxyAgent {
|
|
static lastCreated: ProxyAgent | undefined;
|
|
proxyUrl: string;
|
|
constructor(proxyUrl: string) {
|
|
this.proxyUrl = proxyUrl;
|
|
ProxyAgent.lastCreated = this;
|
|
proxyAgentSpy(proxyUrl);
|
|
}
|
|
}
|
|
|
|
return {
|
|
ProxyAgent,
|
|
undiciFetch,
|
|
proxyAgentSpy,
|
|
setGlobalDispatcher,
|
|
getLastAgent: () => ProxyAgent.lastCreated,
|
|
};
|
|
});
|
|
|
|
let getProxyUrlFromFetch: typeof import("./proxy.js").getProxyUrlFromFetch;
|
|
let makeProxyFetch: typeof import("./proxy.js").makeProxyFetch;
|
|
|
|
function getOwnSymbolValue(
|
|
target: Record<PropertyKey, unknown>,
|
|
description: string,
|
|
): Record<string, unknown> | undefined {
|
|
const symbol = Object.getOwnPropertySymbols(target).find(
|
|
(entry) => entry.description === description,
|
|
);
|
|
const value = symbol ? target[symbol] : undefined;
|
|
return value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
vi.mock("undici", async () => {
|
|
const actual = await vi.importActual<typeof import("undici")>("undici");
|
|
return {
|
|
...actual,
|
|
ProxyAgent: proxyMocks.ProxyAgent,
|
|
fetch: proxyMocks.undiciFetch,
|
|
setGlobalDispatcher: proxyMocks.setGlobalDispatcher,
|
|
};
|
|
});
|
|
|
|
describe("fetchTelegramChatId", () => {
|
|
const cases = [
|
|
{
|
|
name: "returns stringified id when Telegram getChat succeeds",
|
|
fetchImpl: vi.fn(async () => getChatOkResponse(12345)),
|
|
expected: "12345",
|
|
},
|
|
{
|
|
name: "returns null when response is not ok",
|
|
fetchImpl: vi.fn(async () => new Response("{}", { status: 404 })),
|
|
expected: null,
|
|
},
|
|
{
|
|
name: "returns null on transport failures",
|
|
fetchImpl: vi.fn(async () => {
|
|
throw new Error("network failed");
|
|
}),
|
|
expected: null,
|
|
},
|
|
] as const;
|
|
|
|
for (const testCase of cases) {
|
|
it(testCase.name, async () => {
|
|
vi.stubGlobal("fetch", testCase.fetchImpl);
|
|
|
|
const id = await fetchTelegramChatId({
|
|
token: "abc",
|
|
chatId: "@user",
|
|
});
|
|
|
|
expect(id).toBe(testCase.expected);
|
|
});
|
|
}
|
|
|
|
it("calls Telegram getChat endpoint", async () => {
|
|
const fetchMock = vi.fn(async () => getChatOkResponse(12345));
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
await fetchTelegramChatId({ token: "abc", chatId: "@user" });
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
"https://api.telegram.org/botabc/getChat?chat_id=%40user",
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
it("uses caller-provided fetch impl when present", async () => {
|
|
const customFetch = vi.fn(async () => getChatOkResponse(12345));
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => {
|
|
throw new Error("global fetch should not be called");
|
|
}),
|
|
);
|
|
|
|
await fetchTelegramChatId({
|
|
token: "abc",
|
|
chatId: "@user",
|
|
fetchImpl: customFetch as unknown as typeof fetch,
|
|
});
|
|
|
|
expect(customFetch).toHaveBeenCalledWith(
|
|
"https://api.telegram.org/botabc/getChat?chat_id=%40user",
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
it("returns null for oversized getChat JSON responses and cancels the stream", async () => {
|
|
let cancelCount = 0;
|
|
const fetchImpl = vi.fn(async () =>
|
|
oversizedTelegramGetChatJsonResponse(() => {
|
|
cancelCount += 1;
|
|
}),
|
|
);
|
|
|
|
await expect(
|
|
fetchTelegramChatId({
|
|
token: "abc",
|
|
chatId: "@user",
|
|
fetchImpl: fetchImpl as unknown as typeof fetch,
|
|
}),
|
|
).resolves.toBeNull();
|
|
expect(cancelCount).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("undici env proxy semantics", () => {
|
|
it("uses proxyTls rather than connect for proxied HTTPS transport settings", () => {
|
|
vi.stubEnv("HTTPS_PROXY", "http://127.0.0.1:7890");
|
|
const connect = {
|
|
family: 4,
|
|
autoSelectFamily: false,
|
|
};
|
|
|
|
const withoutProxyTls = new EnvHttpProxyAgent({ connect });
|
|
const noProxyAgent = withoutProxyTls[kNoProxyAgent] as Record<PropertyKey, unknown>;
|
|
const httpsProxyAgent = withoutProxyTls[kHttpsProxyAgent] as Record<PropertyKey, unknown>;
|
|
|
|
const noProxyConnect = getOwnSymbolValue(noProxyAgent, "options")?.connect as
|
|
| { autoSelectFamily?: boolean; family?: number }
|
|
| undefined;
|
|
expect(noProxyConnect?.family).toBe(connect.family);
|
|
expect(noProxyConnect?.autoSelectFamily).toBe(connect.autoSelectFamily);
|
|
expect(getOwnSymbolValue(httpsProxyAgent, "proxy tls settings")).toBeUndefined();
|
|
|
|
const withProxyTls = new EnvHttpProxyAgent({
|
|
connect,
|
|
proxyTls: connect,
|
|
});
|
|
const httpsProxyAgentWithProxyTls = withProxyTls[kHttpsProxyAgent] as Record<
|
|
PropertyKey,
|
|
unknown
|
|
>;
|
|
|
|
const proxyTlsSettings = getOwnSymbolValue(
|
|
httpsProxyAgentWithProxyTls,
|
|
"proxy tls settings",
|
|
) as { autoSelectFamily?: boolean; family?: number } | undefined;
|
|
expect(proxyTlsSettings?.family).toBe(connect.family);
|
|
expect(proxyTlsSettings?.autoSelectFamily).toBe(connect.autoSelectFamily);
|
|
});
|
|
});
|
|
|
|
describe("makeProxyFetch", () => {
|
|
beforeAll(async () => {
|
|
({ getProxyUrlFromFetch, makeProxyFetch } = await import("./proxy.js"));
|
|
});
|
|
|
|
beforeEach(() => {
|
|
proxyMocks.undiciFetch.mockReset();
|
|
proxyMocks.proxyAgentSpy.mockClear();
|
|
proxyMocks.setGlobalDispatcher.mockClear();
|
|
});
|
|
|
|
it("attaches proxy metadata for resolver transport handling", () => {
|
|
const proxyUrl = "http://proxy.test:8080";
|
|
const proxyFetch = makeProxyFetch(proxyUrl);
|
|
|
|
expect(getProxyUrlFromFetch(proxyFetch)).toBe(proxyUrl);
|
|
});
|
|
});
|