package main import ( "net/http" "net/http/httptest" "testing" ) func TestConfirmVolunteer(t *testing.T) { app := testApp(t) mux := testMux(app) admin := testAdminUser(t, app) tok := testToken(t, app, admin) dept, _ := app.createDepartment(Department{Name: "Gate"}) deptID := dept.ID p, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@test.com", EmailConfirmed: true}) v, _ := app.createVolunteer(Volunteer{ParticipantID: p.ID, DepartmentID: &deptID}) w := httptest.NewRecorder() mux.ServeHTTP(w, testAuthRequest("POST", "/api/volunteers/"+itoa(v.ID)+"/confirm", nil, tok)) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } result := parseJSON(t, w) vol := result["confirmed"] if vol != true { t.Error("expected confirmed=true in response") } got, _ := app.getVolunteer(v.ID) if got == nil || !got.Confirmed { t.Error("volunteer should be confirmed in DB") } if got.ConfirmedAt == nil { t.Error("confirmed_at should be set") } } func TestConfirmVolunteerIdempotent(t *testing.T) { app := testApp(t) mux := testMux(app) admin := testAdminUser(t, app) tok := testToken(t, app, admin) p, _ := app.createParticipant(Participant{PreferredName: "Puck", Email: "puck@test.com", EmailConfirmed: true}) v, _ := app.createVolunteer(Volunteer{ParticipantID: p.ID}) // Confirm twice — second should be a no-op, not an error. w := httptest.NewRecorder() mux.ServeHTTP(w, testAuthRequest("POST", "/api/volunteers/"+itoa(v.ID)+"/confirm", nil, tok)) if w.Code != 200 { t.Fatalf("first confirm: %d", w.Code) } w = httptest.NewRecorder() mux.ServeHTTP(w, testAuthRequest("POST", "/api/volunteers/"+itoa(v.ID)+"/confirm", nil, tok)) if w.Code != 200 { t.Fatalf("second confirm: %d", w.Code) } } func TestConfirmVolunteerRequiresRole(t *testing.T) { app := testApp(t) mux := testMux(app) // Gatekeeper role should NOT be able to confirm volunteers. gatekeeper := testUserWithRoles(t, app, "Egeus", []string{"gatekeeper"}, []int{}) tok := testToken(t, app, gatekeeper) p, _ := app.createParticipant(Participant{PreferredName: "Helena", EmailConfirmed: true}) v, _ := app.createVolunteer(Volunteer{ParticipantID: p.ID}) w := httptest.NewRecorder() mux.ServeHTTP(w, testAuthRequest("POST", "/api/volunteers/"+itoa(v.ID)+"/confirm", nil, tok)) if w.Code != http.StatusForbidden { t.Errorf("expected 403 for gatekeeper role, got %d", w.Code) } } func TestCoLeadDeleteVolunteerOwnDept(t *testing.T) { app := testApp(t) mux := testMux(app) deptA, _ := app.createDepartment(Department{Name: "Gate"}) colead := testUserWithRoles(t, app, "Hermia", []string{"colead"}, []int{deptA.ID}) tok := testToken(t, app, colead) deptAID := deptA.ID p, _ := app.createParticipant(Participant{PreferredName: "Puck", Email: "puck@test.com"}) v, _ := app.createVolunteer(Volunteer{ParticipantID: p.ID, DepartmentID: &deptAID}) w := httptest.NewRecorder() mux.ServeHTTP(w, testAuthRequest("DELETE", "/api/volunteers/"+itoa(v.ID), nil, tok)) if w.Code != http.StatusNoContent { t.Errorf("expected 204 for own dept, got %d: %s", w.Code, w.Body.String()) } } func TestCoLeadDeleteVolunteerOtherDept(t *testing.T) { app := testApp(t) mux := testMux(app) deptA, _ := app.createDepartment(Department{Name: "Gate"}) deptB, _ := app.createDepartment(Department{Name: "Build"}) colead := testUserWithRoles(t, app, "Hermia", []string{"colead"}, []int{deptA.ID}) tok := testToken(t, app, colead) deptBID := deptB.ID p, _ := app.createParticipant(Participant{PreferredName: "Puck", Email: "puck@test.com"}) v, _ := app.createVolunteer(Volunteer{ParticipantID: p.ID, DepartmentID: &deptBID}) w := httptest.NewRecorder() mux.ServeHTTP(w, testAuthRequest("DELETE", "/api/volunteers/"+itoa(v.ID), nil, tok)) if w.Code != http.StatusForbidden { t.Errorf("expected 403 for other dept, got %d", w.Code) } } func TestCoLeadConfirmVolunteerOtherDept(t *testing.T) { app := testApp(t) mux := testMux(app) deptA, _ := app.createDepartment(Department{Name: "Gate"}) deptB, _ := app.createDepartment(Department{Name: "Build"}) colead := testUserWithRoles(t, app, "Hermia", []string{"colead"}, []int{deptA.ID}) tok := testToken(t, app, colead) deptBID := deptB.ID p, _ := app.createParticipant(Participant{PreferredName: "Puck", Email: "puck@test.com"}) v, _ := app.createVolunteer(Volunteer{ParticipantID: p.ID, DepartmentID: &deptBID}) w := httptest.NewRecorder() mux.ServeHTTP(w, testAuthRequest("POST", "/api/volunteers/"+itoa(v.ID)+"/confirm", nil, tok)) if w.Code != http.StatusForbidden { t.Errorf("expected 403 for other dept, got %d", w.Code) } } func TestCoLeadReadyVolunteerOtherDept(t *testing.T) { app := testApp(t) mux := testMux(app) deptA, _ := app.createDepartment(Department{Name: "Gate"}) deptB, _ := app.createDepartment(Department{Name: "Build"}) colead := testUserWithRoles(t, app, "Hermia", []string{"colead"}, []int{deptA.ID}) tok := testToken(t, app, colead) deptBID := deptB.ID p, _ := app.createParticipant(Participant{PreferredName: "Puck", Email: "puck@test.com"}) v, _ := app.createVolunteer(Volunteer{ParticipantID: p.ID, DepartmentID: &deptBID}) w := httptest.NewRecorder() mux.ServeHTTP(w, testAuthRequest("POST", "/api/volunteers/"+itoa(v.ID)+"/ready", nil, tok)) if w.Code != http.StatusForbidden { t.Errorf("expected 403 for other dept, got %d", w.Code) } } func TestCoLeadAssignShiftOtherDept(t *testing.T) { app := testApp(t) mux := testMux(app) deptA, _ := app.createDepartment(Department{Name: "Gate"}) deptB, _ := app.createDepartment(Department{Name: "Build"}) colead := testUserWithRoles(t, app, "Hermia", []string{"colead"}, []int{deptA.ID}) tok := testToken(t, app, colead) deptBID := deptB.ID p, _ := app.createParticipant(Participant{PreferredName: "Puck", Email: "puck@test.com"}) v, _ := app.createVolunteer(Volunteer{ParticipantID: p.ID, DepartmentID: &deptBID}) s, _ := app.createShift(Shift{DepartmentID: deptB.ID, Name: "AM", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00"}) w := httptest.NewRecorder() mux.ServeHTTP(w, testAuthRequest("POST", "/api/volunteers/"+itoa(v.ID)+"/shifts", map[string]any{ "shift_id": s.ID, }, tok)) if w.Code != http.StatusForbidden { t.Errorf("expected 403 for other dept, got %d", w.Code) } } func TestCoLeadUpdateVolunteerTargetDeptForbidden(t *testing.T) { app := testApp(t) mux := testMux(app) deptA, _ := app.createDepartment(Department{Name: "Gate"}) deptB, _ := app.createDepartment(Department{Name: "Build"}) colead := testUserWithRoles(t, app, "Hermia", []string{"colead"}, []int{deptA.ID}) tok := testToken(t, app, colead) deptAID := deptA.ID p, _ := app.createParticipant(Participant{PreferredName: "Puck", Email: "puck@test.com"}) v, _ := app.createVolunteer(Volunteer{ParticipantID: p.ID, DepartmentID: &deptAID}) w := httptest.NewRecorder() mux.ServeHTTP(w, testAuthRequest("PUT", "/api/volunteers/"+itoa(v.ID), map[string]any{ "department_id": deptB.ID, }, tok)) if w.Code != http.StatusForbidden { t.Errorf("expected 403 moving to other dept, got %d: %s", w.Code, w.Body.String()) } } func TestUpdateVolunteerDepartment(t *testing.T) { app := testApp(t) mux := testMux(app) admin := testAdminUser(t, app) tok := testToken(t, app, admin) dept, _ := app.createDepartment(Department{Name: "Gate"}) p, _ := app.createParticipant(Participant{PreferredName: "Hermia"}) v, _ := app.createVolunteer(Volunteer{ParticipantID: p.ID}) // Assign department via update. w := httptest.NewRecorder() mux.ServeHTTP(w, testAuthRequest("PUT", "/api/volunteers/"+itoa(v.ID), map[string]any{ "department_id": dept.ID, }, tok)) if w.Code != 200 { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } got, _ := app.getVolunteer(v.ID) if got.DepartmentID == nil || *got.DepartmentID != dept.ID { t.Errorf("department_id = %v, want %d", got.DepartmentID, dept.ID) } } func TestUpdateVolunteerCoLeadAutoConfirms(t *testing.T) { app := testApp(t) mux := testMux(app) admin := testAdminUser(t, app) tok := testToken(t, app, admin) dept, _ := app.createDepartment(Department{Name: "Build"}) deptID := dept.ID p, _ := app.createParticipant(Participant{PreferredName: "Lysander", Email: "lys@test.com", EmailConfirmed: true}) v, _ := app.createVolunteer(Volunteer{ParticipantID: p.ID, DepartmentID: &deptID}) // Verify not confirmed before update. got, _ := app.getVolunteer(v.ID) if got.Confirmed { t.Fatal("should not be confirmed before update") } // Update is_lead=true should auto-confirm. w := httptest.NewRecorder() mux.ServeHTTP(w, testAuthRequest("PUT", "/api/volunteers/"+itoa(v.ID), map[string]any{ "department_id": deptID, "is_lead": true, }, tok)) if w.Code != 200 { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } got, _ = app.getVolunteer(v.ID) if !got.IsLead { t.Error("expected is_lead=true") } if !got.Confirmed { t.Error("co-lead should be auto-confirmed") } }