Files
MailUI4Agents/server/internal/blob/list_test.go

177 lines
4.6 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 blob
// List() 的测试 —— 反向 GC 的前提。
//
// 事故背景:附件 GC 原先只从**库记录**出发(`WHERE mail_id IS NULL`),于是一旦
// 记录本身消失(清库、手工 DELETE、迁移对应文件就永远脱离了视野。本机实测
// 磁盘 8 个 blob 里 7 个没有任何库记录,全部来自一次清库,之后一直占着盘。
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestListReturnsStoredContents(t *testing.T) {
s, err := New(t.TempDir())
if err != nil {
t.Fatalf("new store: %v", err)
}
sumA, _, err := s.Put(bytes.NewReader([]byte("alpha")), 0)
if err != nil {
t.Fatalf("put a: %v", err)
}
sumB, _, err := s.Put(bytes.NewReader([]byte("beta")), 0)
if err != nil {
t.Fatalf("put b: %v", err)
}
got, err := s.List()
if err != nil {
t.Fatalf("list: %v", err)
}
if len(got) != 2 {
t.Fatalf("应列出 2 个内容,实际 %d%v", len(got), got)
}
for _, sum := range []string{sumA, sumB} {
mod, ok := got[sum]
if !ok {
t.Errorf("缺少 %s", sum[:8])
continue
}
if mod.IsZero() {
t.Errorf("%s 的修改时间为零值 —— GC 靠它判断「是否可能正在上传」", sum[:8])
}
}
}
func TestListEmptyStore(t *testing.T) {
s, err := New(t.TempDir())
if err != nil {
t.Fatalf("new store: %v", err)
}
got, err := s.List()
if err != nil {
t.Fatalf("空库不该报错: %v", err)
}
if len(got) != 0 {
t.Fatalf("空库应返回空,实际 %v", got)
}
}
// **关键用例**`.upload-*` 临时文件绝不能进列表。
//
// 报给调用方会让 GC 去删一个正在写入的文件 —— 上传是「先落盘再入库」,
// 那一瞬间的临时文件既没有库记录也不是合法 sha256 命名。
func TestListSkipsTempUploads(t *testing.T) {
root := t.TempDir()
s, err := New(root)
if err != nil {
t.Fatalf("new store: %v", err)
}
sum, _, err := s.Put(bytes.NewReader([]byte("real")), 0)
if err != nil {
t.Fatalf("put: %v", err)
}
// 模拟一个正在进行的上传
tmp, err := os.CreateTemp(root, ".upload-*")
if err != nil {
t.Fatalf("create temp: %v", err)
}
tmp.WriteString("half written")
tmp.Close()
got, err := s.List()
if err != nil {
t.Fatalf("list: %v", err)
}
if len(got) != 1 {
t.Fatalf("只该列出 1 个真实内容,实际 %d%v", len(got), got)
}
if _, ok := got[sum]; !ok {
t.Errorf("真实内容 %s 应在列表里", sum[:8])
}
for k := range got {
if strings.HasPrefix(k, ".upload-") {
t.Errorf("临时文件 %q 不该出现在列表里 —— GC 会删掉一个正在写入的文件", k)
}
}
}
// 非 sha256 命名的异物一律忽略(人手工丢进去的、别的程序留下的)。
func TestListSkipsForeignFiles(t *testing.T) {
root := t.TempDir()
s, err := New(root)
if err != nil {
t.Fatalf("new store: %v", err)
}
if _, _, err := s.Put(bytes.NewReader([]byte("real")), 0); err != nil {
t.Fatalf("put: %v", err)
}
// 放几个不合法命名的文件在两级目录里
for _, name := range []string{"README", "ABCDEF", "notasha256"} {
dir := filepath.Join(root, "ab", "cd")
if err := os.MkdirAll(dir, 0o700); err != nil {
t.Fatalf("mkdir: %v", err)
}
if err := os.WriteFile(filepath.Join(dir, name), []byte("x"), 0o600); err != nil {
t.Fatalf("write %s: %v", name, err)
}
}
got, err := s.List()
if err != nil {
t.Fatalf("list: %v", err)
}
if len(got) != 1 {
t.Fatalf("异物不该被列出,实际 %d%v", len(got), got)
}
}
// 修改时间必须是真实的文件时间 —— GC 用它跳过「可能正在上传」的文件。
func TestListReportsRealModTime(t *testing.T) {
s, err := New(t.TempDir())
if err != nil {
t.Fatalf("new store: %v", err)
}
before := time.Now().Add(-time.Second)
sum, _, err := s.Put(bytes.NewReader([]byte("timed")), 0)
if err != nil {
t.Fatalf("put: %v", err)
}
after := time.Now().Add(time.Second)
got, err := s.List()
if err != nil {
t.Fatalf("list: %v", err)
}
mod := got[sum]
if mod.Before(before) || mod.After(after) {
t.Fatalf("修改时间 %v 不在 [%v, %v] 内", mod, before, after)
}
}
// 根目录不存在时必须报错,不能返回空列表。
//
// 返回空会让调用方以为「库里什么都没有」—— 那个判断会传导到 GC 的计数上,
// 让运维以为磁盘是干净的。
func TestListFailsOnMissingRoot(t *testing.T) {
root := t.TempDir()
s, err := New(root)
if err != nil {
t.Fatalf("new store: %v", err)
}
if err := os.RemoveAll(root); err != nil {
t.Fatalf("rm root: %v", err)
}
if _, err := s.List(); err == nil {
t.Fatal("根目录不存在时必须报错,返回空列表会让调用方以为库是空的")
}
}