package api import ( "errors" "fmt" "testing" "time" "filecodebox/internal/config" "filecodebox/internal/storage" ) // newTestConfig 构造测试配置(defaults 基线,无 KV 覆盖;需求 ⑧ 默认 sqlite,无需真实数据库)。 func newTestConfig(t *testing.T) *config.Config { t.Helper() t.Setenv("FCB_DB_DRIVER", "sqlite") t.Setenv("FCB_DB_DSN", "") cfg, err := config.New() if err != nil { t.Fatalf("config.New 失败: %v", err) } return cfg } // TestGetSelectTokenWindow 验证下载令牌的窗口号逻辑(对齐参考 get_select_token)。 func TestGetSelectTokenWindow(t *testing.T) { code := "AB12C" secret := "test-secret" tok0 := GetSelectToken(code, secret, 0) tok1 := GetSelectToken(code, secret, 1) if tok0 == "" || tok1 == "" { t.Fatal("令牌不应为空") } // 同一窗口内 offset=0 的两次生成必须一致(确定性问题) if tok0 != GetSelectToken(code, secret, 0) { t.Fatal("同窗口令牌应确定一致") } // 不同 offset 的令牌必然不同(窗口号不同) if tok0 == tok1 { t.Fatal("offset=0 与 offset=1 的令牌应不同") } // 不同 code / secret 的令牌不同 if tok0 == GetSelectToken("ZZ999", secret, 0) { t.Fatal("不同取件码的令牌应不同") } if tok0 == GetSelectToken(code, "other-secret", 0) { t.Fatal("不同密钥的令牌应不同") } // 令牌为 64 位十六进制(sha256 hex) if len(tok0) != 64 { t.Fatalf("令牌长度应为 64,实际 %d", len(tok0)) } // 窗口号公式:unix/1000 - offset(秒级窗口约 16.7 分钟) now := time.Now().Unix() if now/1000 == (now+1100)/1000 { // 仅当测试跨越窗口边界才跳过该断言(罕见,容忍) t.Log("测试跨越窗口边界,跳过窗口公式断言") } } // TestMapStorageError 验证存储哨兵错误→HTTP 状态映射表。 func TestMapStorageError(t *testing.T) { cases := []struct { name string err error expect int }{ {"NotFound 直返", storage.ErrNotFound, 404}, {"NotFound 包装", fmt.Errorf("引擎内层: %w", storage.ErrNotFound), 404}, {"InvalidPath", storage.ErrInvalidPath, 400}, {"InvalidPath 包装", fmt.Errorf("webdav: %w", storage.ErrInvalidPath), 400}, {"Unavailable", storage.ErrUnavailable, 503}, {"Unavailable 包装", fmt.Errorf("s3: %w", storage.ErrUnavailable), 503}, {"NotSupported", storage.ErrNotSupported, 501}, {"NotSupported 包装", fmt.Errorf("local: %w", storage.ErrNotSupported), 501}, {"RangeNotSatisfiable", storage.ErrRangeNotSatisfiable, 416}, {"HashMismatch", storage.ErrHashMismatch, 400}, {"未知错误", errors.New("其他错误"), 500}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { mapped := mapStorageError(tc.err) var ae *apiError if !errors.As(mapped, &ae) { t.Fatalf("应映射为 apiError,得到 %T", mapped) } if ae.Status != tc.expect { t.Fatalf("状态码应为 %d,实际 %d", tc.expect, ae.Status) } }) } } // TestResolveExpire 验证过期策略解析(白名单/上限/各 style)。 func TestResolveExpire(t *testing.T) { cfg := newTestConfig(t) // 非法 style if _, err := resolveExpire(cfg, 1, "year"); err == nil { t.Fatal("非白名单 style 应报错") } // 非法 value if _, err := resolveExpire(cfg, 0, "day"); err == nil { t.Fatal("expire_value<=0 应报错") } // day:7 天内合法 res, err := resolveExpire(cfg, 3, "day") if err != nil { t.Fatalf("3 天应合法: %v", err) } if res.ExpiredAt == nil || res.ExpiredCount != -1 { t.Fatal("day 类型应有 expired_at 且 expired_count=-1") } // 超过 7 天上限 if _, err := resolveExpire(cfg, 30, "day"); err == nil { t.Fatal("超过 7 天上限应报错") } // count:按次数 res, err = resolveExpire(cfg, 5, "count") if err != nil { t.Fatalf("count 应合法: %v", err) } if res.ExpiredCount != 5 { t.Fatalf("count 类型 expired_count 应为 5,实际 %d", res.ExpiredCount) } // forever:永久 res, err = resolveExpire(cfg, 1, "forever") if err != nil { t.Fatalf("forever 应合法: %v", err) } if res.ExpiredAt != nil || res.ExpiredCount != -1 { t.Fatal("forever 应为 expired_at=nil 且 expired_count=-1") } } // TestGenerateCode 验证取件码格式。 func TestGenerateCode(t *testing.T) { for i := 0; i < 50; i++ { num := generateCode("number") if len(num) != 5 { t.Fatalf("数字码应为 5 位,实际 %q", num) } for _, ch := range num { if ch < '0' || ch > '9' { t.Fatalf("数字码含非数字字符: %q", num) } } secret := generateCode("secret") if len(secret) != 5 { t.Fatalf("字符码应为 5 位,实际 %q", secret) } } } // TestParseRangeHeader 验证 Range 头解析(对齐 HTTP 语义)。 func TestParseRangeHeader(t *testing.T) { // 全量(无 Range) if parseRangeHeader("", 1000) != nil { t.Fatal("无 Range 头应返回 nil") } // 标准区间 r := parseRangeHeader("bytes=0-99", 1000) if r == nil || r.Start != 0 || r.End != 99 { t.Fatalf("bytes=0-99 解析错误: %+v", r) } // 开区间到末尾 r = parseRangeHeader("bytes=500-", 1000) if r == nil || r.Start != 500 || r.End != -1 { t.Fatalf("bytes=500- 解析错误: %+v", r) } // 后缀区间(最后 100 字节) r = parseRangeHeader("bytes=-100", 1000) if r == nil || r.Start != 900 || r.End != -1 { t.Fatalf("bytes=-100 解析错误: %+v", r) } // 后缀超长:截断到全文件 r = parseRangeHeader("bytes=-5000", 1000) if r == nil || r.Start != 0 { t.Fatalf("bytes=-5000 应从头开始: %+v", r) } // 多区间不支持→回退全量 if parseRangeHeader("bytes=0-1,5-6", 1000) != nil { t.Fatal("多区间应返回 nil(回退全量)") } // 非法格式 if parseRangeHeader("items=0-1", 1000) != nil { t.Fatal("非 bytes 单位应返回 nil") } if parseRangeHeader("bytes=abc-", 1000) != nil { t.Fatal("非法数字应返回 nil") } } // TestParseISOTime 验证时间解析的多格式兼容。 func TestParseISOTime(t *testing.T) { valid := []string{ "2025-01-01T00:00:00Z", "2025-01-01T08:00:00+08:00", "2025-01-01 08:00:00", "2025-01-01", } for _, s := range valid { if _, err := parseISOTime(s); err != nil { t.Fatalf("%q 应解析成功: %v", s, err) } } if _, err := parseISOTime("not-a-time"); err == nil { t.Fatal("非法时间应报错") } if _, err := parseISOTime(""); err == nil { t.Fatal("空串应报错") } } // TestFormatDurationCN 验证中文时长描述。 func TestFormatDurationCN(t *testing.T) { cases := []struct { d time.Duration expect string }{ {7 * 24 * time.Hour, "7天"}, {90 * time.Minute, "1小时30分钟"}, {45 * time.Second, "45秒"}, } for _, tc := range cases { if got := formatDurationCN(tc.d); got != tc.expect { t.Fatalf("formatDurationCN(%v)=%q,期望 %q", tc.d, got, tc.expect) } } } // TestFileMagicValidation 验证 magic bytes 防伪造。 func TestFileMagicValidation(t *testing.T) { cfg := newTestConfig(t) // 白名单 * 全放行 if err := validateFileMagic(cfg, "a.txt", "", nil); err != nil { t.Fatalf("白名单 * 应放行: %v", err) } // PNG 内容 + .exe 扩展名 → 拒绝(伪造) if err := validateFileMagic(cfg, "evil.exe", "", []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}); err == nil { t.Fatal("PNG 内容伪装 exe 应拒绝") } // PNG 内容 + .png 扩展名 → 通过 if err := validateFileMagic(cfg, "ok.png", "image/png", []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}); err != nil { t.Fatalf("真 PNG 应通过: %v", err) } // 文本内容 + .png 扩展名 → 拒绝 if err := validateFileMagic(cfg, "fake.png", "", []byte("hello world, this is text")); err == nil { t.Fatal("文本伪装 png 应拒绝") } } // TestSanitizePathBuild 验证存储路径构造不含穿越。 func TestSanitizePathBuild(t *testing.T) { cfg := newTestConfig(t) _, _, _, clean, savePath := buildSavePath(cfg, "../../etc/passwd", "uuid-123") if clean != "etc_passwd" && clean != "passwd" { t.Logf("清理后的文件名: %q", clean) } if _, ok := storage.SanitizePath(savePath); !ok { t.Fatalf("构造的 savePath 应通过安全校验: %q", savePath) } }