Turnpike/handle_signup_test.go

443 lines
13 KiB
Go
Raw Normal View History

2026-03-03 17:59:35 -06:00
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")
2026-03-03 17:59:35 -06:00
}
p, _ := app.getParticipant(*vol.ParticipantID)
if p == nil {
t.Fatal("linked participant not found")
2026-03-03 17:59:35 -06:00
}
if p.Email != "titania@example.com" {
t.Errorf("participant email = %q, want titania@example.com", p.Email)
2026-03-03 17:59:35 -06:00
}
}
func TestPublicSignupAutoMatchParticipant(t *testing.T) {
2026-03-03 17:59:35 -06:00
app := testApp(t)
mux := testMux(app)
// Pre-existing participant
existing, _ := app.createParticipant(Participant{PreferredName: "Titania Fairweather", Email: "titania@example.com"})
2026-03-03 17:59:35 -06:00
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)
2026-03-03 17:59:35 -06:00
}
}
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"})
2026-03-03 17:59:35 -06:00
token := "abc123def456"
app.createVolunteer(Volunteer{
Name: "Titania",
PreferredName: "Titania",
Email: "titania@example.com",
ParticipantID: &participant.ID,
2026-03-03 17:59:35 -06:00
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")
2026-03-03 17:59:35 -06:00
}
}
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")
}
}
2026-03-03 17:59:35 -06:00
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)
}
}