package storage import ( "path/filepath" "testing" "time" "maisym.com/hsa/internal/receipt" ) func newTestStore(t *testing.T) *Store { t.Helper() dbPath := filepath.Join(t.TempDir(), "test.db") s, err := Open(dbPath) if err != nil { t.Fatalf("Open: %v", err) } t.Cleanup(func() { s.Close() }) return s } func sampleReceipt(categoryID int64) receipt.Receipt { return receipt.Receipt{ ID: "11111111-1111-1111-1111-111111111111", UploadedBy: "jm@example.com", UploadedAt: time.Now().UTC().Truncate(time.Second), ReceiptDate: time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC), AmountCents: 1234, CategoryID: categoryID, FilePath: "ab/abcd.jpg", ImageData: []byte("fake-image-bytes"), FileSizeBytes: 16, OriginalFilename: "receipt.jpg", MimeType: "image/jpeg", } } // firstCategoryID returns the id of a seeded category for use in tests. func firstCategoryID(t *testing.T, s *Store) int64 { t.Helper() cats, err := s.ListCategories() if err != nil || len(cats) == 0 { t.Fatalf("ListCategories: %v (len %d)", err, len(cats)) } return cats[0].ID } func TestInsertAndCount(t *testing.T) { s := newTestStore(t) if err := s.Insert(sampleReceipt(firstCategoryID(t, s))); err != nil { t.Fatalf("Insert: %v", err) } n, err := s.CountActive() if err != nil { t.Fatalf("CountActive: %v", err) } if n != 1 { t.Errorf("CountActive = %d, want 1", n) } } func TestInsertPreservesBlobAndFields(t *testing.T) { s := newTestStore(t) r := sampleReceipt(firstCategoryID(t, s)) if err := s.Insert(r); err != nil { t.Fatalf("Insert: %v", err) } got, err := s.Get(r.ID) if err != nil { t.Fatalf("Get: %v", err) } if got.AmountCents != r.AmountCents { t.Errorf("AmountCents = %d, want %d", got.AmountCents, r.AmountCents) } if string(got.ImageData) != string(r.ImageData) { t.Errorf("ImageData = %q, want %q", got.ImageData, r.ImageData) } if got.CategoryID != r.CategoryID || got.OriginalFilename != r.OriginalFilename { t.Errorf("field mismatch: %+v", got) } } func TestInsertWithPerson(t *testing.T) { s := newTestStore(t) pid, err := s.AddPerson("John") if err != nil { t.Fatal(err) } r := sampleReceipt(firstCategoryID(t, s)) r.PersonID = &pid if err := s.Insert(r); err != nil { t.Fatalf("Insert: %v", err) } got, err := s.Get(r.ID) if err != nil { t.Fatal(err) } if got.PersonID == nil || *got.PersonID != pid { t.Errorf("PersonID = %v, want %d", got.PersonID, pid) } } func TestInsertRejectsUnknownCategoryFK(t *testing.T) { s := newTestStore(t) r := sampleReceipt(99999) // no such category if err := s.Insert(r); err == nil { t.Error("expected FK violation for unknown category_id, got nil") } } func TestRenameCategoryPropagatesViaID(t *testing.T) { s := newTestStore(t) cid := firstCategoryID(t, s) r := sampleReceipt(cid) if err := s.Insert(r); err != nil { t.Fatal(err) } if err := s.RenameCategory(cid, "Medical (fixed)"); err != nil { t.Fatal(err) } // The receipt still points at the same id; the label change is automatic. got, _ := s.Get(r.ID) if got.CategoryID != cid { t.Errorf("CategoryID changed after rename: %d", got.CategoryID) } cats, _ := s.ListCategories() var found bool for _, c := range cats { if c.ID == cid && c.Label == "Medical (fixed)" { found = true } } if !found { t.Errorf("rename not reflected in ListCategories: %+v", cats) } } func TestSoftDeleteHidesFromCount(t *testing.T) { s := newTestStore(t) r := sampleReceipt(firstCategoryID(t, s)) if err := s.Insert(r); err != nil { t.Fatalf("Insert: %v", err) } if err := s.SoftDelete(r.ID); err != nil { t.Fatalf("SoftDelete: %v", err) } n, err := s.CountActive() if err != nil { t.Fatalf("CountActive: %v", err) } if n != 0 { t.Errorf("CountActive after soft delete = %d, want 0", n) } } func TestReopenAfterWritePassesIntegrity(t *testing.T) { path := filepath.Join(t.TempDir(), "reopen.db") s, err := Open(path) if err != nil { t.Fatalf("Open: %v", err) } if err := s.Insert(sampleReceipt(firstCategoryID(t, s))); err != nil { t.Fatalf("Insert: %v", err) } s.Close() // Reopening runs WAL recovery + quick_check; committed data must survive. s2, err := Open(path) if err != nil { t.Fatalf("reopen: %v", err) } defer s2.Close() n, err := s2.CountActive() if err != nil { t.Fatal(err) } if n != 1 { t.Errorf("CountActive after reopen = %d, want 1", n) } } func TestSeededCategories(t *testing.T) { s := newTestStore(t) cats, err := s.ListCategories() if err != nil { t.Fatal(err) } if len(cats) != 5 { t.Errorf("seeded categories = %d, want 5", len(cats)) } people, err := s.ListPeople() if err != nil { t.Fatal(err) } if len(people) != 0 { t.Errorf("people should start empty, got %d", len(people)) } }