Added volunteer signup.
This commit is contained in:
parent
ace7f11a60
commit
8dc5d3ed01
12 changed files with 1258 additions and 49 deletions
333
handle_signup_test.go
Normal file
333
handle_signup_test.go
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
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")
|
||||
}
|
||||
|
||||
// Attendee should be auto-created and linked
|
||||
if vol.AttendeeID == nil {
|
||||
t.Fatal("expected attendee to be linked")
|
||||
}
|
||||
a, _ := app.getAttendee(*vol.AttendeeID)
|
||||
if a == nil {
|
||||
t.Fatal("linked attendee not found")
|
||||
}
|
||||
if a.Email != "titania@example.com" {
|
||||
t.Errorf("attendee email = %q, want titania@example.com", a.Email)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicSignupAutoMatchAttendee(t *testing.T) {
|
||||
app := testApp(t)
|
||||
mux := testMux(app)
|
||||
|
||||
// Pre-existing attendee
|
||||
existing, _ := app.createAttendee(Attendee{Name: "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.AttendeeID == nil || *vol.AttendeeID != existing.ID {
|
||||
t.Errorf("expected volunteer linked to existing attendee %d, got %v", existing.ID, vol.AttendeeID)
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
attendee, _ := app.createAttendee(Attendee{Name: "Titania", Email: "titania@example.com"})
|
||||
token := "abc123def456"
|
||||
app.createVolunteer(Volunteer{
|
||||
Name: "Titania",
|
||||
PreferredName: "Titania",
|
||||
Email: "titania@example.com",
|
||||
AttendeeID: &attendee.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")
|
||||
}
|
||||
|
||||
// Attendee should now have a kiosk token
|
||||
a, _ := app.getAttendee(attendee.ID)
|
||||
if a.VolunteerToken == nil {
|
||||
t.Error("attendee should have kiosk token after confirm with signups open")
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue