hsa-app/internal/storage/snapshot_test.go
Jean-Michel Tremblay fbfdc6877d Add scheduled metadata-only DB backups (spec item 11)
- internal/backup: weekly (configurable) snapshots via the existing VACUUM INTO
  + blob-strip path; restart-safe (only backs up if newest is older than the
  interval), retains BACKUP_KEEP most recent, prunes the rest. Runs in a
  background goroutine; failures are logged, never fatal.
- Config: BACKUP_DIR (./data/backups), BACKUP_INTERVAL (168h; 0 disables),
  BACKUP_KEEP (8).
- Fix: blob-strip now clears attachments.image_data too, not just receipts.
- Fix: STORAGE_DIR is the storage ROOT (default ./data) — receipts/ and
  attachments/ live under it; corrects the doubled-nesting from item 10.

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

93 lines
2.2 KiB
Go

package storage
import (
"path/filepath"
"testing"
"maisym.com/hsa/internal/receipt"
)
func TestSnapshotWithBlobs(t *testing.T) {
s := newTestStore(t)
r := sampleReceipt(firstCategoryID(t, s))
if err := s.Insert(r); err != nil {
t.Fatal(err)
}
dest := filepath.Join(t.TempDir(), "snap.db")
if err := s.SnapshotTo(dest, true); err != nil {
t.Fatalf("SnapshotTo: %v", err)
}
snap, err := Open(dest)
if err != nil {
t.Fatal(err)
}
defer snap.Close()
got, err := snap.Get(r.ID)
if err != nil {
t.Fatal(err)
}
if string(got.ImageData) != string(r.ImageData) {
t.Errorf("blob not preserved: got %q", got.ImageData)
}
}
func TestSnapshotWithoutBlobs(t *testing.T) {
s := newTestStore(t)
r := sampleReceipt(firstCategoryID(t, s))
if err := s.Insert(r); err != nil {
t.Fatal(err)
}
// An attachment blob must be stripped too.
att := receipt.Attachment{
ID: "att1", ReceiptID: r.ID, UploadedBy: "jm", UploadedAt: r.UploadedAt,
FilePath: "attachments/2026/x.png", ImageData: []byte("blobbytes"),
FileSizeBytes: 9, OriginalFilename: "x.png", MimeType: "image/png",
}
if err := s.InsertAttachment(att); err != nil {
t.Fatal(err)
}
dest := filepath.Join(t.TempDir(), "snap-noblob.db")
if err := s.SnapshotTo(dest, false); err != nil {
t.Fatalf("SnapshotTo: %v", err)
}
snap, err := Open(dest)
if err != nil {
t.Fatal(err)
}
defer snap.Close()
// Metadata row survives, but the blob is stripped.
n, err := snap.CountActive()
if err != nil {
t.Fatal(err)
}
if n != 1 {
t.Errorf("CountActive = %d, want 1 (metadata should survive)", n)
}
got, err := snap.Get(r.ID)
if err != nil {
t.Fatal(err)
}
if len(got.ImageData) != 0 {
t.Errorf("receipt blob not stripped: got %d bytes", len(got.ImageData))
}
if got.AmountCents != r.AmountCents {
t.Errorf("metadata lost: amount = %d, want %d", got.AmountCents, r.AmountCents)
}
// Attachment metadata survives, blob stripped.
gotAtt, err := snap.GetAttachment("att1")
if err != nil {
t.Fatal(err)
}
if len(gotAtt.ImageData) != 0 {
t.Errorf("attachment blob not stripped: got %d bytes", len(gotAtt.ImageData))
}
if gotAtt.OriginalFilename != "x.png" {
t.Errorf("attachment metadata lost: %+v", gotAtt)
}
}