
























@@ -2,10 +2,26 @@ import fs from "node:fs/promises";
22import os from "node:os";
33import path from "node:path";
44import JSZip from "jszip";
5+import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
56import * as tar from "tar";
6-import { describe, expect, it } from "vitest";
7+import { beforeEach, describe, expect, it, vi } from "vitest";
78import type { ReleaseAsset } from "./install-signal-cli.js";
8-import { extractSignalCliArchive, looksLikeArchive, pickAsset } from "./install-signal-cli.js";
9+10+const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({
11+fetchWithSsrFGuardMock: vi.fn(),
12+}));
13+14+vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
15+fetchWithSsrFGuard: fetchWithSsrFGuardMock,
16+}));
17+18+const {
19+ downloadToFile,
20+ extractSignalCliArchive,
21+ installSignalCliFromRelease,
22+ looksLikeArchive,
23+ pickAsset,
24+} = await import("./install-signal-cli.js");
9251026const SAMPLE_ASSETS: ReleaseAsset[] = [
1127{
@@ -39,6 +55,26 @@ const SAMPLE_ASSETS: ReleaseAsset[] = [
3955},
4056];
415758+function okDownloadResponse(body: BodyInit, init: ResponseInit = {}) {
59+return {
60+response: new Response(body, { status: 200, ...init }),
61+release: vi.fn().mockResolvedValue(undefined),
62+};
63+}
64+65+async function withTempFile(run: (filePath: string) => Promise<void>) {
66+const workDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-signal-download-"));
67+try {
68+await run(path.join(workDir, "signal-cli.tgz"));
69+} finally {
70+await fs.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
71+}
72+}
73+74+beforeEach(() => {
75+fetchWithSsrFGuardMock.mockReset();
76+});
77+4278describe("looksLikeArchive", () => {
4379it("recognises .tar.gz", () => {
4480expect(looksLikeArchive("foo.tar.gz")).toBe(true);
@@ -131,6 +167,94 @@ describe("pickAsset", () => {
131167});
132168});
133169170+describe("downloadToFile", () => {
171+it("downloads through the SSRF guard with an explicit timeout", async () => {
172+const fetchResult = okDownloadResponse("archive");
173+fetchWithSsrFGuardMock.mockResolvedValue(fetchResult);
174+175+await withTempFile(async (filePath) => {
176+await downloadToFile("https://example.com/signal-cli.tgz", filePath);
177+178+await expect(fs.readFile(filePath, "utf-8")).resolves.toBe("archive");
179+});
180+181+expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
182+expect.objectContaining({
183+url: "https://example.com/signal-cli.tgz",
184+requireHttps: true,
185+timeoutMs: 5 * 60_000,
186+auditContext: "signal-cli-install-archive",
187+}),
188+);
189+expect(fetchResult.release).toHaveBeenCalledTimes(1);
190+});
191+192+it("rejects declared archives above the download cap", async () => {
193+const fetchResult = okDownloadResponse("archive", {
194+headers: { "content-length": "12" },
195+});
196+fetchWithSsrFGuardMock.mockResolvedValue(fetchResult);
197+198+await withTempFile(async (filePath) => {
199+await expect(
200+downloadToFile("https://example.com/signal-cli.tgz", filePath, 5, 8),
201+).rejects.toThrow("declared 12");
202+203+await expect(fs.access(filePath)).rejects.toThrow();
204+});
205+206+expect(fetchResult.release).toHaveBeenCalledTimes(1);
207+});
208+209+it("aborts streamed archives above the download cap and removes partial files", async () => {
210+const body = new ReadableStream<Uint8Array>({
211+start(controller) {
212+controller.enqueue(new Uint8Array(6));
213+controller.enqueue(new Uint8Array(6));
214+controller.close();
215+},
216+});
217+const fetchResult = okDownloadResponse(body);
218+fetchWithSsrFGuardMock.mockResolvedValue(fetchResult);
219+220+await withTempFile(async (filePath) => {
221+await expect(
222+downloadToFile("https://example.com/signal-cli.tgz", filePath, 5, 8),
223+).rejects.toThrow("8-byte download cap");
224+225+await expect(fs.access(filePath)).rejects.toThrow();
226+});
227+228+expect(fetchResult.release).toHaveBeenCalledTimes(1);
229+});
230+});
231+232+describe("installSignalCliFromRelease", () => {
233+it("bounds the release metadata request with an explicit timeout", async () => {
234+const fetchResult = okDownloadResponse(JSON.stringify({ tag_name: "v0.14.3", assets: [] }), {
235+headers: { "content-type": "application/json" },
236+});
237+fetchWithSsrFGuardMock.mockResolvedValue(fetchResult);
238+239+await expect(
240+installSignalCliFromRelease({ log: vi.fn() } as unknown as RuntimeEnv),
241+).resolves.toMatchObject({
242+ok: false,
243+error: "No compatible release asset found for this platform.",
244+});
245+246+expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
247+expect.objectContaining({
248+url: "https://api.github.com/repos/AsamK/signal-cli/releases/latest",
249+requireHttps: true,
250+timeoutMs: 30_000,
251+auditContext: "signal-cli-release-info",
252+}),
253+);
254+expect(fetchResult.release).toHaveBeenCalledTimes(1);
255+});
256+});
257+134258describe("extractSignalCliArchive", () => {
135259async function withArchiveWorkspace(run: (workDir: string) => Promise<void>) {
136260const workDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-signal-install-"));
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。