176 lines
4.2 KiB
Go
176 lines
4.2 KiB
Go
|
|
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 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))
|
||
|
|
}
|
||
|
|
}
|