package db import ( "context" "os" "path/filepath" "testing" "time" "veola/internal/crypto" "veola/internal/models" ) func newTestStore(t *testing.T) *Store { t.Helper() dir := t.TempDir() conn, err := Open(filepath.Join(dir, "test.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { conn.Close() }) key, _ := crypto.DeriveKey([]byte("0123456789abcdef0123456789abcdef-aaa")) return NewStore(conn, key) } func TestDedupByItemAndURL(t *testing.T) { if os.Getenv("CI_SKIP_SQLITE") != "" { t.Skip() } s := newTestStore(t) ctx := context.Background() id, err := s.CreateItem(ctx, &models.Item{ Name: "TwinBee", NtfyTopic: "veola", Active: true, PollIntervalMinutes: 60, NtfyPriority: "default", }) if err != nil { t.Fatal(err) } r := &models.Result{ ItemID: id, Title: "TwinBee Famicom", Currency: "USD", URL: "https://example.com/listing/1", } if _, err := s.InsertResult(ctx, r); err != nil { t.Fatal(err) } exists, err := s.ResultExists(ctx, id, "https://example.com/listing/1") if err != nil { t.Fatal(err) } if !exists { t.Error("expected result to be detected as duplicate") } missing, _ := s.ResultExists(ctx, id, "https://example.com/listing/2") if missing { t.Error("expected unknown URL to not be flagged as duplicate") } // Different item, same URL should not collide. id2, _ := s.CreateItem(ctx, &models.Item{Name: "Other", NtfyTopic: "veola", PollIntervalMinutes: 60, NtfyPriority: "default"}) other, _ := s.ResultExists(ctx, id2, "https://example.com/listing/1") if other { t.Error("dedup should be scoped to item_id") } // Empty URL should not collide. emptyExists, _ := s.ResultExists(ctx, id, "") if emptyExists { t.Error("empty URL should never be flagged as duplicate") } } func TestDashboardMoneySavedWindow(t *testing.T) { if os.Getenv("CI_SKIP_SQLITE") != "" { t.Skip() } s := newTestStore(t) ctx := context.Background() bp := 100.0 id, err := s.CreateItem(ctx, &models.Item{ Name: "Windowed", NtfyTopic: "veola", Active: true, PollIntervalMinutes: 60, NtfyPriority: "default", BestPrice: &bp, }) if err != nil { t.Fatal(err) } now := time.Now() // A stale point well outside the 30-day window at a much higher price. If // the average were computed over all history this would inflate "saved". if err := s.InsertPricePoint(ctx, &models.PricePoint{ ItemID: id, Price: 500, PolledAt: now.AddDate(0, 0, -60), }); err != nil { t.Fatal(err) } // A recent point inside the window. Best price (100) is below it, so the // only legitimate saving is 120 - 100 = 20. if err := s.InsertPricePoint(ctx, &models.PricePoint{ ItemID: id, Price: 120, PolledAt: now.AddDate(0, 0, -5), }); err != nil { t.Fatal(err) } stats, err := s.GetDashboardStats(ctx) if err != nil { t.Fatal(err) } if stats.SavedItemCount != 1 { t.Fatalf("expected 1 saved item, got %d", stats.SavedItemCount) } if got := stats.MoneySaved; got < 19.99 || got > 20.01 { t.Errorf("expected money saved ~20 (recent avg only), got %.2f", got) } } func TestCJKRoundTrip(t *testing.T) { s := newTestStore(t) ctx := context.Background() id, err := s.CreateItem(ctx, &models.Item{ Name: "ツインビー", SearchQuery: "ツインビー グラディウス パロディウス", NtfyTopic: "veola", Active: true, PollIntervalMinutes: 60, NtfyPriority: "default", }) if err != nil { t.Fatal(err) } got, err := s.GetItem(ctx, id) if err != nil { t.Fatal(err) } if got.Name != "ツインビー" || got.SearchQuery != "ツインビー グラディウス パロディウス" { t.Errorf("CJK round-trip failed: name=%q query=%q", got.Name, got.SearchQuery) } }