package main import ( "net/http/httptest" "testing" ) func TestPublicSignupConfig(t *testing.T) { app := testApp(t) mux := testMux(app) app.createDepartment(Department{Name: "Setup", Color: "#ff0000"}) app.createDepartment(Department{Name: "Teardown", Color: "#00ff00"}) app.db.Exec(`INSERT OR REPLACE INTO config (key, value) VALUES ('volunteer_note_label', 'Who sent you?')`) app.db.Exec(`INSERT OR REPLACE INTO config (key, value) VALUES ('volunteer_note_required', 'true')`) w := httptest.NewRecorder() mux.ServeHTTP(w, testRequest("GET", "/api/public/signup-config", nil)) if w.Code != 200 { t.Fatalf("expected 200, got %d", w.Code) } result := parseJSON(t, w) depts, ok := result["departments"].([]any) if !ok || len(depts) != 2 { t.Fatalf("expected 2 departments, got %v", result["departments"]) } if result["volunteer_note_label"] != "Who sent you?" { t.Errorf("expected 'Who sent you?', got %v", result["volunteer_note_label"]) } if result["volunteer_note_required"] != true { t.Errorf("expected note required true, got %v", result["volunteer_note_required"]) } } func TestPublicSignup(t *testing.T) { app := testApp(t) mux := testMux(app) app.createDepartment(Department{Name: "Setup", Color: "#ff0000"}) w := httptest.NewRecorder() deptID := 1 mux.ServeHTTP(w, testRequest("POST", "/api/public/signup", map[string]any{ "preferred_name": "Titania", "email": "titania@example.com", "pronouns": "she/they", "department_id": deptID, })) if w.Code != 200 { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } result := parseJSON(t, w) if result["ok"] != true { t.Fatalf("expected ok true, got %v", result) } // Volunteer should exist vol, err := app.getVolunteerByEmail("titania@example.com") if err != nil || vol == nil { t.Fatal("volunteer not created") } if vol.PreferredName != "Titania" { t.Errorf("preferred_name = %q, want Titania", vol.PreferredName) } if vol.Pronouns != "she/they" { t.Errorf("pronouns = %q, want she/they", vol.Pronouns) } if vol.ConfirmationToken == nil || *vol.ConfirmationToken == "" { t.Error("expected confirmation token to be set") } if vol.EmailConfirmed { t.Error("should not be confirmed yet") } // Participant should be auto-created and linked if vol.ParticipantID == nil { t.Fatal("expected participant to be linked") } p, _ := app.getParticipant(*vol.ParticipantID) if p == nil { t.Fatal("linked participant not found") } if p.Email != "titania@example.com" { t.Errorf("participant email = %q, want titania@example.com", p.Email) } } func TestPublicSignupAutoMatchParticipant(t *testing.T) { app := testApp(t) mux := testMux(app) // Pre-existing participant existing, _ := app.createParticipant(Participant{PreferredName: "Titania Fairweather", Email: "titania@example.com"}) w := httptest.NewRecorder() mux.ServeHTTP(w, testRequest("POST", "/api/public/signup", map[string]any{ "preferred_name": "Titania", "ticket_name": "Titania Fairweather", "email": "titania@example.com", })) if w.Code != 200 { t.Fatalf("expected 200, got %d", w.Code) } vol, _ := app.getVolunteerByEmail("titania@example.com") if vol == nil { t.Fatal("volunteer not created") } if vol.ParticipantID == nil || *vol.ParticipantID != existing.ID { t.Errorf("expected volunteer linked to existing participant %d, got %v", existing.ID, vol.ParticipantID) } } func TestPublicSignupDuplicateEmail(t *testing.T) { app := testApp(t) mux := testMux(app) // First signup w := httptest.NewRecorder() mux.ServeHTTP(w, testRequest("POST", "/api/public/signup", map[string]any{ "preferred_name": "Titania", "email": "titania@example.com", })) if w.Code != 200 { t.Fatalf("first signup: expected 200, got %d", w.Code) } // Second signup with same email — should silently succeed w = httptest.NewRecorder() mux.ServeHTTP(w, testRequest("POST", "/api/public/signup", map[string]any{ "preferred_name": "Puck", "email": "titania@example.com", })) if w.Code != 200 { t.Fatalf("duplicate signup: expected 200, got %d", w.Code) } result := parseJSON(t, w) if result["ok"] != true { t.Fatalf("expected ok true for duplicate, got %v", result) } // Should still be only one volunteer vols, _ := app.listVolunteers("", nil, "") if len(vols) != 1 { t.Errorf("expected 1 volunteer, got %d", len(vols)) } } func TestPublicSignupMissingFields(t *testing.T) { app := testApp(t) mux := testMux(app) tests := []struct { name string body map[string]any }{ {"no name", map[string]any{"email": "a@b.com"}}, {"no email", map[string]any{"preferred_name": "Titania"}}, {"empty both", map[string]any{}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w := httptest.NewRecorder() mux.ServeHTTP(w, testRequest("POST", "/api/public/signup", tt.body)) if w.Code != 400 { t.Errorf("expected 400, got %d", w.Code) } }) } } func TestPublicSignupNoteRequired(t *testing.T) { app := testApp(t) mux := testMux(app) app.db.Exec(`INSERT OR REPLACE INTO config (key, value) VALUES ('volunteer_note_required', 'true')`) w := httptest.NewRecorder() mux.ServeHTTP(w, testRequest("POST", "/api/public/signup", map[string]any{ "preferred_name": "Titania", "email": "titania@example.com", "note": "", })) if w.Code != 400 { t.Fatalf("expected 400 when note required but empty, got %d", w.Code) } // With note provided w = httptest.NewRecorder() mux.ServeHTTP(w, testRequest("POST", "/api/public/signup", map[string]any{ "preferred_name": "Titania", "email": "titania@example.com", "note": "A friend sent me", })) if w.Code != 200 { t.Fatalf("expected 200 with note, got %d: %s", w.Code, w.Body.String()) } } func TestConfirmEmail(t *testing.T) { app := testApp(t) mux := testMux(app) token := "abc123def456" app.createVolunteer(Volunteer{ Name: "Titania", PreferredName: "Titania", Email: "titania@example.com", ConfirmationToken: &token, }) w := httptest.NewRecorder() mux.ServeHTTP(w, testRequest("POST", "/api/public/confirm", map[string]any{"token": token})) if w.Code != 200 { t.Fatalf("expected 200, got %d", w.Code) } result := parseJSON(t, w) if result["status"] != "confirmed" { t.Errorf("expected confirmed, got %v", result["status"]) } // Verify volunteer is confirmed vol, _ := app.getVolunteerByEmail("titania@example.com") if vol == nil || !vol.EmailConfirmed { t.Error("volunteer should be email confirmed") } if vol.ConfirmationToken != nil { t.Error("confirmation token should be cleared after confirmation") } } func TestConfirmEmailInvalid(t *testing.T) { app := testApp(t) mux := testMux(app) w := httptest.NewRecorder() mux.ServeHTTP(w, testRequest("POST", "/api/public/confirm", map[string]any{"token": "nonexistent"})) if w.Code != 200 { t.Fatalf("expected 200, got %d", w.Code) } result := parseJSON(t, w) if result["status"] != "invalid" { t.Errorf("expected invalid, got %v", result["status"]) } } func TestConfirmEmailAlreadyConfirmed(t *testing.T) { app := testApp(t) mux := testMux(app) token := "abc123def456" app.createVolunteer(Volunteer{ Name: "Titania", PreferredName: "Titania", Email: "titania@example.com", ConfirmationToken: &token, }) // Confirm first time w := httptest.NewRecorder() mux.ServeHTTP(w, testRequest("POST", "/api/public/confirm", map[string]any{"token": token})) if parseJSON(t, w)["status"] != "confirmed" { t.Fatal("first confirm should succeed") } // Second confirm with same token should be invalid (token cleared) w = httptest.NewRecorder() mux.ServeHTTP(w, testRequest("POST", "/api/public/confirm", map[string]any{"token": token})) result := parseJSON(t, w) if result["status"] != "invalid" { t.Errorf("expected invalid after token cleared, got %v", result["status"]) } } func TestConfirmEmailWithSignupsOpen(t *testing.T) { app := testApp(t) mux := testMux(app) app.db.Exec(`INSERT OR REPLACE INTO config (key, value) VALUES ('shift_signups_open', 'true')`) app.baseURL = "https://example.com" participant, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"}) token := "abc123def456" app.createVolunteer(Volunteer{ Name: "Titania", PreferredName: "Titania", Email: "titania@example.com", ParticipantID: &participant.ID, ConfirmationToken: &token, }) w := httptest.NewRecorder() mux.ServeHTTP(w, testRequest("POST", "/api/public/confirm", map[string]any{"token": token})) if w.Code != 200 { t.Fatalf("expected 200, got %d", w.Code) } result := parseJSON(t, w) if result["status"] != "confirmed" { t.Fatalf("expected confirmed, got %v", result["status"]) } kioskLink, ok := result["kiosk_link"].(string) if !ok || kioskLink == "" { t.Error("expected kiosk_link when signups are open") } // Ticket for participant should now have a code tickets, _ := app.listTickets(&participant.ID, "") hasCode := false for _, tk := range tickets { if tk.Code != nil && *tk.Code != "" { hasCode = true break } } if !hasCode { t.Error("participant should have a ticket with code after confirm with signups open") } } func TestPublicSignupTicketNameDoesNotOverwritePreferredName(t *testing.T) { app := testApp(t) mux := testMux(app) w := httptest.NewRecorder() mux.ServeHTTP(w, testRequest("POST", "/api/public/signup", map[string]any{ "preferred_name": "Titania", "ticket_name": "Titania Fairweather", "email": "titania@example.com", })) if w.Code != 200 { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } vol, _ := app.getVolunteerByEmail("titania@example.com") if vol == nil || vol.ParticipantID == nil { t.Fatal("volunteer/participant not created") } p, _ := app.getParticipant(*vol.ParticipantID) if p == nil { t.Fatal("participant not found") } if p.PreferredName != "Titania" { t.Errorf("participant preferred_name = %q, want %q (not ticket_name)", p.PreferredName, "Titania") } if vol.TicketName != "Titania Fairweather" { t.Errorf("vol.TicketName = %q, want %q", vol.TicketName, "Titania Fairweather") } } func TestConfirmEmailStubTicketHasName(t *testing.T) { app := testApp(t) mux := testMux(app) app.db.Exec(`INSERT OR REPLACE INTO config (key, value) VALUES ('shift_signups_open', 'true')`) app.baseURL = "https://example.com" // Volunteer with a ticket_name but no pre-existing ticket participant, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"}) token := "abc123def456" app.createVolunteer(Volunteer{ Name: "Titania", PreferredName: "Titania", TicketName: "Titania Fairweather", Email: "titania@example.com", ParticipantID: &participant.ID, ConfirmationToken: &token, }) w := httptest.NewRecorder() mux.ServeHTTP(w, testRequest("POST", "/api/public/confirm", map[string]any{"token": token})) if w.Code != 200 { t.Fatalf("expected 200, got %d", w.Code) } result := parseJSON(t, w) if result["status"] != "confirmed" { t.Fatalf("expected confirmed, got %v", result["status"]) } // Stub ticket should have been created with TicketName as its name tickets, _ := app.listTickets(&participant.ID, "") if len(tickets) == 0 { t.Fatal("expected stub ticket to be created") } if tickets[0].Name != "Titania Fairweather" { t.Errorf("stub ticket name = %q, want %q", tickets[0].Name, "Titania Fairweather") } } func TestConfirmEmailStubTicketFallsBackToPreferredName(t *testing.T) { app := testApp(t) mux := testMux(app) app.db.Exec(`INSERT OR REPLACE INTO config (key, value) VALUES ('shift_signups_open', 'true')`) app.baseURL = "https://example.com" // Volunteer with no ticket_name — stub should use preferred_name participant, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"}) token := "abc123def456" app.createVolunteer(Volunteer{ Name: "Titania", PreferredName: "Titania", Email: "titania@example.com", ParticipantID: &participant.ID, ConfirmationToken: &token, }) w := httptest.NewRecorder() mux.ServeHTTP(w, testRequest("POST", "/api/public/confirm", map[string]any{"token": token})) if w.Code != 200 { t.Fatalf("expected 200, got %d", w.Code) } tickets, _ := app.listTickets(&participant.ID, "") if len(tickets) == 0 { t.Fatal("expected stub ticket to be created") } if tickets[0].Name != "Titania" { t.Errorf("stub ticket name = %q, want %q (preferred_name fallback)", tickets[0].Name, "Titania") } } func TestToggleShiftSignups(t *testing.T) { app := testApp(t) mux := testMux(app) admin := testAdminUser(t, app) tok := testToken(t, app, admin) w := httptest.NewRecorder() mux.ServeHTTP(w, testAuthRequest("POST", "/api/settings/shift-signups", map[string]any{"open": true}, tok)) if w.Code != 200 { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } result := parseJSON(t, w) if result["shift_signups_open"] != true { t.Errorf("expected shift_signups_open true, got %v", result) } // Check config stored var val string app.db.QueryRow(`SELECT value FROM config WHERE key = 'shift_signups_open'`).Scan(&val) if val != "true" { t.Errorf("config not stored, got %q", val) } }