package backup import ( "context" "os" "path/filepath" "testing" "time" ) func TestDueForBackup(t *testing.T) { now := time.Date(2026, 9, 25, 10, 0, 0, 0, time.UTC) // 还没有备份 → 立刻做一份 if !dueForBackup(time.Time{}, now, AutoInterval) { t.Fatal("没有备份时应立即备份") } // 刚备份过 → 跳过:这正是重启不会重复备份的原因 if dueForBackup(now.Add(-time.Hour), now, AutoInterval) { t.Fatal("间隔内的备份不应重复执行") } // 正好到间隔 → 该做了 if !dueForBackup(now.Add(-AutoInterval), now, AutoInterval) { t.Fatal("到达间隔应再备份一次") } // 超过间隔 → 该做 if !dueForBackup(now.Add(-25*time.Hour), now, AutoInterval) { t.Fatal("超过间隔应再备份一次") } } func TestExportVerifyRoundTrip(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "fonu.db"), []byte("db"), 0o600); err != nil { t.Fatal(err) } svc := New(dir) path, err := svc.Export(context.Background()) if err != nil { t.Fatalf("export: %v", err) } entries, err := svc.Verify(context.Background(), path) if err != nil { t.Fatalf("verify: %v", err) } if entries < 1 { t.Fatalf("条目数应至少为 1,实际 %d", entries) } archives, err := svc.List() if err != nil { t.Fatalf("list: %v", err) } if len(archives) != 1 { t.Fatalf("应列出 1 份备份,实际 %d", len(archives)) } if archives[0].Size == 0 || !archiveNamePattern.MatchString(archives[0].Name) { t.Fatalf("备份条目异常:%+v", archives[0]) } } func TestVerifyRejectsGarbage(t *testing.T) { svc := New(t.TempDir()) path := filepath.Join(svc.backupsDir(), "fonu-backup-20260925-000000-1.tar.gz") if err := os.MkdirAll(svc.backupsDir(), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte("not a gzip stream"), 0o600); err != nil { t.Fatal(err) } if _, err := svc.Verify(context.Background(), path); err == nil { t.Fatal("非 gzip 内容应校验失败") } } func TestListPruneAndPath(t *testing.T) { svc := New(t.TempDir()) if err := os.MkdirAll(svc.backupsDir(), 0o755); err != nil { t.Fatal(err) } for _, name := range []string{ "fonu-backup-20260923-100000-1.tar.gz", "fonu-backup-20260924-100000-2.tar.gz", "fonu-backup-20260925-100000-3.tar.gz", } { if err := os.WriteFile(filepath.Join(svc.backupsDir(), name), []byte("x"), 0o600); err != nil { t.Fatal(err) } } // 不符合命名格式的文件不参与:备份目录里可能混着手工放进来的东西 if err := os.WriteFile(filepath.Join(svc.backupsDir(), "notes.txt"), []byte("x"), 0o600); err != nil { t.Fatal(err) } archives, err := svc.List() if err != nil { t.Fatalf("list: %v", err) } if len(archives) != 3 { t.Fatalf("应只列出 3 份备份,实际 %d", len(archives)) } if archives[0].Name != "fonu-backup-20260925-100000-3.tar.gz" { t.Fatalf("应按时间倒序,实际首条 %s", archives[0].Name) } removed, err := svc.Prune(2) if err != nil { t.Fatalf("prune: %v", err) } if removed != 1 { t.Fatalf("应删除 1 份,实际 %d", removed) } if archives, _ = svc.List(); len(archives) != 2 { t.Fatalf("剪枝后应剩 2 份,实际 %d", len(archives)) } // 名称校验:不能让备份名把读写带出备份目录 for _, name := range []string{"../fonu.db", "notes.txt", "", "fonu-backup-x.tar.gz"} { if _, err := svc.Path(name); err == nil { t.Fatalf("非法备份名应拒绝:%q", name) } } if err := svc.Remove("fonu-backup-20260920-100000-9.tar.gz"); err == nil { t.Fatal("删除不存在的备份应报错") } if _, err := svc.Prune(0); err != nil { t.Fatalf("keep<=0 应直接返回:%v", err) } }