Compare commits

...

No commits in common. "d470fb5707b3cd0437af7837baa361ef0ce28f5d" and "ace7f11a60315a6cf0dd835dff97ba6e6a5cf623" have entirely different histories.

9 changed files with 45 additions and 45 deletions

View file

@ -20,11 +20,11 @@ func TestMigrate(t *testing.T) {
func TestAttendeesCRUD(t *testing.T) { func TestAttendeesCRUD(t *testing.T) {
app := testApp(t) app := testApp(t)
a, err := app.createAttendee(Attendee{Name: "Alice", Email: "alice@test.com", TicketType: "GA"}) a, err := app.createAttendee(Attendee{Name: "Titania", Email: "titania@test.com", TicketType: "GA"})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if a.ID == 0 || a.Name != "Alice" { if a.ID == 0 || a.Name != "Titania" {
t.Errorf("create: got %+v", a) t.Errorf("create: got %+v", a)
} }
@ -32,16 +32,16 @@ func TestAttendeesCRUD(t *testing.T) {
if err != nil || got == nil { if err != nil || got == nil {
t.Fatal("get: not found") t.Fatal("get: not found")
} }
if got.Email != "alice@test.com" { if got.Email != "titania@test.com" {
t.Errorf("get: email = %q", got.Email) t.Errorf("get: email = %q", got.Email)
} }
got.Name = "Alice Smith" got.Name = "Titania Fairweather"
if err := app.updateAttendee(*got); err != nil { if err := app.updateAttendee(*got); err != nil {
t.Fatal(err) t.Fatal(err)
} }
got2, _ := app.getAttendee(a.ID) got2, _ := app.getAttendee(a.ID)
if got2.Name != "Alice Smith" { if got2.Name != "Titania Fairweather" {
t.Errorf("update: name = %q", got2.Name) t.Errorf("update: name = %q", got2.Name)
} }
@ -60,9 +60,9 @@ func TestAttendeesCRUD(t *testing.T) {
func TestIncrementPartySize(t *testing.T) { func TestIncrementPartySize(t *testing.T) {
app := testApp(t) app := testApp(t)
app.createAttendee(Attendee{Name: "Bob", TicketID: "ORD-100"}) app.createAttendee(Attendee{Name: "Oberon", TicketID: "ORD-100"})
merged, err := app.incrementPartySize("Bob", "ORD-100") merged, err := app.incrementPartySize("Oberon", "ORD-100")
if err != nil || !merged { if err != nil || !merged {
t.Fatalf("increment: merged=%v, err=%v", merged, err) t.Fatalf("increment: merged=%v, err=%v", merged, err)
} }
@ -73,7 +73,7 @@ func TestIncrementPartySize(t *testing.T) {
} }
// Different ticket_id should not merge // Different ticket_id should not merge
merged2, _ := app.incrementPartySize("Bob", "ORD-200") merged2, _ := app.incrementPartySize("Oberon", "ORD-200")
if merged2 { if merged2 {
t.Error("should not merge different ticket_id") t.Error("should not merge different ticket_id")
} }
@ -83,7 +83,7 @@ func TestCheckInAttendee(t *testing.T) {
app := testApp(t) app := testApp(t)
admin := testAdminUser(t, app) admin := testAdminUser(t, app)
app.createAttendee(Attendee{Name: "Charlie"}) app.createAttendee(Attendee{Name: "Puck"})
// Set party_size directly since createAttendee defaults to 1 // Set party_size directly since createAttendee defaults to 1
app.db.Exec(`UPDATE attendees SET party_size = 3 WHERE id = 1`) app.db.Exec(`UPDATE attendees SET party_size = 3 WHERE id = 1`)
@ -197,7 +197,7 @@ func TestAssignAndUnassignShift(t *testing.T) {
dept, _ := app.createDepartment(Department{Name: "Gate"}) dept, _ := app.createDepartment(Department{Name: "Gate"})
deptID := dept.ID deptID := dept.ID
s, _ := app.createShift(Shift{DepartmentID: deptID, Name: "AM", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00", Capacity: 2}) s, _ := app.createShift(Shift{DepartmentID: deptID, Name: "AM", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00", Capacity: 2})
v, _ := app.createVolunteer(Volunteer{Name: "Dana", DepartmentID: &deptID}) v, _ := app.createVolunteer(Volunteer{Name: "Helena", DepartmentID: &deptID})
if err := app.assignShift(v.ID, s.ID); err != nil { if err := app.assignShift(v.ID, s.ID); err != nil {
t.Fatal(err) t.Fatal(err)
@ -221,7 +221,7 @@ func TestCheckShiftConflict(t *testing.T) {
dept, _ := app.createDepartment(Department{Name: "Gate"}) dept, _ := app.createDepartment(Department{Name: "Gate"})
deptID := dept.ID deptID := dept.ID
v, _ := app.createVolunteer(Volunteer{Name: "Eve", DepartmentID: &deptID}) v, _ := app.createVolunteer(Volunteer{Name: "Hermia", DepartmentID: &deptID})
s1, _ := app.createShift(Shift{DepartmentID: deptID, Name: "Morning", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00"}) s1, _ := app.createShift(Shift{DepartmentID: deptID, Name: "Morning", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00"})
s2, _ := app.createShift(Shift{DepartmentID: deptID, Name: "Overlap", Day: "2026-03-15", StartTime: "10:00", EndTime: "14:00"}) s2, _ := app.createShift(Shift{DepartmentID: deptID, Name: "Overlap", Day: "2026-03-15", StartTime: "10:00", EndTime: "14:00"})
@ -250,7 +250,7 @@ func TestCheckShiftConflictMidnight(t *testing.T) {
dept, _ := app.createDepartment(Department{Name: "Sound"}) dept, _ := app.createDepartment(Department{Name: "Sound"})
deptID := dept.ID deptID := dept.ID
v, _ := app.createVolunteer(Volunteer{Name: "Frank", DepartmentID: &deptID}) v, _ := app.createVolunteer(Volunteer{Name: "Lysander", DepartmentID: &deptID})
// Night shift: 22:00-02:00 (spans midnight) // Night shift: 22:00-02:00 (spans midnight)
night, _ := app.createShift(Shift{DepartmentID: deptID, Name: "Night", Day: "2026-03-15", StartTime: "22:00", EndTime: "02:00"}) night, _ := app.createShift(Shift{DepartmentID: deptID, Name: "Night", Day: "2026-03-15", StartTime: "22:00", EndTime: "02:00"})

View file

@ -57,7 +57,7 @@ Column matching is case-insensitive. Extra columns are ignored. BOM-encoded file
CrowdWork exports one row per ticket, even when the same person bought multiple tickets in one order. Turnpike handles this automatically: CrowdWork exports one row per ticket, even when the same person bought multiple tickets in one order. Turnpike handles this automatically:
- First row for "Alice Smith" (order 1234) creates a record with `party_size=1` - First row for "Titania Fairweather" (order 1234) creates a record with `party_size=1`
- Subsequent rows with the same name + order number increment `party_size` (no duplicate record) - Subsequent rows with the same name + order number increment `party_size` (no duplicate record)
- Result: one attendee record, `party_size=3` if three tickets were purchased - Result: one attendee record, `party_size=3` if three tickets were purchased

View file

@ -50,9 +50,9 @@ describe('apiFetch', () => {
describe('apiJSON', () => { describe('apiJSON', () => {
it('parses JSON response', async () => { it('parses JSON response', async () => {
mockFetch({ name: 'Alice' }) mockFetch({ name: 'Titania' })
const result = await apiJSON('/api/test') const result = await apiJSON('/api/test')
expect(result.name).toBe('Alice') expect(result.name).toBe('Titania')
}) })
it('throws on non-OK response', async () => { it('throws on non-OK response', async () => {

View file

@ -21,7 +21,7 @@ describe('syncPull', () => {
it('writes attendees to Dexie', async () => { it('writes attendees to Dexie', async () => {
mockFetch({ mockFetch({
server_time: '2026-03-01T12:00:00Z', server_time: '2026-03-01T12:00:00Z',
attendees: [{ id: 1, name: 'Alice' }], attendees: [{ id: 1, name: 'Titania' }],
departments: [], departments: [],
volunteers: [], volunteers: [],
shifts: [], shifts: [],
@ -32,16 +32,16 @@ describe('syncPull', () => {
await syncPull() await syncPull()
const a = await db.attendees.get(1) const a = await db.attendees.get(1)
expect(a.name).toBe('Alice') expect(a.name).toBe('Titania')
expect(await getLastSync()).toBe('2026-03-01T12:00:00Z') expect(await getLastSync()).toBe('2026-03-01T12:00:00Z')
}) })
it('deletes soft-deleted attendees from Dexie', async () => { it('deletes soft-deleted attendees from Dexie', async () => {
await db.attendees.put({ id: 1, name: 'Alice' }) await db.attendees.put({ id: 1, name: 'Titania' })
mockFetch({ mockFetch({
server_time: '2026-03-01T13:00:00Z', server_time: '2026-03-01T13:00:00Z',
attendees: [{ id: 1, name: 'Alice', deleted_at: '2026-03-01T12:30:00Z' }], attendees: [{ id: 1, name: 'Titania', deleted_at: '2026-03-01T12:30:00Z' }],
departments: [], departments: [],
volunteers: [], volunteers: [],
shifts: [], shifts: [],

View file

@ -13,7 +13,7 @@ func TestAttendeesListCreateDelete(t *testing.T) {
mux := testMux(app) mux := testMux(app)
// Create // Create
req := testAuthRequest("POST", "/api/attendees", map[string]string{"name": "Alice"}, token) req := testAuthRequest("POST", "/api/attendees", map[string]string{"name": "Titania"}, token)
w := httptest.NewRecorder() w := httptest.NewRecorder()
mux.ServeHTTP(w, req) mux.ServeHTTP(w, req)
if w.Code != http.StatusCreated { if w.Code != http.StatusCreated {
@ -59,7 +59,7 @@ func TestCheckInAttendeeHandler(t *testing.T) {
token := testToken(t, app, admin) token := testToken(t, app, admin)
mux := testMux(app) mux := testMux(app)
app.createAttendee(Attendee{Name: "Bob"}) app.createAttendee(Attendee{Name: "Oberon"})
app.db.Exec(`UPDATE attendees SET party_size = 3 WHERE id = 1`) app.db.Exec(`UPDATE attendees SET party_size = 3 WHERE id = 1`)
// Check in 1 // Check in 1
@ -82,7 +82,7 @@ func TestGateRoleCanCheckIn(t *testing.T) {
token := testToken(t, app, gate) token := testToken(t, app, gate)
mux := testMux(app) mux := testMux(app)
app.createAttendee(Attendee{Name: "Charlie"}) app.createAttendee(Attendee{Name: "Puck"})
req := testAuthRequest("POST", "/api/attendees/1/checkin", nil, token) req := testAuthRequest("POST", "/api/attendees/1/checkin", nil, token)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -98,7 +98,7 @@ func TestGateRoleCannotDelete(t *testing.T) {
token := testToken(t, app, gate) token := testToken(t, app, gate)
mux := testMux(app) mux := testMux(app)
app.createAttendee(Attendee{Name: "Charlie"}) app.createAttendee(Attendee{Name: "Puck"})
req := testAuthRequest("DELETE", "/api/attendees/1", nil, token) req := testAuthRequest("DELETE", "/api/attendees/1", nil, token)
w := httptest.NewRecorder() w := httptest.NewRecorder()

View file

@ -31,7 +31,7 @@ func TestImportCrowdWorkFormat(t *testing.T) {
token := testToken(t, app, admin) token := testToken(t, app, admin)
mux := testMux(app) mux := testMux(app)
csv := "Patron Name,Patron Email,Order Number,Tier Name\nAlice,alice@test.com,ORD-1,GA\nBob,bob@test.com,ORD-2,VIP\n" csv := "Patron Name,Patron Email,Order Number,Tier Name\nTitania,titania@test.com,ORD-1,GA\nOberon,oberon@test.com,ORD-2,VIP\n"
w := postCSV(t, mux, token, csv) w := postCSV(t, mux, token, csv)
if w.Code != http.StatusOK { if w.Code != http.StatusOK {
@ -49,7 +49,7 @@ func TestImportGenericFormat(t *testing.T) {
token := testToken(t, app, admin) token := testToken(t, app, admin)
mux := testMux(app) mux := testMux(app)
csv := "name,email,ticket_id,ticket_type,note\nAlice,alice@test.com,T1,GA,VIP guest\n" csv := "name,email,ticket_id,ticket_type,note\nTitania,titania@test.com,T1,GA,VIP guest\n"
w := postCSV(t, mux, token, csv) w := postCSV(t, mux, token, csv)
if w.Code != http.StatusOK { if w.Code != http.StatusOK {
@ -68,7 +68,7 @@ func TestImportPartySizeDedup(t *testing.T) {
mux := testMux(app) mux := testMux(app)
// 3 rows same name+order = 1 record, party_size=3 // 3 rows same name+order = 1 record, party_size=3
csv := "Patron Name,Patron Email,Order Number,Tier Name\nAlice,alice@test.com,ORD-1,GA\nAlice,alice@test.com,ORD-1,GA\nAlice,alice@test.com,ORD-1,GA\n" csv := "Patron Name,Patron Email,Order Number,Tier Name\nTitania,titania@test.com,ORD-1,GA\nTitania,titania@test.com,ORD-1,GA\nTitania,titania@test.com,ORD-1,GA\n"
w := postCSV(t, mux, token, csv) w := postCSV(t, mux, token, csv)
result := parseJSON(t, w) result := parseJSON(t, w)
@ -94,7 +94,7 @@ func TestImportReimportSkips(t *testing.T) {
token := testToken(t, app, admin) token := testToken(t, app, admin)
mux := testMux(app) mux := testMux(app)
csv := "name\nAlice\nBob\n" csv := "name\nTitania\nOberon\n"
postCSV(t, mux, token, csv) postCSV(t, mux, token, csv)
// Re-import same data // Re-import same data
@ -114,7 +114,7 @@ func TestImportMissingNameColumn(t *testing.T) {
token := testToken(t, app, admin) token := testToken(t, app, admin)
mux := testMux(app) mux := testMux(app)
csv := "email,phone\nalice@test.com,555-1234\n" csv := "email,phone\ntitania@test.com,555-1234\n"
w := postCSV(t, mux, token, csv) w := postCSV(t, mux, token, csv)
if w.Code != http.StatusBadRequest { if w.Code != http.StatusBadRequest {
@ -129,7 +129,7 @@ func TestImportBOM(t *testing.T) {
mux := testMux(app) mux := testMux(app)
// BOM-encoded CSV // BOM-encoded CSV
csv := "\xef\xbb\xbfname,email\nAlice,alice@test.com\n" csv := "\xef\xbb\xbfname,email\nTitania,titania@test.com\n"
w := postCSV(t, mux, token, csv) w := postCSV(t, mux, token, csv)
if w.Code != http.StatusOK { if w.Code != http.StatusOK {

View file

@ -15,12 +15,12 @@ func setupKiosk(t *testing.T) (*App, *http.ServeMux, string) {
deptID := dept.ID deptID := dept.ID
// Create attendee with token // Create attendee with token
a, _ := app.createAttendee(Attendee{Name: "Alice", Email: "alice@test.com"}) a, _ := app.createAttendee(Attendee{Name: "Titania", Email: "titania@test.com"})
token, _ := app.generateUniqueToken() token, _ := app.generateUniqueToken()
app.db.Exec(`UPDATE attendees SET volunteer_token = ? WHERE id = ?`, token, a.ID) app.db.Exec(`UPDATE attendees SET volunteer_token = ? WHERE id = ?`, token, a.ID)
// Create linked volunteer // Create linked volunteer
app.createVolunteer(Volunteer{Name: "Alice", AttendeeID: &a.ID, DepartmentID: &deptID}) app.createVolunteer(Volunteer{Name: "Titania", AttendeeID: &a.ID, DepartmentID: &deptID})
// Create shifts // 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: "Morning", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00", Capacity: 2})

View file

@ -55,7 +55,7 @@ func TestShiftAssignVolunteer(t *testing.T) {
dept, _ := app.createDepartment(Department{Name: "Gate"}) dept, _ := app.createDepartment(Department{Name: "Gate"})
deptID := dept.ID deptID := dept.ID
app.createShift(Shift{DepartmentID: deptID, Name: "AM", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00"}) app.createShift(Shift{DepartmentID: deptID, Name: "AM", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00"})
app.createVolunteer(Volunteer{Name: "Alice", DepartmentID: &deptID}) app.createVolunteer(Volunteer{Name: "Titania", DepartmentID: &deptID})
// Assign // Assign
req := testAuthRequest("POST", "/api/shifts/1/volunteers", map[string]any{ req := testAuthRequest("POST", "/api/shifts/1/volunteers", map[string]any{
@ -86,7 +86,7 @@ func TestShiftAssignConflict(t *testing.T) {
deptID := dept.ID deptID := dept.ID
app.createShift(Shift{DepartmentID: deptID, Name: "AM", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00"}) app.createShift(Shift{DepartmentID: deptID, Name: "AM", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00"})
app.createShift(Shift{DepartmentID: deptID, Name: "Overlap", Day: "2026-03-15", StartTime: "10:00", EndTime: "14:00"}) app.createShift(Shift{DepartmentID: deptID, Name: "Overlap", Day: "2026-03-15", StartTime: "10:00", EndTime: "14:00"})
app.createVolunteer(Volunteer{Name: "Alice", DepartmentID: &deptID}) app.createVolunteer(Volunteer{Name: "Titania", DepartmentID: &deptID})
// Assign to first shift // Assign to first shift
app.assignShift(1, 1) app.assignShift(1, 1)

View file

@ -13,10 +13,10 @@ func TestSyncPullFull(t *testing.T) {
token := testToken(t, app, admin) token := testToken(t, app, admin)
mux := testMux(app) mux := testMux(app)
app.createAttendee(Attendee{Name: "Alice"}) app.createAttendee(Attendee{Name: "Titania"})
dept, _ := app.createDepartment(Department{Name: "Gate"}) dept, _ := app.createDepartment(Department{Name: "Gate"})
deptID := dept.ID deptID := dept.ID
app.createVolunteer(Volunteer{Name: "Alice", DepartmentID: &deptID}) app.createVolunteer(Volunteer{Name: "Titania", DepartmentID: &deptID})
app.createShift(Shift{DepartmentID: deptID, Name: "AM", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00"}) app.createShift(Shift{DepartmentID: deptID, Name: "AM", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00"})
req := testAuthRequest("GET", "/api/sync/pull", nil, token) req := testAuthRequest("GET", "/api/sync/pull", nil, token)
@ -47,14 +47,14 @@ func TestSyncPullIncremental(t *testing.T) {
token := testToken(t, app, admin) token := testToken(t, app, admin)
mux := testMux(app) mux := testMux(app)
app.createAttendee(Attendee{Name: "Alice"}) app.createAttendee(Attendee{Name: "Titania"})
// Backdate Alice so she falls before the "since" cutoff // Backdate Titania so she falls before the "since" cutoff
app.db.Exec(`UPDATE attendees SET updated_at = '2026-01-01T00:00:00Z' WHERE name = 'Alice'`) app.db.Exec(`UPDATE attendees SET updated_at = '2026-01-01T00:00:00Z' WHERE name = 'Titania'`)
since := "2026-01-01T12:00:00Z" since := "2026-01-01T12:00:00Z"
// Bob created with default updated_at (now), which is after our since // Oberon created with default updated_at (now), which is after our since
app.createAttendee(Attendee{Name: "Bob"}) app.createAttendee(Attendee{Name: "Oberon"})
req := testAuthRequest("GET", "/api/sync/pull?since="+since, nil, token) req := testAuthRequest("GET", "/api/sync/pull?since="+since, nil, token)
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -62,14 +62,14 @@ func TestSyncPullIncremental(t *testing.T) {
result := parseJSON(t, w) result := parseJSON(t, w)
attendees := result["attendees"].([]any) attendees := result["attendees"].([]any)
// Should only include Bob (created after `since`) // Should only include Oberon (created after `since`)
if len(attendees) != 1 { if len(attendees) != 1 {
t.Errorf("incremental: got %d attendees, want 1", len(attendees)) t.Errorf("incremental: got %d attendees, want 1", len(attendees))
} }
if len(attendees) == 1 { if len(attendees) == 1 {
a := attendees[0].(map[string]any) a := attendees[0].(map[string]any)
if a["name"] != "Bob" { if a["name"] != "Oberon" {
t.Errorf("name = %v, want Bob", a["name"]) t.Errorf("name = %v, want Oberon", a["name"])
} }
} }
} }
@ -80,8 +80,8 @@ func TestSyncPullIncludesSoftDeleted(t *testing.T) {
token := testToken(t, app, admin) token := testToken(t, app, admin)
mux := testMux(app) mux := testMux(app)
a, _ := app.createAttendee(Attendee{Name: "Alice"}) a, _ := app.createAttendee(Attendee{Name: "Titania"})
// Backdate Alice's creation so the since cutoff is between creation and deletion // Backdate Titania's creation so the since cutoff is between creation and deletion
app.db.Exec(`UPDATE attendees SET updated_at = '2026-01-01T00:00:00Z' WHERE id = ?`, a.ID) app.db.Exec(`UPDATE attendees SET updated_at = '2026-01-01T00:00:00Z' WHERE id = ?`, a.ID)
since := "2026-01-01T12:00:00Z" since := "2026-01-01T12:00:00Z"