mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 08:12:43 +00:00
Landed from contributor PR #31151 by @scoootscooob. Co-authored-by: scoootscooob <scoootscooob@users.noreply.github.com>
120 lines
3.5 KiB
TypeScript
120 lines
3.5 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
|
|
|
|
const getMessageContentMock = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock("@line/bot-sdk", () => ({
|
|
messagingApi: {
|
|
MessagingApiBlobClient: class {
|
|
getMessageContent(messageId: string) {
|
|
return getMessageContentMock(messageId);
|
|
}
|
|
},
|
|
},
|
|
}));
|
|
|
|
vi.mock("../globals.js", () => ({
|
|
logVerbose: () => {},
|
|
}));
|
|
|
|
import { downloadLineMedia } from "./download.js";
|
|
|
|
async function* chunks(parts: Buffer[]): AsyncGenerator<Buffer> {
|
|
for (const part of parts) {
|
|
yield part;
|
|
}
|
|
}
|
|
|
|
describe("downloadLineMedia", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("does not derive temp file path from external messageId", async () => {
|
|
const messageId = "a/../../../../etc/passwd";
|
|
const jpeg = Buffer.from([0xff, 0xd8, 0xff, 0x00]);
|
|
getMessageContentMock.mockResolvedValueOnce(chunks([jpeg]));
|
|
|
|
const writeSpy = vi.spyOn(fs.promises, "writeFile").mockResolvedValueOnce(undefined);
|
|
|
|
const result = await downloadLineMedia(messageId, "token");
|
|
const writtenPath = writeSpy.mock.calls[0]?.[0];
|
|
|
|
expect(result.size).toBe(jpeg.length);
|
|
expect(result.contentType).toBe("image/jpeg");
|
|
expect(typeof writtenPath).toBe("string");
|
|
if (typeof writtenPath !== "string") {
|
|
throw new Error("expected string temp file path");
|
|
}
|
|
expect(result.path).toBe(writtenPath);
|
|
expect(writtenPath).toContain("line-media-");
|
|
expect(writtenPath).toMatch(/\.jpg$/);
|
|
expect(writtenPath).not.toContain(messageId);
|
|
expect(writtenPath).not.toContain("..");
|
|
|
|
const tmpRoot = path.resolve(resolvePreferredOpenClawTmpDir());
|
|
const rel = path.relative(tmpRoot, path.resolve(writtenPath));
|
|
expect(rel === ".." || rel.startsWith(`..${path.sep}`)).toBe(false);
|
|
});
|
|
|
|
it("rejects oversized media before writing to disk", async () => {
|
|
getMessageContentMock.mockResolvedValueOnce(chunks([Buffer.alloc(4), Buffer.alloc(4)]));
|
|
const writeSpy = vi.spyOn(fs.promises, "writeFile").mockResolvedValue(undefined);
|
|
|
|
await expect(downloadLineMedia("mid", "token", 7)).rejects.toThrow(/Media exceeds/i);
|
|
expect(writeSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("detects M4A audio from ftyp major brand (#29751)", async () => {
|
|
// Real M4A magic bytes: size(4) + "ftyp" + "M4A "
|
|
const m4a = Buffer.from([
|
|
0x00,
|
|
0x00,
|
|
0x00,
|
|
0x1c, // box size
|
|
0x66,
|
|
0x74,
|
|
0x79,
|
|
0x70, // "ftyp"
|
|
0x4d,
|
|
0x34,
|
|
0x41,
|
|
0x20, // "M4A " major brand
|
|
]);
|
|
getMessageContentMock.mockResolvedValueOnce(chunks([m4a]));
|
|
vi.spyOn(fs.promises, "writeFile").mockResolvedValueOnce(undefined);
|
|
|
|
const result = await downloadLineMedia("mid-m4a", "token");
|
|
|
|
expect(result.contentType).toBe("audio/mp4");
|
|
expect(result.path).toMatch(/\.m4a$/);
|
|
});
|
|
|
|
it("detects MP4 video from ftyp major brand (isom)", async () => {
|
|
// MP4 video magic bytes: size(4) + "ftyp" + "isom"
|
|
const mp4 = Buffer.from([
|
|
0x00,
|
|
0x00,
|
|
0x00,
|
|
0x1c,
|
|
0x66,
|
|
0x74,
|
|
0x79,
|
|
0x70,
|
|
0x69,
|
|
0x73,
|
|
0x6f,
|
|
0x6d, // "isom" major brand
|
|
]);
|
|
getMessageContentMock.mockResolvedValueOnce(chunks([mp4]));
|
|
vi.spyOn(fs.promises, "writeFile").mockResolvedValueOnce(undefined);
|
|
|
|
const result = await downloadLineMedia("mid-mp4", "token");
|
|
|
|
expect(result.contentType).toBe("video/mp4");
|
|
expect(result.path).toMatch(/\.mp4$/);
|
|
});
|
|
});
|