package event import ( "context" "database/sql" "errors" "path/filepath" "testing" "time" "github.com/bluesky-social/indigo/atproto/syntax" atdb "atmoquest/internal/db" ) // newTestDB returns a freshly-migrated SQLite DB scoped to the test. func newTestDB(t *testing.T) (context.Context, *sql.DB) { t.Helper() dir := t.TempDir() dsn := "file:" + filepath.Join(dir, "event.db") + "?_pragma=foreign_keys(ON)" conn, err := atdb.Open(dsn) if err != nil { t.Fatalf("db open: %v", err) } if err := atdb.Migrate(conn); err != nil { t.Fatalf("migrate: %v", err) } t.Cleanup(func() { _ = conn.Close() }) return context.Background(), conn } const ( testEventURI = "at://did:plc:eventorganizer/quest.atmo.event/3lkabc" testOrgDID = "did:plc:eventorganizer" ) func sampleEvent() Record { return Record{ URI: testEventURI, Name: "CascadiaJS 2026", StartTime: time.Now().Add(-1 * time.Hour).UTC(), EndTime: time.Now().Add(8 * time.Hour).UTC(), Location: "Portland, OR", OrganizerDID: syntax.DID(testOrgDID), } } func TestCache_InsertAndGet(t *testing.T) { ctx, db := newTestDB(t) rec := sampleEvent() if err := Cache(ctx, db, rec); err != nil { t.Fatalf("Cache: %v", err) } got, err := Get(ctx, db, rec.URI) if err != nil { t.Fatalf("Get: %v", err) } if got.Name != rec.Name { t.Errorf("Name = %q, want %q", got.Name, rec.Name) } if got.Location != rec.Location { t.Errorf("Location = %q, want %q", got.Location, rec.Location) } if got.OrganizerDID.String() != rec.OrganizerDID.String() { t.Errorf("OrganizerDID = %q, want %q", got.OrganizerDID, rec.OrganizerDID) } if got.Geofence != nil { t.Errorf("Geofence = %+v, want nil", got.Geofence) } } func TestCache_UpsertOverwrites(t *testing.T) { ctx, db := newTestDB(t) rec := sampleEvent() if err := Cache(ctx, db, rec); err != nil { t.Fatalf("Cache initial: %v", err) } rec.Name = "CascadiaJS 2026 — Day 2" rec.Location = "Portland Convention Center" if err := Cache(ctx, db, rec); err != nil { t.Fatalf("Cache update: %v", err) } got, err := Get(ctx, db, rec.URI) if err != nil { t.Fatalf("Get: %v", err) } if got.Name != "CascadiaJS 2026 — Day 2" { t.Errorf("Name after upsert = %q", got.Name) } if got.Location != "Portland Convention Center" { t.Errorf("Location after upsert = %q", got.Location) } } func TestCache_RoundTripsGeofence(t *testing.T) { ctx, db := newTestDB(t) rec := sampleEvent() rec.Geofence = &Geofence{Lat: 45.5152, Lng: -122.6784, RadiusMeters: 250} if err := Cache(ctx, db, rec); err != nil { t.Fatalf("Cache: %v", err) } got, err := Get(ctx, db, rec.URI) if err != nil { t.Fatalf("Get: %v", err) } if got.Geofence == nil { t.Fatalf("Geofence is nil; want %+v", rec.Geofence) } if got.Geofence.Lat != 45.5152 || got.Geofence.Lng != -122.6784 || got.Geofence.RadiusMeters != 250 { t.Errorf("Geofence = %+v, want %+v", got.Geofence, rec.Geofence) } } func TestCache_RejectsBadInput(t *testing.T) { ctx, db := newTestDB(t) cases := []struct { name string mut func(*Record) }{ {"empty URI", func(r *Record) { r.URI = "" }}, {"empty name", func(r *Record) { r.Name = "" }}, {"end before start", func(r *Record) { r.StartTime = time.Now() r.EndTime = r.StartTime.Add(-time.Hour) }}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { rec := sampleEvent() tc.mut(&rec) if err := Cache(ctx, db, rec); err == nil { t.Errorf("Cache succeeded; want validation error") } }) } } func TestGet_NotFoundReturnsSentinel(t *testing.T) { ctx, db := newTestDB(t) _, err := Get(ctx, db, "at://did:plc:nope/quest.atmo.event/none") if !errors.Is(err, ErrNotFound) { t.Errorf("err = %v, want ErrNotFound", err) } } func TestIsOngoing(t *testing.T) { start := time.Date(2026, 6, 1, 9, 0, 0, 0, time.UTC) end := time.Date(2026, 6, 1, 18, 0, 0, 0, time.UTC) rec := Record{StartTime: start, EndTime: end} cases := []struct { name string at time.Time want bool }{ {"before", start.Add(-time.Hour), false}, {"at start", start, true}, {"middle", start.Add(4 * time.Hour), true}, {"at end", end, true}, {"after", end.Add(time.Hour), false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := rec.IsOngoing(tc.at); got != tc.want { t.Errorf("IsOngoing(%v) = %v, want %v", tc.at, got, tc.want) } }) } } func TestCache_PreservesLinks(t *testing.T) { ctx, db := newTestDB(t) rec := sampleEvent() rec.Links = []Link{ {Label: "Schedule", URL: "https://example.com/schedule"}, {Label: "Discord", URL: "https://discord.gg/abc"}, } if err := Cache(ctx, db, rec); err != nil { t.Fatalf("Cache: %v", err) } got, err := Get(ctx, db, rec.URI) if err != nil { t.Fatalf("Get: %v", err) } if len(got.Links) != 2 { t.Fatalf("got %d links, want 2: %+v", len(got.Links), got.Links) } if got.Links[0].Label != "Schedule" || got.Links[0].URL != "https://example.com/schedule" { t.Errorf("link[0] = %+v", got.Links[0]) } if got.Links[1].Label != "Discord" || got.Links[1].URL != "https://discord.gg/abc" { t.Errorf("link[1] = %+v", got.Links[1]) } } func TestCache_NoLinksDecodesEmpty(t *testing.T) { ctx, db := newTestDB(t) rec := sampleEvent() // no links if err := Cache(ctx, db, rec); err != nil { t.Fatalf("Cache: %v", err) } got, err := Get(ctx, db, rec.URI) if err != nil { t.Fatalf("Get: %v", err) } if len(got.Links) != 0 { t.Errorf("expected no links, got %+v", got.Links) } }