import { afterEach, describe, expect, it, vi } from "vitest"; import { resolveTelegramTransport, shouldRetryTelegramIpv4Fallback } from "../telegram/fetch.js"; import { fetchRemoteMedia } from "./fetch.js"; const undiciMocks = vi.hoisted(() => { const createDispatcherCtor = | string>() => vi.fn(function MockDispatcher(this: { options?: T }, options?: T) { this.options = options; }); return { fetch: vi.fn(), agentCtor: createDispatcherCtor>(), envHttpProxyAgentCtor: createDispatcherCtor>(), proxyAgentCtor: createDispatcherCtor | string>(), }; }); vi.mock("undici", () => ({ Agent: undiciMocks.agentCtor, EnvHttpProxyAgent: undiciMocks.envHttpProxyAgentCtor, ProxyAgent: undiciMocks.proxyAgentCtor, fetch: undiciMocks.fetch, })); describe("fetchRemoteMedia telegram network policy", () => { type LookupFn = NonNullable[0]["lookupFn"]>; function createTelegramFetchFailedError(code: string): Error { return Object.assign(new TypeError("fetch failed"), { cause: { code }, }); } afterEach(() => { undiciMocks.fetch.mockReset(); undiciMocks.agentCtor.mockClear(); undiciMocks.envHttpProxyAgentCtor.mockClear(); undiciMocks.proxyAgentCtor.mockClear(); vi.unstubAllEnvs(); }); it("preserves Telegram resolver transport policy for file downloads", async () => { const lookupFn = vi.fn(async () => [ { address: "149.154.167.220", family: 4 }, ]) as unknown as LookupFn; undiciMocks.fetch.mockResolvedValueOnce( new Response(new Uint8Array([0xff, 0xd8, 0xff, 0x00]), { status: 200, headers: { "content-type": "image/jpeg" }, }), ); const telegramTransport = resolveTelegramTransport(undefined, { network: { autoSelectFamily: true, dnsResultOrder: "verbatim", }, }); await fetchRemoteMedia({ url: "https://api.telegram.org/file/bottok/photos/1.jpg", fetchImpl: telegramTransport.sourceFetch, dispatcherPolicy: telegramTransport.pinnedDispatcherPolicy, lookupFn, maxBytes: 1024, ssrfPolicy: { allowedHostnames: ["api.telegram.org"], allowRfc2544BenchmarkRange: true, }, }); const init = undiciMocks.fetch.mock.calls[0]?.[1] as | (RequestInit & { dispatcher?: { options?: { connect?: Record; }; }; }) | undefined; expect(init?.dispatcher?.options?.connect).toEqual( expect.objectContaining({ autoSelectFamily: true, autoSelectFamilyAttemptTimeout: 300, lookup: expect.any(Function), }), ); }); it("keeps explicit proxy routing for file downloads", async () => { const { makeProxyFetch } = await import("../telegram/proxy.js"); const lookupFn = vi.fn(async () => [ { address: "149.154.167.220", family: 4 }, ]) as unknown as LookupFn; undiciMocks.fetch.mockResolvedValueOnce( new Response(new Uint8Array([0x25, 0x50, 0x44, 0x46]), { status: 200, headers: { "content-type": "application/pdf" }, }), ); const telegramTransport = resolveTelegramTransport(makeProxyFetch("http://127.0.0.1:7890"), { network: { autoSelectFamily: false, dnsResultOrder: "ipv4first", }, }); await fetchRemoteMedia({ url: "https://api.telegram.org/file/bottok/files/1.pdf", fetchImpl: telegramTransport.sourceFetch, dispatcherPolicy: telegramTransport.pinnedDispatcherPolicy, lookupFn, maxBytes: 1024, ssrfPolicy: { allowedHostnames: ["api.telegram.org"], allowRfc2544BenchmarkRange: true, }, }); const init = undiciMocks.fetch.mock.calls[0]?.[1] as | (RequestInit & { dispatcher?: { options?: { uri?: string; }; }; }) | undefined; expect(init?.dispatcher?.options?.uri).toBe("http://127.0.0.1:7890"); expect(undiciMocks.proxyAgentCtor).toHaveBeenCalled(); }); it("retries Telegram file downloads with IPv4 fallback when the first fetch fails", async () => { const lookupFn = vi.fn(async () => [ { address: "149.154.167.220", family: 4 }, { address: "2001:67c:4e8:f004::9", family: 6 }, ]) as unknown as LookupFn; undiciMocks.fetch .mockRejectedValueOnce(createTelegramFetchFailedError("EHOSTUNREACH")) .mockResolvedValueOnce( new Response(new Uint8Array([0xff, 0xd8, 0xff, 0x00]), { status: 200, headers: { "content-type": "image/jpeg" }, }), ); const telegramTransport = resolveTelegramTransport(undefined, { network: { autoSelectFamily: true, dnsResultOrder: "ipv4first", }, }); await fetchRemoteMedia({ url: "https://api.telegram.org/file/bottok/photos/2.jpg", fetchImpl: telegramTransport.sourceFetch, dispatcherPolicy: telegramTransport.pinnedDispatcherPolicy, fallbackDispatcherPolicy: telegramTransport.fallbackPinnedDispatcherPolicy, shouldRetryFetchError: shouldRetryTelegramIpv4Fallback, lookupFn, maxBytes: 1024, ssrfPolicy: { allowedHostnames: ["api.telegram.org"], allowRfc2544BenchmarkRange: true, }, }); const firstInit = undiciMocks.fetch.mock.calls[0]?.[1] as | (RequestInit & { dispatcher?: { options?: { connect?: Record; }; }; }) | undefined; const secondInit = undiciMocks.fetch.mock.calls[1]?.[1] as | (RequestInit & { dispatcher?: { options?: { connect?: Record; }; }; }) | undefined; expect(undiciMocks.fetch).toHaveBeenCalledTimes(2); expect(firstInit?.dispatcher?.options?.connect).toEqual( expect.objectContaining({ autoSelectFamily: true, autoSelectFamilyAttemptTimeout: 300, lookup: expect.any(Function), }), ); expect(secondInit?.dispatcher?.options?.connect).toEqual( expect.objectContaining({ family: 4, autoSelectFamily: false, lookup: expect.any(Function), }), ); }); it("preserves both primary and fallback errors when Telegram media retry fails twice", async () => { const lookupFn = vi.fn(async () => [ { address: "149.154.167.220", family: 4 }, { address: "2001:67c:4e8:f004::9", family: 6 }, ]) as unknown as LookupFn; const primaryError = createTelegramFetchFailedError("EHOSTUNREACH"); const fallbackError = createTelegramFetchFailedError("ETIMEDOUT"); undiciMocks.fetch.mockRejectedValueOnce(primaryError).mockRejectedValueOnce(fallbackError); const telegramTransport = resolveTelegramTransport(undefined, { network: { autoSelectFamily: true, dnsResultOrder: "ipv4first", }, }); await expect( fetchRemoteMedia({ url: "https://api.telegram.org/file/bottok/photos/3.jpg", fetchImpl: telegramTransport.sourceFetch, dispatcherPolicy: telegramTransport.pinnedDispatcherPolicy, fallbackDispatcherPolicy: telegramTransport.fallbackPinnedDispatcherPolicy, shouldRetryFetchError: shouldRetryTelegramIpv4Fallback, lookupFn, maxBytes: 1024, ssrfPolicy: { allowedHostnames: ["api.telegram.org"], allowRfc2544BenchmarkRange: true, }, }), ).rejects.toMatchObject({ name: "MediaFetchError", code: "fetch_failed", cause: expect.objectContaining({ name: "Error", cause: fallbackError, primaryError, }), }); }); });