package backup import ( "context" "os" "path/filepath" "strings" "testing" "time" ) // fakeSnap writes a tiny file so backup files exist on disk for the tests, and // records how many times it ran. type fakeSnap struct{ calls int } func (f *fakeSnap) SnapshotTo(dest string, includeBlobs bool) error { f.calls++ return os.WriteFile(dest, []byte("snap"), 0o600) } func TestWriteBackupAndNewest(t *testing.T) { dir := t.TempDir() s := &fakeSnap{} now := time.Date(2026, 6, 10, 9, 0, 0, 0, time.UTC) if err := writeBackup(s, dir, now); err != nil { t.Fatal(err) } files := backupFiles(dir) if len(files) != 1 || files[0] != "hsa-backup-2026-06-10-090000.db" { t.Fatalf("files = %v", files) } if got := newestBackup(dir); !got.Equal(now) { t.Errorf("newestBackup = %v, want %v", got, now) } } func TestUntilNext(t *testing.T) { dir := t.TempDir() every := 168 * time.Hour now := time.Date(2026, 6, 10, 9, 0, 0, 0, time.UTC) // No backups yet → back up immediately. if d := untilNext(dir, every, now); d != 0 { t.Errorf("empty dir: untilNext = %v, want 0", d) } // A fresh backup → wait nearly the full interval. _ = writeBackup(&fakeSnap{}, dir, now.Add(-1*time.Hour)) if d := untilNext(dir, every, now); d <= 0 || d > every { t.Errorf("fresh backup: untilNext = %v, want ~167h", d) } // An old backup → back up now. dir2 := t.TempDir() _ = writeBackup(&fakeSnap{}, dir2, now.Add(-200*time.Hour)) if d := untilNext(dir2, every, now); d != 0 { t.Errorf("stale backup: untilNext = %v, want 0", d) } } func TestPruneKeepsMostRecent(t *testing.T) { dir := t.TempDir() s := &fakeSnap{} base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) for i := 0; i < 5; i++ { _ = writeBackup(s, dir, base.AddDate(0, 0, i)) } if err := prune(dir, 2); err != nil { t.Fatal(err) } files := backupFiles(dir) if len(files) != 2 { t.Fatalf("after prune: %v", files) } // The two newest (Jan 04, Jan 05) survive. if !strings.Contains(files[0], "2026-01-04") || !strings.Contains(files[1], "2026-01-05") { t.Errorf("wrong files kept: %v", files) } } func TestScheduleRunsOnceThenStops(t *testing.T) { dir := t.TempDir() s := &fakeSnap{} ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) // A real store would be passed in production; here the fake is enough. go Schedule(ctx, s, dir, time.Hour, 4, func() time.Time { return time.Date(2026, 6, 10, 9, 0, 0, 0, time.UTC) }) // Wait for the immediate first backup to land. deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { if _, err := os.Stat(filepath.Join(dir, "hsa-backup-2026-06-10-090000.db")); err == nil { return } time.Sleep(10 * time.Millisecond) } t.Fatal("Schedule did not write an initial backup") }