package web import ( "net/http" "net/http/httptest" "strconv" "strings" "testing" "time" "maisym.com/hsa/internal/receipt" ) // addReceipt inserts a receipt directly via the store for view tests. func addReceipt(t *testing.T, s *Server, id string, amountCents int64, date time.Time) { t.Helper() cats, err := s.store.ListCategories() if err != nil || len(cats) == 0 { t.Fatalf("ListCategories: %v", err) } r := receipt.Receipt{ ID: id, UploadedBy: "jm@example.com", UploadedAt: time.Now().UTC(), ReceiptDate: date, AmountCents: amountCents, CategoryID: cats[0].ID, FilePath: id + ".png", ImageData: []byte("\x89PNG\r\n\x1a\nfake"), FileSizeBytes: 12, OriginalFilename: id + ".png", MimeType: "image/png", } if err := s.store.Insert(r); err != nil { t.Fatalf("Insert: %v", err) } } func get(t *testing.T, s *Server, path string) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodGet, path, nil) req.AddCookie(authCookie(t, s)) rec := httptest.NewRecorder() s.Routes().ServeHTTP(rec, req) return rec } func TestDuplicates_JSONMatch(t *testing.T) { s := testServerWithStore(t) addReceipt(t, s, "dup1", 4250, time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)) rec := get(t, s, "/duplicates?date=2026-06-01&amount=42.50") if rec.Code != http.StatusOK { t.Fatalf("status = %d", rec.Code) } body := rec.Body.String() if !strings.Contains(body, "42.50") || !strings.Contains(body, "dup1.png") { t.Errorf("body missing match: %s", body) } } func TestDuplicates_NoMatchEmpty(t *testing.T) { s := testServerWithStore(t) addReceipt(t, s, "x", 100, time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)) rec := get(t, s, "/duplicates?date=2026-06-01&amount=99.99") if !strings.Contains(rec.Body.String(), `"matches":[]`) { t.Errorf("expected empty matches, got %s", rec.Body.String()) } } func TestTallyPage_RendersTotals(t *testing.T) { s := testServerWithStore(t) addReceipt(t, s, "t1", 1000, time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)) addReceipt(t, s, "t2", 2500, time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)) rec := get(t, s, "/tally") if rec.Code != http.StatusOK { t.Fatalf("status = %d", rec.Code) } body := rec.Body.String() if !strings.Contains(body, "2026") || !strings.Contains(body, "35.00") { t.Errorf("tally body missing year/grand: %s", body) } } func TestRecentPage_ListsAndPages(t *testing.T) { s := testServerWithStore(t) base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) for i := 0; i < 12; i++ { addReceipt(t, s, "rec"+strconv.Itoa(i), int64(100+i), base.AddDate(0, 0, i)) } rec := get(t, s, "/recent") if rec.Code != http.StatusOK { t.Fatalf("status = %d", rec.Code) } body := rec.Body.String() if !strings.Contains(body, "Load next 10") { t.Errorf("expected pager on first page: %s", body) } if strings.Count(body, "/receipt/rec") != 10 { t.Errorf("expected 10 rows, got %d", strings.Count(body, "/receipt/rec")) } } func TestShortWho(t *testing.T) { cases := map[string]string{ "Jean-Michel Tremblay": "JM. Tremblay", "Lynna Nguyen": "L. Nguyen", "Jude Tremblay": "J. Tremblay", "Madonna": "Madonna", // single word unchanged "": "", } for in, want := range cases { if got := shortWho(in); got != want { t.Errorf("shortWho(%q) = %q, want %q", in, got, want) } } } func TestReceiptFile_ServesBlob(t *testing.T) { s := testServerWithStore(t) addReceipt(t, s, "file1", 100, time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)) rec := get(t, s, "/receipt/file1/file") if rec.Code != http.StatusOK { t.Fatalf("status = %d", rec.Code) } if ct := rec.Header().Get("Content-Type"); ct != "image/png" { t.Errorf("content-type = %q, want image/png", ct) } if !strings.HasPrefix(rec.Body.String(), "\x89PNG") { t.Errorf("body not the stored blob") } }