Files
veola/internal/db/dedup_test.go
2026-06-20 11:57:54 -07:00

144 lines
3.6 KiB
Go

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