hsa-app/internal/storage/storage_test.go

202 lines
4.7 KiB
Go
Raw Normal View History

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))
}
}