package main import ( "context" "database/sql" "testing" _ "github.com/mattn/go-sqlite3" "github.com/ryanchen/bbq/db" ) func setupTestDB(t *testing.T) (*sql.DB, *db.Queries) { t.Helper() database, err := sql.Open("sqlite3", ":memory:?_foreign_keys=on") if err != nil { t.Fatal(err) } if _, err := database.Exec(schemaSQL); err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() }) return database, db.New(database) } func createTestEvent(t *testing.T, q *db.Queries) db.Event { t.Helper() event, err := q.CreateEvent(context.Background(), db.CreateEventParams{ Slug: "test", Title: "Test Event", Date: "June 1", Time: "2pm", Location: "Park", AdminToken: "tok123", }) if err != nil { t.Fatal(err) } return event } // autoRsvp simulates the merged handleRsvp logic: create RSVP (deduped) + optional claim func autoRsvp(ctx context.Context, q *db.Queries, event db.Event, name string, note string, slotID *int64) error { return autoRsvpPlusOne(ctx, q, event, name, note, 0, slotID) } func autoRsvpPlusOne(ctx context.Context, q *db.Queries, event db.Event, name string, note string, plusOne int64, slotID *int64) error { if slotID != nil { _, err := q.CreateClaim(ctx, db.CreateClaimParams{ SlotID: *slotID, Name: name, Note: note, }) if err != nil { return err } } _, err := q.GetRsvpByName(ctx, db.GetRsvpByNameParams{ EventID: event.ID, Name: name, }) if err == sql.ErrNoRows { _, err = q.CreateRsvp(ctx, db.CreateRsvpParams{ EventID: event.ID, Name: name, Note: note, PlusOne: plusOne, }) return err } return nil } func TestRsvpOnly(t *testing.T) { _, q := setupTestDB(t) ctx := context.Background() event := createTestEvent(t, q) if err := autoRsvp(ctx, q, event, "Alice", "", nil); err != nil { t.Fatal(err) } rsvps, _ := q.ListRsvps(ctx, event.ID) if len(rsvps) != 1 { t.Fatalf("expected 1 RSVP, got %d", len(rsvps)) } if rsvps[0].Name != "Alice" { t.Fatalf("expected RSVP name Alice, got %s", rsvps[0].Name) } } func TestRsvpWithClaim(t *testing.T) { _, q := setupTestDB(t) ctx := context.Background() event := createTestEvent(t, q) slot, err := q.CreateSlot(ctx, db.CreateSlotParams{ EventID: event.ID, Name: "Drinks", MaxClaims: 5, }) if err != nil { t.Fatal(err) } if err := autoRsvp(ctx, q, event, "Alice", "sparkling water", &slot.ID); err != nil { t.Fatal(err) } // Check RSVP created rsvps, _ := q.ListRsvps(ctx, event.ID) if len(rsvps) != 1 { t.Fatalf("expected 1 RSVP, got %d", len(rsvps)) } if rsvps[0].Name != "Alice" { t.Fatalf("expected RSVP name Alice, got %s", rsvps[0].Name) } // Check claim created claims, _ := q.ListClaimsByEvent(ctx, event.ID) if len(claims) != 1 { t.Fatalf("expected 1 claim, got %d", len(claims)) } if claims[0].Name != "Alice" { t.Fatalf("expected claim name Alice, got %s", claims[0].Name) } } func TestRsvpNoDuplicate(t *testing.T) { _, q := setupTestDB(t) ctx := context.Background() event := createTestEvent(t, q) slot, _ := q.CreateSlot(ctx, db.CreateSlotParams{ EventID: event.ID, Name: "Drinks", MaxClaims: 5, }) // RSVP first (no slot) autoRsvp(ctx, q, event, "Bob", "", nil) // Then RSVP again with a slot claim — should not duplicate RSVP autoRsvp(ctx, q, event, "Bob", "", &slot.ID) rsvps, _ := q.ListRsvps(ctx, event.ID) if len(rsvps) != 1 { t.Fatalf("expected 1 RSVP (no duplicate), got %d", len(rsvps)) } } func TestRsvpCaseInsensitiveDedup(t *testing.T) { _, q := setupTestDB(t) ctx := context.Background() event := createTestEvent(t, q) slot, _ := q.CreateSlot(ctx, db.CreateSlotParams{ EventID: event.ID, Name: "Drinks", MaxClaims: 5, }) // RSVP as "alice" autoRsvp(ctx, q, event, "alice", "", nil) // Claim as "Alice" — should not create duplicate RSVP autoRsvp(ctx, q, event, "Alice", "", &slot.ID) rsvps, _ := q.ListRsvps(ctx, event.ID) if len(rsvps) != 1 { t.Fatalf("expected 1 RSVP (case-insensitive dedup), got %d", len(rsvps)) } } func TestDeduplicateExistingRsvps(t *testing.T) { database, q := setupTestDB(t) ctx := context.Background() event := createTestEvent(t, q) // Insert duplicate RSVPs directly q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Charlie", PlusOne: 0}) q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Charlie", PlusOne: 0}) q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "charlie", PlusOne: 0}) q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Dana", PlusOne: 0}) rsvps, _ := q.ListRsvps(ctx, event.ID) if len(rsvps) != 4 { t.Fatalf("expected 4 RSVPs before dedup, got %d", len(rsvps)) } // Run the dedup query (same as migration) database.Exec(`DELETE FROM rsvps WHERE id NOT IN (SELECT MIN(id) FROM rsvps GROUP BY event_id, name COLLATE NOCASE)`) rsvps, _ = q.ListRsvps(ctx, event.ID) if len(rsvps) != 2 { t.Fatalf("expected 2 RSVPs after dedup (Charlie + Dana), got %d", len(rsvps)) } } func TestUpdateRsvp(t *testing.T) { _, q := setupTestDB(t) ctx := context.Background() event := createTestEvent(t, q) rsvp, err := q.CreateRsvp(ctx, db.CreateRsvpParams{ EventID: event.ID, Name: "Alice", Note: "hi", PlusOne: 1, }) if err != nil { t.Fatal(err) } err = q.UpdateRsvp(ctx, db.UpdateRsvpParams{ Name: "Alicia", Note: "updated", PlusOne: 3, ID: rsvp.ID, }) if err != nil { t.Fatal(err) } updated, err := q.GetRsvp(ctx, rsvp.ID) if err != nil { t.Fatal(err) } if updated.Name != "Alicia" { t.Fatalf("expected name Alicia, got %s", updated.Name) } if updated.Note != "updated" { t.Fatalf("expected note 'updated', got %s", updated.Note) } if updated.PlusOne != 3 { t.Fatalf("expected PlusOne=3, got %d", updated.PlusOne) } } func TestRsvpPlusOne(t *testing.T) { _, q := setupTestDB(t) ctx := context.Background() event := createTestEvent(t, q) if err := autoRsvpPlusOne(ctx, q, event, "Alice", "", 2, nil); err != nil { t.Fatal(err) } rsvps, _ := q.ListRsvps(ctx, event.ID) if len(rsvps) != 1 { t.Fatalf("expected 1 RSVP, got %d", len(rsvps)) } if rsvps[0].PlusOne != 2 { t.Fatalf("expected PlusOne=2, got %d", rsvps[0].PlusOne) } }