Files
bbq/handlers_test.go
T
2026-07-13 17:21:01 -04:00

344 lines
9.0 KiB
Go

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 TestUpdateEventDateTime(t *testing.T) {
_, q := setupTestDB(t)
ctx := context.Background()
event := createTestEvent(t, q)
err := q.UpdateEventDateTime(ctx, db.UpdateEventDateTimeParams{
Date: "Sunday, July 20", Time: "5:30 PM", ID: event.ID,
})
if err != nil {
t.Fatal(err)
}
updated, err := q.GetEventBySlug(ctx, event.Slug)
if err != nil {
t.Fatal(err)
}
if updated.Date != "Sunday, July 20" {
t.Fatalf("expected date 'Sunday, July 20', got %s", updated.Date)
}
if updated.Time != "5:30 PM" {
t.Fatalf("expected time '5:30 PM', got %s", updated.Time)
}
}
func TestCountGoing(t *testing.T) {
_, q := setupTestDB(t)
ctx := context.Background()
event := createTestEvent(t, q)
going, err := q.CountGoing(ctx, event.ID)
if err != nil {
t.Fatal(err)
}
if going != 0 {
t.Fatalf("expected 0 going on empty event, got %d", going)
}
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Alice", PlusOne: 0})
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Bob", PlusOne: 2})
going, err = q.CountGoing(ctx, event.ID)
if err != nil {
t.Fatal(err)
}
if going != 4 {
t.Fatalf("expected 4 going (2 RSVPs + 2 plus-ones), got %d", going)
}
}
func TestGetRsvpByUser(t *testing.T) {
_, q := setupTestDB(t)
ctx := context.Background()
event := createTestEvent(t, q)
user, err := q.CreateUserByEmail(ctx, sql.NullString{String: "alice@example.com", Valid: true})
if err != nil {
t.Fatal(err)
}
_, err = q.GetRsvpByUser(ctx, db.GetRsvpByUserParams{
EventID: event.ID, UserID: sql.NullInt64{Int64: user.ID, Valid: true},
})
if err != sql.ErrNoRows {
t.Fatalf("expected ErrNoRows before RSVP, got %v", err)
}
q.CreateRsvp(ctx, db.CreateRsvpParams{
EventID: event.ID, Name: "Alice", PlusOne: 1,
UserID: sql.NullInt64{Int64: user.ID, Valid: true},
})
rsvp, err := q.GetRsvpByUser(ctx, db.GetRsvpByUserParams{
EventID: event.ID, UserID: sql.NullInt64{Int64: user.ID, Valid: true},
})
if err != nil {
t.Fatal(err)
}
if rsvp.Name != "Alice" {
t.Fatalf("expected RSVP name Alice, got %s", rsvp.Name)
}
}
func TestAttendeeCapArithmetic(t *testing.T) {
_, q := setupTestDB(t)
ctx := context.Background()
event, err := q.CreateEvent(ctx, db.CreateEventParams{
Slug: "capped", Title: "Capped Event", Date: "June 1", Time: "2pm",
Location: "Park", AdminToken: "tok456", AttendeeCap: 3,
})
if err != nil {
t.Fatal(err)
}
if event.AttendeeCap != 3 {
t.Fatalf("expected AttendeeCap=3, got %d", event.AttendeeCap)
}
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Alice", PlusOne: 1})
going, _ := q.CountGoing(ctx, event.ID)
// Same check as handleRsvp: a new RSVP with plus_one=1 would exceed the cap.
if going+1+1 <= event.AttendeeCap {
t.Fatalf("expected RSVP +1 to exceed cap (going=%d, cap=%d)", going, event.AttendeeCap)
}
// A solo RSVP still fits.
if going+1+0 > event.AttendeeCap {
t.Fatalf("expected solo RSVP to fit (going=%d, cap=%d)", going, event.AttendeeCap)
}
}
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)
}
}