import { createRequire } from "node:module"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { fetchTelegramChatId } from "./api-fetch.js"; const require = createRequire(import.meta.url); const EnvHttpProxyAgent = require("undici/lib/dispatcher/env-http-proxy-agent.js") as { new (opts?: Record): Record; }; 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, description: string, ): Record | 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) : undefined; } afterEach(() => { vi.unstubAllEnvs(); }); describe("fetchTelegramChatId", () => { const cases = [ { name: "returns stringified id when Telegram getChat succeeds", fetchImpl: vi.fn(async () => ({ ok: true, json: async () => ({ ok: true, result: { id: 12345 } }), })), expected: "12345", }, { name: "returns null when response is not ok", fetchImpl: vi.fn(async () => ({ ok: false, json: async () => ({}), })), 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 () => ({ ok: true, json: async () => ({ ok: true, result: { id: 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 () => ({ ok: true, json: async () => ({ ok: true, result: { id: 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, ); }); }); 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; const httpsProxyAgent = withoutProxyTls[kHttpsProxyAgent] as Record; expect(getOwnSymbolValue(noProxyAgent, "options")?.connect).toEqual( expect.objectContaining(connect), ); expect(getOwnSymbolValue(httpsProxyAgent, "proxy tls settings")).toBeUndefined(); const withProxyTls = new EnvHttpProxyAgent({ connect, proxyTls: connect, }); const httpsProxyAgentWithProxyTls = withProxyTls[kHttpsProxyAgent] as Record< PropertyKey, unknown >; expect(getOwnSymbolValue(httpsProxyAgentWithProxyTls, "proxy tls settings")).toEqual( expect.objectContaining(connect), ); }); }); describe("makeProxyFetch", () => { beforeEach(async () => { vi.resetModules(); proxyMocks.undiciFetch.mockReset(); proxyMocks.proxyAgentSpy.mockClear(); proxyMocks.setGlobalDispatcher.mockClear(); vi.doMock("undici", () => ({ ProxyAgent: proxyMocks.ProxyAgent, fetch: proxyMocks.undiciFetch, setGlobalDispatcher: proxyMocks.setGlobalDispatcher, })); ({ getProxyUrlFromFetch, makeProxyFetch } = await import("./proxy.js")); }); it("uses undici fetch with ProxyAgent dispatcher", async () => { const proxyUrl = "http://proxy.test:8080"; proxyMocks.undiciFetch.mockResolvedValue({ ok: true }); const proxyFetch = makeProxyFetch(proxyUrl); await proxyFetch("https://api.telegram.org/bot123/getMe"); expect(proxyMocks.proxyAgentSpy).toHaveBeenCalledWith(proxyUrl); expect(proxyMocks.undiciFetch).toHaveBeenCalledWith( "https://api.telegram.org/bot123/getMe", expect.objectContaining({ dispatcher: proxyMocks.getLastAgent() }), ); expect(proxyMocks.setGlobalDispatcher).not.toHaveBeenCalled(); }); it("attaches proxy metadata for resolver transport handling", () => { const proxyUrl = "http://proxy.test:8080"; const proxyFetch = makeProxyFetch(proxyUrl); expect(getProxyUrlFromFetch(proxyFetch)).toBe(proxyUrl); }); });