import { describe, expect, test } from "bun:test"; import { attachmentDisposition, contentDisposition } from "./content-disposition"; import { safeDecode } from "./safe-decode"; describe("attachmentDisposition (GIT-A9)", () => { test("plain names pass through with an RFC 5987 twin", () => { expect(attachmentDisposition("app-main.tar.gz")).toBe( `attachment; filename="app-main.tar.gz"; filename*=UTF-8''app-main.tar.gz`, ); }); test("quotes, semicolons and backslashes cannot inject parameters", () => { const header = attachmentDisposition(`x";filename*=UTF-8''evil.exe;x.tar.gz`); const [, quoted] = /filename="([^"]*)"/.exec(header) ?? []; expect(quoted).toBe("x__filename*=UTF-8''evil.exe_x.tar.gz"); expect(header.split(";").length).toBe(3); expect(header).toContain("filename*=UTF-8''x%22%3Bfilename%2A%3DUTF-8%27%27evil.exe%3Bx.tar.gz"); }); test("non-ASCII goes percent-encoded, ASCII fallback is sanitized", () => { const header = attachmentDisposition("répo-ünicode.tar.gz"); expect(header).toContain(`filename="r_po-_nicode.tar.gz"`); expect(header).toContain("filename*=UTF-8''r%C3%A9po-%C3%BCnicode.tar.gz"); expect(/[^\x20-\x7e]/.test(header)).toBe(false); }); }); describe("safeDecode (GIT-A11)", () => { test("decodes valid encodings and rejects malformed ones", () => { expect(safeDecode("a%20b")).toBe("a b"); expect(safeDecode("%E0%A4%A")).toBeNull(); expect(safeDecode("%")).toBeNull(); }); }); describe("contentDisposition (DEPLOY-P9)", () => { test("inline attachments get the same sanitized ASCII fallback and RFC 5987 name", () => { expect(contentDisposition("inline", 'ré"sumé;.png')).toBe( `inline; filename="r__sum__.png"; filename*=UTF-8''r%C3%A9%22sum%C3%A9%3B.png`, ); }); });