forked from bai/curriculum-project-hub
fix: confine Agent and MCP data access
This commit is contained in:
@@ -1,26 +1,31 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { access, mkdir, mkdtemp, readFile, realpath, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
import {
|
||||
downloadMessageFile,
|
||||
FeishuFileDeliveryError,
|
||||
resolveSenderName,
|
||||
sendFile,
|
||||
sendFileData,
|
||||
sendLongText,
|
||||
sendTextMessage,
|
||||
type FeishuRuntime,
|
||||
} from "../../src/feishu/client.js";
|
||||
import { SenderNameCache } from "../../src/feishu/senderCache.js";
|
||||
|
||||
const itOnLinux = process.platform === "linux" ? it : it.skip;
|
||||
|
||||
describe("Feishu client helpers", () => {
|
||||
it("downloads an image message resource through the SDK stream helper", async () => {
|
||||
itOnLinux("downloads an image message resource through the SDK stream helper", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "hub-feishu-client-"));
|
||||
try {
|
||||
const destination = join(dir, "image.png");
|
||||
const resourceWriteFile = vi.fn(async (filePath: string) => {
|
||||
await writeFile(filePath, "image bytes");
|
||||
});
|
||||
const messageResourceGet = vi.fn(async () => ({ writeFile: resourceWriteFile }));
|
||||
const workspace = join(dir, "workspace");
|
||||
await mkdir(workspace);
|
||||
const destination = join(workspace, ".cph", "inbox", "image.png");
|
||||
const getReadableStream = vi.fn(() => Readable.from([Buffer.from("image bytes")]));
|
||||
const messageResourceGet = vi.fn(async () => ({ getReadableStream }));
|
||||
const rawRequest = vi.fn(async () => Buffer.from("image bytes"));
|
||||
const rt = mockRuntime({
|
||||
fileCreate: vi.fn(),
|
||||
@@ -29,13 +34,15 @@ describe("Feishu client helpers", () => {
|
||||
request: rawRequest,
|
||||
});
|
||||
|
||||
await downloadMessageFile(rt, "message-1", "img-key-1", destination, "image");
|
||||
await expect(
|
||||
downloadMessageFile(rt, "message-1", "img-key-1", dir, workspace, ".cph/inbox/image.png", "image"),
|
||||
).resolves.toBe(join(await realpath(workspace), ".cph", "inbox", "image.png"));
|
||||
|
||||
expect(messageResourceGet).toHaveBeenCalledWith({
|
||||
params: { type: "image" },
|
||||
path: { message_id: "message-1", file_key: "img-key-1" },
|
||||
});
|
||||
expect(resourceWriteFile).toHaveBeenCalledWith(destination);
|
||||
expect(getReadableStream).toHaveBeenCalledTimes(1);
|
||||
await expect(readFile(destination, "utf8")).resolves.toBe("image bytes");
|
||||
expect(rawRequest).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
@@ -43,6 +50,27 @@ describe("Feishu client helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
itOnLinux("refuses an inbound download through a symlinked workspace directory", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "hub-feishu-client-"));
|
||||
try {
|
||||
const workspace = join(root, "workspace");
|
||||
const outside = join(root, "outside");
|
||||
await Promise.all([mkdir(workspace), mkdir(outside)]);
|
||||
await symlink(outside, join(workspace, ".cph"));
|
||||
const messageResourceGet = vi.fn(async () => ({
|
||||
getReadableStream: () => Readable.from([Buffer.from("must-not-escape")]),
|
||||
}));
|
||||
const rt = mockRuntime({ fileCreate: vi.fn(), messageCreate: vi.fn(), messageResourceGet });
|
||||
|
||||
await expect(
|
||||
downloadMessageFile(rt, "message-1", "file-key-1", root, workspace, ".cph/inbox/file.bin", "file"),
|
||||
).rejects.toThrow(/workspace|symlink|directory/i);
|
||||
await expect(access(join(outside, "inbox", "file.bin"))).rejects.toMatchObject({ code: "ENOENT" });
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts top-level file_key from the Lark SDK file upload response", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "hub-feishu-client-"));
|
||||
try {
|
||||
@@ -62,6 +90,19 @@ describe("Feishu client helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects malformed file upload responses instead of hiding the failure", async () => {
|
||||
const rt = mockRuntime({
|
||||
fileCreate: vi.fn(async () => ({ data: {} })),
|
||||
messageCreate: vi.fn(),
|
||||
});
|
||||
|
||||
await expect(sendFileData(rt, "chat-1", Buffer.from("bytes"), "student.pdf"))
|
||||
.rejects.toEqual(expect.objectContaining({
|
||||
name: "FeishuFileDeliveryError",
|
||||
message: "Feishu file upload response is missing file_key",
|
||||
} satisfies Partial<FeishuFileDeliveryError>));
|
||||
});
|
||||
|
||||
it("accepts top-level message_id from message create responses", async () => {
|
||||
const rt = mockRuntime({
|
||||
fileCreate: vi.fn(),
|
||||
|
||||
Reference in New Issue
Block a user