























@@ -1,13 +1,30 @@
1-import { describe, expect, it } from "vitest";
1+import fs from "node:fs/promises";
2+import os from "node:os";
3+import path from "node:path";
4+import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
5+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
26import {
7+createDefaultMenuConfig,
38createGridLayout,
9+datetimePickerAction,
410messageAction,
5-uriAction,
611postbackAction,
7-datetimePickerAction,
8-createDefaultMenuConfig,
12+uploadRichMenuImage,
13+uriAction,
914} from "./rich-menu.js";
101516+const { setRichMenuImageMock, MessagingApiBlobClientMock } = vi.hoisted(() => {
17+const setRichMenuImageMock = vi.fn();
18+const MessagingApiBlobClientMock = vi.fn(function () {
19+return { setRichMenuImage: setRichMenuImageMock };
20+});
21+return { setRichMenuImageMock, MessagingApiBlobClientMock };
22+});
23+24+vi.mock("@line/bot-sdk", () => ({
25+messagingApi: { MessagingApiBlobClient: MessagingApiBlobClientMock },
26+}));
27+1128describe("messageAction", () => {
1229it("creates message actions with explicit or default text", () => {
1330const cases = [
@@ -205,3 +222,89 @@ describe("createDefaultMenuConfig", () => {
205222expect(commands).toContain("/settings");
206223});
207224});
225+226+const richMenuUploadCfg: OpenClawConfig = {
227+channels: {
228+line: {
229+channelAccessToken: "line-token",
230+channelSecret: "line-secret",
231+},
232+},
233+};
234+235+describe("uploadRichMenuImage", () => {
236+let tempRoot: string;
237+238+beforeEach(async () => {
239+tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-line-rich-menu-"));
240+setRichMenuImageMock.mockReset();
241+MessagingApiBlobClientMock.mockClear();
242+});
243+244+afterEach(async () => {
245+await fs.rm(tempRoot, { recursive: true, force: true });
246+});
247+248+it("loads local image paths through approved media localRoots", async () => {
249+const workspaceDir = path.join(tempRoot, "workspace");
250+await fs.mkdir(workspaceDir, { recursive: true });
251+const imagePath = path.join(workspaceDir, "menu.png");
252+const imageBytes = Buffer.from([
253+0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x00,
254+]);
255+await fs.writeFile(imagePath, imageBytes);
256+257+await uploadRichMenuImage("rich-menu-1", imagePath, {
258+cfg: richMenuUploadCfg,
259+mediaLocalRoots: [workspaceDir],
260+});
261+262+expect(MessagingApiBlobClientMock).toHaveBeenCalledWith({ channelAccessToken: "line-token" });
263+expect(setRichMenuImageMock).toHaveBeenCalledOnce();
264+const [richMenuId, blob] = setRichMenuImageMock.mock.calls[0] ?? [];
265+expect(richMenuId).toBe("rich-menu-1");
266+expect(blob).toBeInstanceOf(Blob);
267+expect((blob as Blob).type).toBe("image/png");
268+await expect((blob as Blob).arrayBuffer()).resolves.toEqual(
269+imageBytes.buffer.slice(imageBytes.byteOffset, imageBytes.byteOffset + imageBytes.byteLength),
270+);
271+});
272+273+it("rejects local image paths outside approved media localRoots before uploading", async () => {
274+const workspaceDir = path.join(tempRoot, "workspace");
275+const outsideDir = path.join(tempRoot, "outside");
276+await fs.mkdir(workspaceDir, { recursive: true });
277+await fs.mkdir(outsideDir, { recursive: true });
278+const outsideImagePath = path.join(outsideDir, "menu.jpg");
279+await fs.writeFile(outsideImagePath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
280+281+await expect(
282+uploadRichMenuImage("rich-menu-1", outsideImagePath, {
283+cfg: richMenuUploadCfg,
284+mediaLocalRoots: [workspaceDir],
285+}),
286+).rejects.toThrow(/Local media path is not under an allowed directory/i);
287+288+expect(setRichMenuImageMock).not.toHaveBeenCalled();
289+});
290+291+it("preserves extension-based content-type fallback for approved local paths", async () => {
292+const workspaceDir = path.join(tempRoot, "workspace");
293+await fs.mkdir(workspaceDir, { recursive: true });
294+const imagePath = path.join(workspaceDir, "menu.jpg");
295+const imageBytes = Buffer.from("placeholder image bytes");
296+await fs.writeFile(imagePath, imageBytes);
297+298+await uploadRichMenuImage("rich-menu-2", imagePath, {
299+cfg: richMenuUploadCfg,
300+mediaLocalRoots: [workspaceDir],
301+});
302+303+expect(setRichMenuImageMock).toHaveBeenCalledOnce();
304+const blob = setRichMenuImageMock.mock.calls[0]?.[1] as Blob;
305+expect(blob.type).toBe("image/jpeg");
306+await expect(blob.arrayBuffer()).resolves.toEqual(
307+imageBytes.buffer.slice(imageBytes.byteOffset, imageBytes.byteOffset + imageBytes.byteLength),
308+);
309+});
310+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。