package handlers import ( "strings" "testing" ) func TestSafeContentType_RewritesExecutableTypes(t *testing.T) { cases := []struct { in, want string }{ {"text/html", "application/octet-stream"}, {"text/html; charset=utf-8", "application/octet-stream"}, {"TEXT/HTML", "application/octet-stream"}, {" text/html ", "application/octet-stream"}, {"application/xhtml+xml", "application/octet-stream"}, {"image/svg+xml", "application/octet-stream"}, {"application/xml", "application/octet-stream"}, {"text/xml", "application/octet-stream"}, {"application/javascript", "application/octet-stream"}, {"text/javascript", "application/octet-stream"}, // Safe types pass through unchanged (including parameters). {"image/png", "image/png"}, {"application/octet-stream", "application/octet-stream"}, {"text/plain; charset=utf-8", "text/plain; charset=utf-8"}, {"", ""}, } for _, tc := range cases { t.Run(tc.in, func(t *testing.T) { if got := safeContentType(tc.in); got != tc.want { t.Errorf("safeContentType(%q) = %q, want %q", tc.in, got, tc.want) } }) } } func TestContentDispositionHeader_StripsPathAndEscapes(t *testing.T) { cases := []struct { name string disp, key string mustContain []string mustNotContain []string }{ { name: "simple filename", disp: "inline", key: "photo.png", mustContain: []string{ `inline; filename="photo.png"; filename*=UTF-8''photo.png`, }, }, { name: "path components stripped", disp: "attachment", key: "a/b/c/file.txt", mustContain: []string{`filename="file.txt"`, `filename*=UTF-8''file.txt`}, mustNotContain: []string{`a/b/c`}, }, { name: "quote and backslash replaced in ASCII fallback", disp: "inline", key: `"evil\name".txt`, mustContain: []string{`filename="_evil_name_.txt"`}, mustNotContain: []string{`"evil`, `\name`}, }, { name: "control character replaced", disp: "inline", key: "line\nbreak.txt", mustContain: []string{`filename="line_break.txt"`}, }, { name: "non-ASCII preserved in filename* only", disp: "inline", key: "漢字.txt", mustContain: []string{ `filename*=UTF-8''`, // fallback replaced every non-ASCII rune with _ (2 runes + .txt). `filename="__.txt"`, }, }, { name: "empty key falls back to download", disp: "inline", key: "", mustContain: []string{`filename="download"`, `filename*=UTF-8''download`}, }, { name: "key of only slashes falls back to download", disp: "inline", key: "/", mustContain: []string{`filename="download"`, `filename*=UTF-8''`}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := contentDispositionHeader(tc.disp, tc.key) for _, sub := range tc.mustContain { if !strings.Contains(got, sub) { t.Errorf("got %q\nmust contain %q", got, sub) } } for _, sub := range tc.mustNotContain { if strings.Contains(got, sub) { t.Errorf("got %q\nmust NOT contain %q", got, sub) } } }) } }