hsa-app/internal/backup/backup_test.go
Jean-Michel Tremblay 885fae06ec Backups: ROOT/dbbackup, hsa_sqlite_backup_YYYY_MM_DD.db naming
- Backup dir default ./data/dbbackup; files named hsa_sqlite_backup_<YYYY_MM_DD>.db
  (one per day; a same-day re-run is a no-op since the dated file exists).
- Simplify the scheduler: attempt on start, then tick every BACKUP_INTERVAL.
  Per-day idempotency makes restarts and sub-day intervals settle at one/day —
  removes the date-parsing untilNext/newestBackup logic (and its busy-loop edge).
- BACKUP_INTERVAL remains the frequency knob (Go duration; 0 disables).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 07:34:31 -04:00

84 lines
2.3 KiB
Go

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 TestWriteBackup_NamesAndSameDayNoOp(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_sqlite_backup_2026_06_10.db" {
t.Fatalf("files = %v", files)
}
// A same-day second call is a no-op: no error, still one file, no re-snapshot.
if err := writeBackup(s, dir, now.Add(time.Hour)); err != nil {
t.Fatal(err)
}
if files := backupFiles(dir); len(files) != 1 {
t.Errorf("same-day re-run produced %v, want 1 file", files)
}
if s.calls != 1 {
t.Errorf("SnapshotTo called %d times, want 1 (second was a no-op)", s.calls)
}
}
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_sqlite_backup_2026_06_10.db")); err == nil {
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatal("Schedule did not write an initial backup")
}