43 lines · 1.8 KB
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`,
    );