Files
MailUI4Agents/server/internal/handler/attachments_test.go

139 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handler
import (
"strings"
"testing"
)
// 文件名只用于展示与下载头;磁盘路径由 sha256 派生,
// 因此这里守的是「不污染 HTTP 头、不被当成目录」而非路径穿越。
func TestSanitizeFilename(t *testing.T) {
cases := []struct{ in, want string }{
{"report.pdf", "report.pdf"},
{"中文 文件名.txt", "中文 文件名.txt"},
// 目录成分必须剥掉(含 Windows 风格)
{"../../etc/passwd", "passwd"},
{"/abs/path/x.log", "x.log"},
{`C:\Users\me\a.txt`, "a.txt"},
{"a/b/c.txt", "c.txt"},
// 控制字符会污染 Content-Disposition
{"bad\r\nname.txt", "badname.txt"},
{"tab\there.txt", "tabhere.txt"},
// 无意义的名字兜底
{"", "unnamed"},
{" ", "unnamed"},
{".", "unnamed"},
{"..", "unnamed"},
{"/", "unnamed"},
}
for _, c := range cases {
if got := sanitizeFilename(c.in); got != c.want {
t.Errorf("sanitizeFilename(%q) = %q, want %q", c.in, got, c.want)
}
}
}
// 超长名字按 UTF-8 边界截断,不产生非法序列。
func TestSanitizeFilenameTruncates(t *testing.T) {
long := strings.Repeat("中", 200) + ".txt" // 每字 3 字节,共 600+
got := sanitizeFilename(long)
if len(got) > 255 {
t.Errorf("截断后 %d 字节,超过 255", len(got))
}
for _, r := range got {
if r == '\uFFFD' {
t.Fatalf("截断产生非法 UTF-8: %q", got)
}
}
}
func TestDetectContentType(t *testing.T) {
cases := []struct{ declared, filename, want string }{
{"application/pdf", "x.pdf", "application/pdf"},
// 客户端没给类型时按扩展名猜
{"", "notes.txt", "text/plain"},
{"application/octet-stream", "data.json", "application/json"},
// 带参数的声明要剥掉参数
{"text/plain; charset=utf-8", "a.txt", "text/plain"},
// 认不出就兜底
{"", "blob.unknownext", "application/octet-stream"},
{"garbage//not-a-type", "blob.unknownext", "application/octet-stream"},
}
for _, c := range cases {
got := detectContentType(c.declared, c.filename)
// mime.TypeByExtension 在不同系统上可能返回带参数的值,只比主类型
if !strings.HasPrefix(got, c.want) {
t.Errorf("detectContentType(%q, %q) = %q, want prefix %q",
c.declared, c.filename, got, c.want)
}
}
}
// Content-Disposition 必须双写filename* 承载 UTF-8filename= 给老客户端兜底。
// 兜底值里的引号/反斜杠/非 ASCII 一律换成下划线,否则能截断响应头。
func TestContentDisposition(t *testing.T) {
got := contentDisposition("报告 v2.pdf")
if !strings.HasPrefix(got, "attachment; ") {
t.Errorf("必须以 attachment 开头: %s", got)
}
if !strings.Contains(got, "filename*=UTF-8''") {
t.Errorf("缺少 RFC 5987 编码: %s", got)
}
// 非 ASCII 不能出现在 filename= 的兜底值里
ascii := got[:strings.Index(got, "filename*=")]
for _, r := range ascii {
if r > 0x7e {
t.Errorf("兜底 filename 含非 ASCII 字符 %q: %s", r, ascii)
}
}
// 引号注入不能逃出引号
evil := contentDisposition(`a"; x="y`)
if strings.Contains(evil[:strings.Index(evil, "filename*=")], `"; x=`) {
t.Errorf("引号未转义,可截断响应头: %s", evil)
}
// 控制字符(若绕过 sanitize 直达此处)也不能出现
ctl := contentDisposition("a\r\nb.txt")
if strings.ContainsAny(ctl, "\r\n") {
t.Errorf("响应头含换行: %q", ctl)
}
}
func TestURLEncodeRFC5987(t *testing.T) {
cases := []struct{ in, want string }{
{"abc.txt", "abc.txt"},
{"a b", "a%20b"},
{"中", "%E4%B8%AD"},
{`a"b`, "a%22b"},
}
for _, c := range cases {
if got := urlEncodeRFC5987(c.in); got != c.want {
t.Errorf("urlEncodeRFC5987(%q) = %q, want %q", c.in, got, c.want)
}
}
}
func TestParseAttachmentIDs(t *testing.T) {
valid := "3f2504e0-4f89-11d3-9a0c-0305e82c3301"
got, err := parseAttachmentIDs([]string{valid, " ", ""})
if err != nil {
t.Fatalf("合法输入报错: %v", err)
}
if len(got) != 1 {
t.Errorf("空白项应被忽略,得到 %d 个", len(got))
}
if _, err := parseAttachmentIDs([]string{"not-a-uuid"}); err == nil {
t.Error("非法 UUID 应报错")
}
if got, err := parseAttachmentIDs(nil); err != nil || len(got) != 0 {
t.Errorf("nil 应返回空列表,得到 %v, %v", got, err)
}
}