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`,
);