package main import ( "net/http" "net/http/httptest" "testing" ) func setupKiosk(t *testing.T) (*App, *http.ServeMux, string) { t.Helper() app := testApp(t) mux := testMux(app) dept, _ := app.createDepartment(Department{Name: "Gate"}) deptID := dept.ID // Create participant + ticket with code p, _ := app.createParticipant(Participant{Email: "titania@test.com", PreferredName: "Titania"}) token, _ := app.generateUniqueToken() tk, _ := app.createTicket(Ticket{ParticipantID: &p.ID, Name: "Titania", Source: "manual", Code: &token}) _ = tk // Create linked volunteer via participant_id app.createVolunteer(Volunteer{Name: "Titania", ParticipantID: &p.ID, DepartmentID: &deptID}) // Create shifts app.createShift(Shift{DepartmentID: deptID, Name: "Morning", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00", Capacity: 2}) app.createShift(Shift{DepartmentID: deptID, Name: "Afternoon", Day: "2026-03-15", StartTime: "14:00", EndTime: "18:00", Capacity: 1}) return app, mux, token } func TestKioskGetValid(t *testing.T) { _, mux, token := setupKiosk(t) req := httptest.NewRequest("GET", "/api/v/"+token, nil) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d\nbody: %s", w.Code, w.Body.String()) } result := parseJSON(t, w) if result["volunteer"] == nil { t.Error("missing volunteer") } available := result["available"].([]any) if len(available) != 2 { t.Errorf("available = %d, want 2", len(available)) } } func TestKioskGetInvalidToken(t *testing.T) { _, mux, _ := setupKiosk(t) req := httptest.NewRequest("GET", "/api/v/BADTOKEN", nil) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("status = %d, want 404", w.Code) } } func TestKioskClaimShift(t *testing.T) { _, mux, token := setupKiosk(t) req := httptest.NewRequest("POST", "/api/v/"+token+"/shifts/1", nil) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("claim: status = %d\nbody: %s", w.Code, w.Body.String()) } result := parseJSON(t, w) shifts := result["shifts"].([]any) if len(shifts) != 1 { t.Errorf("assigned shifts = %d, want 1", len(shifts)) } } func TestKioskClaimConflict(t *testing.T) { app, mux, token := setupKiosk(t) dept, _ := app.createDepartment(Department{Name: "Build"}) deptID := dept.ID // Create overlapping shift app.createShift(Shift{DepartmentID: deptID, Name: "Overlap", Day: "2026-03-15", StartTime: "10:00", EndTime: "14:00"}) // Claim morning (08:00-12:00) req := httptest.NewRequest("POST", "/api/v/"+token+"/shifts/1", nil) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("first claim: status = %d", w.Code) } // Claim overlapping shift (10:00-14:00) — should get 409 req = httptest.NewRequest("POST", "/api/v/"+token+"/shifts/3", nil) w = httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusConflict { t.Fatalf("conflict: status = %d, want 409\nbody: %s", w.Code, w.Body.String()) } result := parseJSON(t, w) if result["conflict"] != true { t.Error("missing conflict flag") } } func TestKioskClaimForce(t *testing.T) { app, mux, token := setupKiosk(t) dept, _ := app.createDepartment(Department{Name: "Build"}) deptID := dept.ID app.createShift(Shift{DepartmentID: deptID, Name: "Overlap", Day: "2026-03-15", StartTime: "10:00", EndTime: "14:00"}) // Claim morning req := httptest.NewRequest("POST", "/api/v/"+token+"/shifts/1", nil) w := httptest.NewRecorder() mux.ServeHTTP(w, req) // Force-claim overlapping shift req = httptest.NewRequest("POST", "/api/v/"+token+"/shifts/3?force=true", nil) w = httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("force: status = %d, want 200", w.Code) } } func TestKioskClaimFull(t *testing.T) { app, mux, token := setupKiosk(t) // Shift 2 has capacity 1. Fill it with another volunteer. dept, _ := app.createDepartment(Department{Name: "Build"}) deptID := dept.ID other, _ := app.createVolunteer(Volunteer{Name: "Other", DepartmentID: &deptID}) app.assignShift(other.ID, 2) // fills the capacity-1 shift req := httptest.NewRequest("POST", "/api/v/"+token+"/shifts/2", nil) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusConflict { t.Errorf("full: status = %d, want 409\nbody: %s", w.Code, w.Body.String()) } } func TestKioskUnclaim(t *testing.T) { _, mux, token := setupKiosk(t) // Claim then unclaim req := httptest.NewRequest("POST", "/api/v/"+token+"/shifts/1", nil) w := httptest.NewRecorder() mux.ServeHTTP(w, req) req = httptest.NewRequest("DELETE", "/api/v/"+token+"/shifts/1", nil) w = httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("unclaim: status = %d", w.Code) } result := parseJSON(t, w) shifts := result["shifts"].([]any) if len(shifts) != 0 { t.Errorf("after unclaim: shifts = %d, want 0", len(shifts)) } }