diff --git a/extensions/bluebubbles/src/attachments.test.ts b/extensions/bluebubbles/src/attachments.test.ts index acf8e9be437..3f695861509 100644 --- a/extensions/bluebubbles/src/attachments.test.ts +++ b/extensions/bluebubbles/src/attachments.test.ts @@ -6,7 +6,6 @@ import { sendBlueBubblesAttachment, } from "./attachments.js"; import { fetchBlueBubblesServerInfo, getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; -import type { PluginRuntime } from "./runtime-api.js"; import { setBlueBubblesRuntime } from "./runtime.js"; import { BLUE_BUBBLES_PRIVATE_API_STATUS, @@ -14,53 +13,27 @@ import { mockBlueBubblesPrivateApiStatus, mockBlueBubblesPrivateApiStatusOnce, } from "./test-harness.js"; +import { + createBlueBubblesFetchRemoteMediaMock, + createBlueBubblesRuntimeStub, +} from "./test-helpers.js"; import type { BlueBubblesAttachment } from "./types.js"; const mockFetch = vi.fn(); const fetchServerInfoMock = vi.mocked(fetchBlueBubblesServerInfo); -const fetchRemoteMediaMock = vi.fn( - async (params: { - url: string; - maxBytes?: number; - fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise; - }) => { - const fetchFn = params.fetchImpl ?? fetch; - const res = await fetchFn(params.url); - if (!res.ok) { - const text = await res.text().catch(() => "unknown"); - throw new Error( - `Failed to fetch media from ${params.url}: HTTP ${res.status}; body: ${text}`, - ); - } - const buffer = Buffer.from(await res.arrayBuffer()); - if (typeof params.maxBytes === "number" && buffer.byteLength > params.maxBytes) { - const error = new Error(`payload exceeds maxBytes ${params.maxBytes}`) as Error & { - code?: string; - }; - error.code = "max_bytes"; - throw error; - } - return { - buffer, - contentType: res.headers.get("content-type") ?? undefined, - fileName: undefined, - }; +const fetchRemoteMediaMock = createBlueBubblesFetchRemoteMediaMock({ + createHttpError: async ({ response, url }) => { + const text = await response.text().catch(() => "unknown"); + return new Error(`Failed to fetch media from ${url}: HTTP ${response.status}; body: ${text}`); }, -); +}); installBlueBubblesFetchTestHooks({ mockFetch, privateApiStatusMock: vi.mocked(getCachedBlueBubblesPrivateApiStatus), }); -const runtimeStub = { - channel: { - media: { - fetchRemoteMedia: - fetchRemoteMediaMock as unknown as PluginRuntime["channel"]["media"]["fetchRemoteMedia"], - }, - }, -} as unknown as PluginRuntime; +const runtimeStub = createBlueBubblesRuntimeStub(fetchRemoteMediaMock); describe("downloadBlueBubblesAttachment", () => { beforeEach(() => { diff --git a/extensions/bluebubbles/src/client.test.ts b/extensions/bluebubbles/src/client.test.ts index 407e2c4e7d6..81bc919d0ff 100644 --- a/extensions/bluebubbles/src/client.test.ts +++ b/extensions/bluebubbles/src/client.test.ts @@ -1,4 +1,3 @@ -import type { SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import "./test-mocks.js"; import { @@ -11,12 +10,15 @@ import { resolveBlueBubblesClientSsrfPolicy, } from "./client.js"; import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; -import type { PluginRuntime } from "./runtime-api.js"; import { setBlueBubblesRuntime } from "./runtime.js"; import { createBlueBubblesFetchGuardPassthroughInstaller, installBlueBubblesFetchTestHooks, } from "./test-harness.js"; +import { + createBlueBubblesFetchRemoteMediaMock, + createBlueBubblesRuntimeStub, +} from "./test-helpers.js"; import type { BlueBubblesAttachment } from "./types.js"; import { _setFetchGuardForTesting } from "./types.js"; @@ -24,47 +26,16 @@ import { _setFetchGuardForTesting } from "./types.js"; const mockFetch = vi.fn(); -const fetchRemoteMediaMock = vi.fn( - async (params: { - url: string; - maxBytes?: number; - ssrfPolicy?: SsrFPolicy; - fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise; - }) => { - const fetchFn = params.fetchImpl ?? fetch; - const res = await fetchFn(params.url); - if (!res.ok) { - throw new Error(`media fetch failed: HTTP ${res.status}`); - } - const buffer = Buffer.from(await res.arrayBuffer()); - if (typeof params.maxBytes === "number" && buffer.byteLength > params.maxBytes) { - const error = new Error(`payload exceeds maxBytes ${params.maxBytes}`) as Error & { - code?: string; - }; - error.code = "max_bytes"; - throw error; - } - return { - buffer, - contentType: res.headers.get("content-type") ?? undefined, - fileName: undefined, - }; - }, -); +const fetchRemoteMediaMock = createBlueBubblesFetchRemoteMediaMock({ + createHttpError: ({ response }) => new Error(`media fetch failed: HTTP ${response.status}`), +}); installBlueBubblesFetchTestHooks({ mockFetch, privateApiStatusMock: vi.mocked(getCachedBlueBubblesPrivateApiStatus), }); -const runtimeStub = { - channel: { - media: { - fetchRemoteMedia: - fetchRemoteMediaMock as unknown as PluginRuntime["channel"]["media"]["fetchRemoteMedia"], - }, - }, -} as unknown as PluginRuntime; +const runtimeStub = createBlueBubblesRuntimeStub(fetchRemoteMediaMock); beforeEach(() => { fetchRemoteMediaMock.mockClear(); diff --git a/extensions/bluebubbles/src/test-helpers.ts b/extensions/bluebubbles/src/test-helpers.ts new file mode 100644 index 00000000000..1f0035ab75b --- /dev/null +++ b/extensions/bluebubbles/src/test-helpers.ts @@ -0,0 +1,52 @@ +import { vi } from "vitest"; +import type { PluginRuntime } from "./runtime-api.js"; + +type FetchRemoteMediaParams = { + url: string; + maxBytes?: number; + ssrfPolicy?: unknown; + fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise; +}; + +type FetchRemoteMediaHttpErrorParams = { + response: Response; + url: string; +}; + +export function createBlueBubblesFetchRemoteMediaMock(options: { + createHttpError: (params: FetchRemoteMediaHttpErrorParams) => Error | Promise; +}) { + return vi.fn(async (params: FetchRemoteMediaParams) => { + const fetchFn = params.fetchImpl ?? fetch; + const res = await fetchFn(params.url); + if (!res.ok) { + throw await options.createHttpError({ response: res, url: params.url }); + } + const buffer = Buffer.from(await res.arrayBuffer()); + if (typeof params.maxBytes === "number" && buffer.byteLength > params.maxBytes) { + const error = new Error(`payload exceeds maxBytes ${params.maxBytes}`) as Error & { + code?: string; + }; + error.code = "max_bytes"; + throw error; + } + return { + buffer, + contentType: res.headers.get("content-type") ?? undefined, + fileName: undefined, + }; + }); +} + +export function createBlueBubblesRuntimeStub( + fetchRemoteMediaMock: ReturnType, +) { + return { + channel: { + media: { + fetchRemoteMedia: + fetchRemoteMediaMock as unknown as PluginRuntime["channel"]["media"]["fetchRemoteMedia"], + }, + }, + } as unknown as PluginRuntime; +}