























@@ -1,11 +1,4 @@
1-import * as ssrf from "openclaw/plugin-sdk/infra-runtime";
2-import * as mediaFetch from "openclaw/plugin-sdk/media-runtime";
3-import type { SavedMedia } from "openclaw/plugin-sdk/media-runtime";
4-import * as mediaStore from "openclaw/plugin-sdk/media-runtime";
5-import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
6-import { type FetchMock, withFetchPreconnect } from "openclaw/plugin-sdk/testing";
71import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
8-import { mockPinnedHostnameResolution } from "../../../../src/test-helpers/ssrf.js";
92import {
103fetchWithSlackAuth,
114resolveSlackAttachmentContent,
@@ -14,16 +7,84 @@ import {
147resolveSlackThreadStarter,
158resetSlackThreadStarterCacheForTest,
169} from "./media.js";
17-18-vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
19-logVerbose: vi.fn(),
20-danger: (message: string) => message,
21-shouldLogVerbose: () => false,
10+import type { FetchLike, SavedMedia } from "./media.runtime.js";
11+import * as mediaRuntime from "./media.runtime.js";
12+import { logVerbose } from "./media.runtime.js";
13+14+type FetchMock = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
15+16+const fetchRemoteMediaMock = vi.hoisted(() =>
17+vi.fn(
18+async (params: {
19+url: string;
20+fetchImpl: FetchLike;
21+filePathHint?: string;
22+requestInit?: RequestInit;
23+}) => {
24+let response = await params.fetchImpl(params.url, {
25+ ...params.requestInit,
26+dispatcher: {},
27+} as RequestInit & { dispatcher: unknown });
28+if (response.status >= 300 && response.status < 400) {
29+const location = response.headers.get("location");
30+if (location) {
31+const source = new URL(params.url);
32+const redirect = new URL(location, source);
33+const sameOrigin = redirect.origin === source.origin;
34+response = await params.fetchImpl(redirect.toString(), {
35+ ...(sameOrigin ? params.requestInit : {}),
36+redirect: "follow",
37+dispatcher: {},
38+} as RequestInit & { dispatcher: unknown });
39+}
40+}
41+if (response.status < 200 || response.status >= 300) {
42+throw new Error(`fetch failed: ${response.status}`);
43+}
44+return {
45+buffer: Buffer.from(await response.arrayBuffer()),
46+contentType: response.headers.get("content-type") ?? undefined,
47+fileName: params.filePathHint ?? new URL(params.url).pathname.split("/").at(-1),
48+};
49+},
50+),
51+);
52+const saveMediaBufferMock = vi.hoisted(() =>
53+vi.fn(async (_buffer: Buffer, contentType?: string) => ({
54+id: "saved-media-id",
55+path: "/tmp/test.bin",
56+size: _buffer.byteLength,
57+ contentType,
58+})),
59+);
60+const fetchWithRuntimeDispatcherMock = vi.hoisted(() => vi.fn());
61+const logVerboseMock = vi.hoisted(() => vi.fn());
62+63+vi.mock("./media.runtime.js", () => ({
64+fetchRemoteMedia: fetchRemoteMediaMock,
65+fetchWithRuntimeDispatcher: fetchWithRuntimeDispatcherMock,
66+logVerbose: logVerboseMock,
67+saveMediaBuffer: saveMediaBufferMock,
2268}));
236970+function withFetchPreconnect(fetchMock: ReturnType<typeof vi.fn<FetchMock>>): typeof fetch {
71+return Object.assign(
72+((input: RequestInfo | URL, init?: RequestInit) => fetchMock(input, init)) as typeof fetch,
73+{ mock: fetchMock.mock },
74+);
75+}
76+2477// Store original fetch
2578const originalFetch = globalThis.fetch;
2679let mockFetch: ReturnType<typeof vi.fn<FetchMock>>;
80+81+beforeEach(() => {
82+fetchRemoteMediaMock.mockClear();
83+fetchWithRuntimeDispatcherMock.mockClear();
84+logVerboseMock.mockClear();
85+saveMediaBufferMock.mockClear();
86+});
87+2788const createSavedMedia = (filePath: string, contentType: string): SavedMedia => ({
2889id: "saved-media-id",
2990path: filePath,
@@ -41,7 +102,7 @@ async function expectPrivateDownloadRedirect(params: {
41102redirectedUrl: string;
42103secondAuthorization: string | null;
43104}) {
44-vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue(
105+vi.spyOn(mediaRuntime, "saveMediaBuffer").mockResolvedValue(
45106createSavedMedia("/tmp/test.jpg", "image/jpeg"),
46107);
47108@@ -225,7 +286,6 @@ describe("resolveSlackMedia", () => {
225286beforeEach(() => {
226287mockFetch = vi.fn();
227288globalThis.fetch = mockFetch as unknown as typeof fetch;
228-mockPinnedHostnameResolution();
229289});
230290231291afterEach(() => {
@@ -234,7 +294,7 @@ describe("resolveSlackMedia", () => {
234294});
235295236296it("prefers url_private_download over url_private", async () => {
237-vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue(
297+vi.spyOn(mediaRuntime, "saveMediaBuffer").mockResolvedValue(
238298createSavedMedia("/tmp/test.jpg", "image/jpeg"),
239299);
240300@@ -313,7 +373,7 @@ describe("resolveSlackMedia", () => {
313373});
314374315375it("rejects HTML auth pages for non-HTML files", async () => {
316-const saveMediaBufferMock = vi.spyOn(mediaStore, "saveMediaBuffer");
376+const saveMediaBufferMock = vi.spyOn(mediaRuntime, "saveMediaBuffer");
317377mockFetch.mockResolvedValueOnce(
318378new Response("<!DOCTYPE html><html><body>login</body></html>", {
319379status: 200,
@@ -332,7 +392,7 @@ describe("resolveSlackMedia", () => {
332392});
333393334394it("allows expected HTML uploads", async () => {
335-vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue(
395+vi.spyOn(mediaRuntime, "saveMediaBuffer").mockResolvedValue(
336396createSavedMedia("/tmp/page.html", "text/html"),
337397);
338398mockFetch.mockResolvedValueOnce(
@@ -363,7 +423,7 @@ describe("resolveSlackMedia", () => {
363423// video/mp4 for MP4 containers. Verify resolveSlackMedia preserves
364424// the overridden audio/* type in its return value despite this.
365425const saveMediaBufferMock = vi
366-.spyOn(mediaStore, "saveMediaBuffer")
426+.spyOn(mediaRuntime, "saveMediaBuffer")
367427.mockResolvedValue(createSavedMedia("/tmp/voice.mp4", "video/mp4"));
368428369429const mockResponse = new Response(Buffer.from("audio data"), {
@@ -401,7 +461,7 @@ describe("resolveSlackMedia", () => {
401461402462it("preserves original MIME for non-voice Slack files", async () => {
403463const saveMediaBufferMock = vi
404-.spyOn(mediaStore, "saveMediaBuffer")
464+.spyOn(mediaRuntime, "saveMediaBuffer")
405465.mockResolvedValue(createSavedMedia("/tmp/video.mp4", "video/mp4"));
406466407467const mockResponse = new Response(Buffer.from("video data"), {
@@ -434,7 +494,7 @@ describe("resolveSlackMedia", () => {
434494});
435495436496it("falls through to next file when first file returns error", async () => {
437-vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue(
497+vi.spyOn(mediaRuntime, "saveMediaBuffer").mockResolvedValue(
438498createSavedMedia("/tmp/test.jpg", "image/jpeg"),
439499);
440500@@ -463,7 +523,7 @@ describe("resolveSlackMedia", () => {
463523});
464524465525it("returns all successfully downloaded files as an array", async () => {
466-vi.spyOn(mediaStore, "saveMediaBuffer").mockImplementation(async (buffer, _contentType) => {
526+vi.spyOn(mediaRuntime, "saveMediaBuffer").mockImplementation(async (buffer, _contentType) => {
467527const text = Buffer.from(buffer).toString("utf8");
468528if (text.includes("image a")) {
469529return createSavedMedia("/tmp/a.jpg", "image/jpeg");
@@ -510,7 +570,7 @@ describe("resolveSlackMedia", () => {
510570511571it("caps downloads to 8 files for large multi-attachment messages", async () => {
512572const saveMediaBufferMock = vi
513-.spyOn(mediaStore, "saveMediaBuffer")
573+.spyOn(mediaRuntime, "saveMediaBuffer")
514574.mockResolvedValue(createSavedMedia("/tmp/x.jpg", "image/jpeg"));
515575516576mockFetch.mockImplementation(async () => {
@@ -539,14 +599,14 @@ describe("resolveSlackMedia", () => {
539599});
540600541601it("routes dispatcher-backed Slack media requests through runtime fetch", async () => {
542-vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue(
602+vi.spyOn(mediaRuntime, "saveMediaBuffer").mockResolvedValue(
543603createSavedMedia("/tmp/test.jpg", "image/jpeg"),
544604);
545605globalThis.fetch = (async () => {
546606throw new Error("global fetch should not receive dispatcher-backed Slack media requests");
547607}) as typeof fetch;
548608const runtimeFetchSpy = vi
549-.spyOn(ssrf, "fetchWithRuntimeDispatcher")
609+.spyOn(mediaRuntime, "fetchWithRuntimeDispatcher")
550610.mockImplementation(async () => {
551611return new Response(Buffer.from("image data"), {
552612status: 200,
@@ -578,7 +638,6 @@ describe("Slack media SSRF policy", () => {
578638beforeEach(() => {
579639mockFetch = vi.fn();
580640globalThis.fetch = withFetchPreconnect(mockFetch);
581-mockPinnedHostnameResolution();
582641});
583642584643afterEach(() => {
@@ -587,14 +646,14 @@ describe("Slack media SSRF policy", () => {
587646});
588647589648it("passes ssrfPolicy with Slack CDN allowedHostnames and allowRfc2544BenchmarkRange to file downloads", async () => {
590-vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue(
649+vi.spyOn(mediaRuntime, "saveMediaBuffer").mockResolvedValue(
591650createSavedMedia("/tmp/test.jpg", "image/jpeg"),
592651);
593652mockFetch.mockResolvedValueOnce(
594653new Response(Buffer.from("img"), { status: 200, headers: { "content-type": "image/jpeg" } }),
595654);
596655597-const spy = vi.spyOn(mediaFetch, "fetchRemoteMedia");
656+const spy = vi.spyOn(mediaRuntime, "fetchRemoteMedia");
598657599658await resolveSlackMedia({
600659files: [{ url_private: "https://files.slack.com/test.jpg", name: "test.jpg" }],
@@ -615,22 +674,14 @@ describe("Slack media SSRF policy", () => {
615674});
616675617676it("passes ssrfPolicy to forwarded attachment image downloads", async () => {
618-vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue(
677+vi.spyOn(mediaRuntime, "saveMediaBuffer").mockResolvedValue(
619678createSavedMedia("/tmp/fwd.jpg", "image/jpeg"),
620679);
621-vi.spyOn(ssrf, "resolvePinnedHostnameWithPolicy").mockImplementation(async (hostname) => {
622-const normalized = hostname.trim().toLowerCase().replace(/\.$/, "");
623-return {
624-hostname: normalized,
625-addresses: ["93.184.216.34"],
626-lookup: ssrf.createPinnedLookup({ hostname: normalized, addresses: ["93.184.216.34"] }),
627-};
628-});
629680mockFetch.mockResolvedValueOnce(
630681new Response(Buffer.from("fwd"), { status: 200, headers: { "content-type": "image/jpeg" } }),
631682);
632683633-const spy = vi.spyOn(mediaFetch, "fetchRemoteMedia");
684+const spy = vi.spyOn(mediaRuntime, "fetchRemoteMedia");
634685635686await resolveSlackAttachmentContent({
636687attachments: [{ is_share: true, image_url: "https://files.slack.com/forwarded.jpg" }],
@@ -650,7 +701,6 @@ describe("resolveSlackAttachmentContent", () => {
650701beforeEach(() => {
651702mockFetch = vi.fn();
652703globalThis.fetch = mockFetch as unknown as typeof fetch;
653-mockPinnedHostnameResolution();
654704});
655705656706afterEach(() => {
@@ -696,7 +746,7 @@ describe("resolveSlackAttachmentContent", () => {
696746});
697747698748it("skips forwarded image URLs on non-Slack hosts", async () => {
699-const saveMediaBufferMock = vi.spyOn(mediaStore, "saveMediaBuffer");
749+const saveMediaBufferMock = vi.spyOn(mediaRuntime, "saveMediaBuffer");
700750701751const result = await resolveSlackAttachmentContent({
702752attachments: [{ is_share: true, image_url: "https://example.com/forwarded.jpg" }],
@@ -710,7 +760,7 @@ describe("resolveSlackAttachmentContent", () => {
710760});
711761712762it("downloads Slack-hosted images from forwarded shared attachments", async () => {
713-vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue(
763+vi.spyOn(mediaRuntime, "saveMediaBuffer").mockResolvedValue(
714764createSavedMedia("/tmp/forwarded.jpg", "image/jpeg"),
715765);
716766此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。