215 lines
5.9 KiB
TypeScript
215 lines
5.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
buildMessageWithAttachments,
|
|
type ChatAttachment,
|
|
parseMessageWithAttachments,
|
|
} from "./chat-attachments.js";
|
|
|
|
const PNG_1x1 =
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII=";
|
|
|
|
describe("buildMessageWithAttachments", () => {
|
|
it("embeds a single image as data URL", () => {
|
|
const msg = buildMessageWithAttachments("see this", [
|
|
{
|
|
type: "image",
|
|
mimeType: "image/png",
|
|
fileName: "dot.png",
|
|
content: PNG_1x1,
|
|
},
|
|
]);
|
|
expect(msg).toContain("see this");
|
|
expect(msg).toContain(`data:image/png;base64,${PNG_1x1}`);
|
|
expect(msg).toContain("![dot.png]");
|
|
});
|
|
|
|
it("rejects non-image mime types", () => {
|
|
const bad: ChatAttachment = {
|
|
type: "file",
|
|
mimeType: "application/pdf",
|
|
fileName: "a.pdf",
|
|
content: "AAA",
|
|
};
|
|
expect(() => buildMessageWithAttachments("x", [bad])).toThrow(/image/);
|
|
});
|
|
|
|
it("rejects invalid base64 content", () => {
|
|
const bad: ChatAttachment = {
|
|
type: "image",
|
|
mimeType: "image/png",
|
|
fileName: "dot.png",
|
|
content: "%not-base64%",
|
|
};
|
|
expect(() => buildMessageWithAttachments("x", [bad])).toThrow(/base64/);
|
|
});
|
|
|
|
it("rejects images over limit", () => {
|
|
const big = Buffer.alloc(6_000_000, 0).toString("base64");
|
|
const att: ChatAttachment = {
|
|
type: "image",
|
|
mimeType: "image/png",
|
|
fileName: "big.png",
|
|
content: big,
|
|
};
|
|
expect(() => buildMessageWithAttachments("x", [att], { maxBytes: 5_000_000 })).toThrow(
|
|
/exceeds size limit/i,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("parseMessageWithAttachments", () => {
|
|
it("strips data URL prefix", async () => {
|
|
const parsed = await parseMessageWithAttachments(
|
|
"see this",
|
|
[
|
|
{
|
|
type: "image",
|
|
mimeType: "image/png",
|
|
fileName: "dot.png",
|
|
content: `data:image/png;base64,${PNG_1x1}`,
|
|
},
|
|
],
|
|
{ log: { warn: () => {} } },
|
|
);
|
|
expect(parsed.images).toHaveLength(1);
|
|
expect(parsed.images[0]?.mimeType).toBe("image/png");
|
|
expect(parsed.images[0]?.data).toBe(PNG_1x1);
|
|
});
|
|
|
|
it("rejects invalid base64 content", async () => {
|
|
await expect(
|
|
parseMessageWithAttachments(
|
|
"x",
|
|
[
|
|
{
|
|
type: "image",
|
|
mimeType: "image/png",
|
|
fileName: "dot.png",
|
|
content: "%not-base64%",
|
|
},
|
|
],
|
|
{ log: { warn: () => {} } },
|
|
),
|
|
).rejects.toThrow(/base64/i);
|
|
});
|
|
|
|
it("rejects images over limit", async () => {
|
|
const big = Buffer.alloc(6_000_000, 0).toString("base64");
|
|
await expect(
|
|
parseMessageWithAttachments(
|
|
"x",
|
|
[
|
|
{
|
|
type: "image",
|
|
mimeType: "image/png",
|
|
fileName: "big.png",
|
|
content: big,
|
|
},
|
|
],
|
|
{ maxBytes: 5_000_000, log: { warn: () => {} } },
|
|
),
|
|
).rejects.toThrow(/exceeds size limit/i);
|
|
});
|
|
|
|
it("sniffs mime when missing", async () => {
|
|
const logs: string[] = [];
|
|
const parsed = await parseMessageWithAttachments(
|
|
"see this",
|
|
[
|
|
{
|
|
type: "image",
|
|
fileName: "dot.png",
|
|
content: PNG_1x1,
|
|
},
|
|
],
|
|
{ log: { warn: (message) => logs.push(message) } },
|
|
);
|
|
expect(parsed.message).toBe("see this");
|
|
expect(parsed.images).toHaveLength(1);
|
|
expect(parsed.images[0]?.mimeType).toBe("image/png");
|
|
expect(parsed.images[0]?.data).toBe(PNG_1x1);
|
|
expect(logs).toHaveLength(0);
|
|
});
|
|
|
|
it("drops non-image payloads and logs", async () => {
|
|
const logs: string[] = [];
|
|
const pdf = Buffer.from("%PDF-1.4\n").toString("base64");
|
|
const parsed = await parseMessageWithAttachments(
|
|
"x",
|
|
[
|
|
{
|
|
type: "file",
|
|
mimeType: "image/png",
|
|
fileName: "not-image.pdf",
|
|
content: pdf,
|
|
},
|
|
],
|
|
{ log: { warn: (message) => logs.push(message) } },
|
|
);
|
|
expect(parsed.images).toHaveLength(0);
|
|
expect(logs).toHaveLength(1);
|
|
expect(logs[0]).toMatch(/non-image/i);
|
|
});
|
|
|
|
it("prefers sniffed mime type and logs mismatch", async () => {
|
|
const logs: string[] = [];
|
|
const parsed = await parseMessageWithAttachments(
|
|
"x",
|
|
[
|
|
{
|
|
type: "image",
|
|
mimeType: "image/jpeg",
|
|
fileName: "dot.png",
|
|
content: PNG_1x1,
|
|
},
|
|
],
|
|
{ log: { warn: (message) => logs.push(message) } },
|
|
);
|
|
expect(parsed.images).toHaveLength(1);
|
|
expect(parsed.images[0]?.mimeType).toBe("image/png");
|
|
expect(logs).toHaveLength(1);
|
|
expect(logs[0]).toMatch(/mime mismatch/i);
|
|
});
|
|
|
|
it("drops unknown mime when sniff fails and logs", async () => {
|
|
const logs: string[] = [];
|
|
const unknown = Buffer.from("not an image").toString("base64");
|
|
const parsed = await parseMessageWithAttachments(
|
|
"x",
|
|
[{ type: "file", fileName: "unknown.bin", content: unknown }],
|
|
{ log: { warn: (message) => logs.push(message) } },
|
|
);
|
|
expect(parsed.images).toHaveLength(0);
|
|
expect(logs).toHaveLength(1);
|
|
expect(logs[0]).toMatch(/unable to detect image mime type/i);
|
|
});
|
|
|
|
it("keeps valid images and drops invalid ones", async () => {
|
|
const logs: string[] = [];
|
|
const pdf = Buffer.from("%PDF-1.4\n").toString("base64");
|
|
const parsed = await parseMessageWithAttachments(
|
|
"x",
|
|
[
|
|
{
|
|
type: "image",
|
|
mimeType: "image/png",
|
|
fileName: "dot.png",
|
|
content: PNG_1x1,
|
|
},
|
|
{
|
|
type: "file",
|
|
mimeType: "image/png",
|
|
fileName: "not-image.pdf",
|
|
content: pdf,
|
|
},
|
|
],
|
|
{ log: { warn: (message) => logs.push(message) } },
|
|
);
|
|
expect(parsed.images).toHaveLength(1);
|
|
expect(parsed.images[0]?.mimeType).toBe("image/png");
|
|
expect(parsed.images[0]?.data).toBe(PNG_1x1);
|
|
expect(logs.some((l) => /non-image/i.test(l))).toBe(true);
|
|
});
|
|
});
|