Added tests, shift 'delete'. Fixed overnight shifts, sync, error handling.
This commit is contained in:
parent
49047b4745
commit
bbf611df5c
21 changed files with 2522 additions and 40 deletions
275
db_test.go
Normal file
275
db_test.go
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
app := testApp(t)
|
||||
// Verify tables exist by querying each one
|
||||
tables := []string{"event", "users", "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: "Alice", Email: "alice@test.com", TicketType: "GA"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if a.ID == 0 || a.Name != "Alice" {
|
||||
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 != "alice@test.com" {
|
||||
t.Errorf("get: email = %q", got.Email)
|
||||
}
|
||||
|
||||
got.Name = "Alice Smith"
|
||||
if err := app.updateAttendee(*got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got2, _ := app.getAttendee(a.ID)
|
||||
if got2.Name != "Alice Smith" {
|
||||
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: "Bob", TicketID: "ORD-100"})
|
||||
|
||||
merged, err := app.incrementPartySize("Bob", "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("Bob", "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: "Charlie"})
|
||||
// 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})
|
||||
v, _ := app.createVolunteer(Volunteer{Name: "Dana", 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
|
||||
v, _ := app.createVolunteer(Volunteer{Name: "Eve", 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
|
||||
v, _ := app.createVolunteer(Volunteer{Name: "Frank", 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))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue