import { beforeEach, describe, expect, it, vi } from "vitest"; const createFeishuClientMock = vi.hoisted(() => vi.fn()); const resolveFeishuAccountMock = vi.hoisted(() => vi.fn()); const normalizeFeishuTargetMock = vi.hoisted(() => vi.fn()); const resolveReceiveIdTypeMock = vi.hoisted(() => vi.fn()); const loadWebMediaMock = vi.hoisted(() => vi.fn()); const fileCreateMock = vi.hoisted(() => vi.fn()); const messageCreateMock = vi.hoisted(() => vi.fn()); const messageReplyMock = vi.hoisted(() => vi.fn()); vi.mock("./client.js", () => ({ createFeishuClient: createFeishuClientMock, })); vi.mock("./accounts.js", () => ({ resolveFeishuAccount: resolveFeishuAccountMock, })); vi.mock("./targets.js", () => ({ normalizeFeishuTarget: normalizeFeishuTargetMock, resolveReceiveIdType: resolveReceiveIdTypeMock, })); vi.mock("./runtime.js", () => ({ getFeishuRuntime: () => ({ media: { loadWebMedia: loadWebMediaMock, }, }), })); import { sendMediaFeishu } from "./media.js"; describe("sendMediaFeishu msg_type routing", () => { beforeEach(() => { vi.clearAllMocks(); resolveFeishuAccountMock.mockReturnValue({ configured: true, accountId: "main", config: {}, appId: "app_id", appSecret: "app_secret", domain: "feishu", }); normalizeFeishuTargetMock.mockReturnValue("ou_target"); resolveReceiveIdTypeMock.mockReturnValue("open_id"); createFeishuClientMock.mockReturnValue({ im: { file: { create: fileCreateMock, }, message: { create: messageCreateMock, reply: messageReplyMock, }, }, }); fileCreateMock.mockResolvedValue({ code: 0, data: { file_key: "file_key_1" }, }); messageCreateMock.mockResolvedValue({ code: 0, data: { message_id: "msg_1" }, }); messageReplyMock.mockResolvedValue({ code: 0, data: { message_id: "reply_1" }, }); loadWebMediaMock.mockResolvedValue({ buffer: Buffer.from("remote-audio"), fileName: "remote.opus", kind: "audio", contentType: "audio/ogg", }); }); it("uses msg_type=media for mp4", async () => { await sendMediaFeishu({ cfg: {} as any, to: "user:ou_target", mediaBuffer: Buffer.from("video"), fileName: "clip.mp4", }); expect(fileCreateMock).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ file_type: "mp4" }), }), ); expect(messageCreateMock).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ msg_type: "media" }), }), ); }); it("uses msg_type=media for opus", async () => { await sendMediaFeishu({ cfg: {} as any, to: "user:ou_target", mediaBuffer: Buffer.from("audio"), fileName: "voice.opus", }); expect(fileCreateMock).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ file_type: "opus" }), }), ); expect(messageCreateMock).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ msg_type: "media" }), }), ); }); it("uses msg_type=file for documents", async () => { await sendMediaFeishu({ cfg: {} as any, to: "user:ou_target", mediaBuffer: Buffer.from("doc"), fileName: "paper.pdf", }); expect(fileCreateMock).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ file_type: "pdf" }), }), ); expect(messageCreateMock).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ msg_type: "file" }), }), ); }); it("uses msg_type=media when replying with mp4", async () => { await sendMediaFeishu({ cfg: {} as any, to: "user:ou_target", mediaBuffer: Buffer.from("video"), fileName: "reply.mp4", replyToMessageId: "om_parent", }); expect(messageReplyMock).toHaveBeenCalledWith( expect.objectContaining({ path: { message_id: "om_parent" }, data: expect.objectContaining({ msg_type: "media" }), }), ); expect(messageCreateMock).not.toHaveBeenCalled(); }); it("fails closed when media URL fetch is blocked", async () => { loadWebMediaMock.mockRejectedValueOnce( new Error("Blocked: resolves to private/internal IP address"), ); await expect( sendMediaFeishu({ cfg: {} as any, to: "user:ou_target", mediaUrl: "https://x/img", fileName: "voice.opus", }), ).rejects.toThrow(/private\/internal/i); expect(fileCreateMock).not.toHaveBeenCalled(); expect(messageCreateMock).not.toHaveBeenCalled(); expect(messageReplyMock).not.toHaveBeenCalled(); }); });