package main import ( "testing" ) func TestMigrate(t *testing.T) { app := testApp(t) // Verify tables exist by querying each one tables := []string{"event", "participants", "participant_roles", "attendees", "departments", "volunteers", "shifts", "volunteer_shifts"} for _, table := range tables { var count int err := app.db.QueryRow("SELECT COUNT(*) FROM " + table).Scan(&count) if err != nil { t.Errorf("table %s: %v", table, err) } } } func TestAttendeesCRUD(t *testing.T) { app := testApp(t) a, err := app.createAttendee(Attendee{Name: "Titania", Email: "titania@test.com", TicketType: "GA"}) if err != nil { t.Fatal(err) } if a.ID == 0 || a.Name != "Titania" { t.Errorf("create: got %+v", a) } got, err := app.getAttendee(a.ID) if err != nil || got == nil { t.Fatal("get: not found") } if got.Email != "titania@test.com" { t.Errorf("get: email = %q", got.Email) } got.Name = "Titania Fairweather" if err := app.updateAttendee(*got); err != nil { t.Fatal(err) } got2, _ := app.getAttendee(a.ID) if got2.Name != "Titania Fairweather" { t.Errorf("update: name = %q", got2.Name) } if err := app.deleteAttendee(a.ID); err != nil { t.Fatal(err) } // getAttendee returns soft-deleted records; listAttendees filters them attendees, _ := app.listAttendees("", "", "") for _, at := range attendees { if at.ID == a.ID { t.Error("delete: still visible in list") } } } func TestIncrementPartySize(t *testing.T) { app := testApp(t) app.createAttendee(Attendee{Name: "Oberon", TicketID: "ORD-100"}) merged, err := app.incrementPartySize("Oberon", "ORD-100") if err != nil || !merged { t.Fatalf("increment: merged=%v, err=%v", merged, err) } a, _ := app.getAttendee(1) if a.PartySize != 2 { t.Errorf("party_size = %d, want 2", a.PartySize) } // Different ticket_id should not merge merged2, _ := app.incrementPartySize("Oberon", "ORD-200") if merged2 { t.Error("should not merge different ticket_id") } } func TestCheckInAttendee(t *testing.T) { app := testApp(t) admin := testAdminUser(t, app) app.createAttendee(Attendee{Name: "Puck"}) // Set party_size directly since createAttendee defaults to 1 app.db.Exec(`UPDATE attendees SET party_size = 3 WHERE id = 1`) // Check in 1 a, err := app.checkInAttendee(1, admin.ID, 1) if err != nil { t.Fatal(err) } if a.CheckedInCount != 1 || !a.CheckedIn { t.Errorf("after 1: count=%d, checked_in=%v", a.CheckedInCount, a.CheckedIn) } // Check in 2 more (should cap at party_size=3) a, _ = app.checkInAttendee(1, admin.ID, 5) if a.CheckedInCount != 3 { t.Errorf("after cap: count=%d, want 3", a.CheckedInCount) } // Check in again — already full, should stay at 3 a, _ = app.checkInAttendee(1, admin.ID, 1) if a.CheckedInCount != 3 { t.Errorf("after full: count=%d, want 3", a.CheckedInCount) } } func TestGenerateToken(t *testing.T) { token, err := generateToken() if err != nil { t.Fatal(err) } if len(token) != 8 { t.Errorf("token length = %d, want 8", len(token)) } for _, c := range token { if !isValidTokenChar(c) { t.Errorf("invalid char %c in token %s", c, token) } } } func isValidTokenChar(c rune) bool { for _, tc := range tokenChars { if c == tc { return true } } return false } func TestGenerateUniqueToken(t *testing.T) { app := testApp(t) token, err := app.generateUniqueToken() if err != nil || len(token) != 8 { t.Fatalf("token=%q, err=%v", token, err) } } func TestDepartmentsCRUD(t *testing.T) { app := testApp(t) d, err := app.createDepartment(Department{Name: "Gate"}) if err != nil { t.Fatal(err) } if d.Name != "Gate" { t.Errorf("name = %q", d.Name) } depts, _ := app.listDepartments("") if len(depts) != 1 { t.Errorf("list: got %d", len(depts)) } if err := app.deleteDepartment(d.ID); err != nil { t.Fatal(err) } } func TestShiftsCRUD(t *testing.T) { app := testApp(t) dept, _ := app.createDepartment(Department{Name: "Gate"}) s, err := app.createShift(Shift{ DepartmentID: dept.ID, Name: "Morning", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00", Capacity: 5, }) if err != nil { t.Fatal(err) } if s.Name != "Morning" || s.Capacity != 5 { t.Errorf("create: %+v", s) } got, _ := app.getShift(s.ID) if got == nil || got.Day != "2026-03-15" { t.Error("get: not found or wrong day") } if err := app.deleteShift(s.ID); err != nil { t.Fatal(err) } } func TestAssignAndUnassignShift(t *testing.T) { app := testApp(t) dept, _ := app.createDepartment(Department{Name: "Gate"}) deptID := dept.ID s, _ := app.createShift(Shift{DepartmentID: deptID, Name: "AM", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00", Capacity: 2}) p, _ := app.createParticipant(Participant{PreferredName: "Helena", Email: "helena@test.com"}) v, _ := app.createVolunteer(Volunteer{ParticipantID: p.ID, DepartmentID: &deptID}) if err := app.assignShift(v.ID, s.ID); err != nil { t.Fatal(err) } count, _ := app.shiftAssignedCount(s.ID) if count != 1 { t.Errorf("assigned count = %d, want 1", count) } if err := app.unassignShift(v.ID, s.ID); err != nil { t.Fatal(err) } count, _ = app.shiftAssignedCount(s.ID) if count != 0 { t.Errorf("after unassign: count = %d, want 0", count) } } func TestCheckShiftConflict(t *testing.T) { app := testApp(t) dept, _ := app.createDepartment(Department{Name: "Gate"}) deptID := dept.ID p, _ := app.createParticipant(Participant{PreferredName: "Hermia", Email: "hermia@test.com"}) v, _ := app.createVolunteer(Volunteer{ParticipantID: p.ID, DepartmentID: &deptID}) s1, _ := app.createShift(Shift{DepartmentID: deptID, Name: "Morning", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00"}) s2, _ := app.createShift(Shift{DepartmentID: deptID, Name: "Overlap", Day: "2026-03-15", StartTime: "10:00", EndTime: "14:00"}) s3, _ := app.createShift(Shift{DepartmentID: deptID, Name: "NoOverlap", Day: "2026-03-15", StartTime: "14:00", EndTime: "18:00"}) app.assignShift(v.ID, s1.ID) // s2 overlaps s1 (10:00-14:00 vs 08:00-12:00) conflicts, err := app.checkShiftConflict(v.ID, s2.ID) if err != nil { t.Fatal(err) } if len(conflicts) != 1 { t.Errorf("overlap: got %d conflicts, want 1", len(conflicts)) } // s3 does not overlap s1 (14:00-18:00 vs 08:00-12:00) conflicts, _ = app.checkShiftConflict(v.ID, s3.ID) if len(conflicts) != 0 { t.Errorf("no overlap: got %d conflicts, want 0", len(conflicts)) } } func TestCheckShiftConflictMidnight(t *testing.T) { app := testApp(t) dept, _ := app.createDepartment(Department{Name: "Sound"}) deptID := dept.ID p, _ := app.createParticipant(Participant{PreferredName: "Lysander", Email: "lysander@test.com"}) v, _ := app.createVolunteer(Volunteer{ParticipantID: p.ID, DepartmentID: &deptID}) // Night shift: 22:00-02:00 (spans midnight) night, _ := app.createShift(Shift{DepartmentID: deptID, Name: "Night", Day: "2026-03-15", StartTime: "22:00", EndTime: "02:00"}) // Late shift: 23:00-03:00 (overlaps with night) late, _ := app.createShift(Shift{DepartmentID: deptID, Name: "Late", Day: "2026-03-15", StartTime: "23:00", EndTime: "03:00"}) // Morning shift: 08:00-12:00 (no overlap with night) morning, _ := app.createShift(Shift{DepartmentID: deptID, Name: "Morning", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00"}) app.assignShift(v.ID, night.ID) // Late should conflict with night conflicts, _ := app.checkShiftConflict(v.ID, late.ID) if len(conflicts) != 1 { t.Errorf("midnight overlap: got %d conflicts, want 1", len(conflicts)) } // Morning should not conflict with night conflicts, _ = app.checkShiftConflict(v.ID, morning.ID) if len(conflicts) != 0 { t.Errorf("no midnight overlap: got %d conflicts, want 0", len(conflicts)) } }