package main import ( "encoding/json" "net/http" "net/http/httptest" "testing" ) func TestSyncPullFull(t *testing.T) { app := testApp(t) admin := testAdminUser(t, app) token := testToken(t, app, admin) mux := testMux(app) p, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"}) dept, _ := app.createDepartment(Department{Name: "Gate"}) deptID := dept.ID app.createVolunteer(Volunteer{Name: "Titania", ParticipantID: p.ID, DepartmentID: &deptID}) 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) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d", w.Code) } result := parseJSON(t, w) if result["server_time"] == nil { t.Error("missing server_time") } participants := result["participants"].([]any) if len(participants) != 2 { // admin + Titania t.Errorf("participants = %d, want 2", len(participants)) } depts := result["departments"].([]any) if len(depts) != 1 { t.Errorf("departments = %d, want 1", len(depts)) } } func TestSyncPullIncremental(t *testing.T) { app := testApp(t) admin := testAdminUser(t, app) token := testToken(t, app, admin) mux := testMux(app) // Backdate admin participant so it falls before the "since" cutoff. app.db.Exec(`UPDATE participants SET updated_at = '2026-01-01T00:00:00Z' WHERE id = ?`, admin.ID) p1, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"}) app.db.Exec(`UPDATE participants SET updated_at = '2026-01-01T00:00:00Z' WHERE id = ?`, p1.ID) since := "2026-01-01T12:00:00Z" // Lysander created with default updated_at (now), which is after our since app.createParticipant(Participant{PreferredName: "Lysander", Email: "lysander@example.com"}) req := testAuthRequest("GET", "/api/sync/pull?since="+since, nil, token) w := httptest.NewRecorder() mux.ServeHTTP(w, req) result := parseJSON(t, w) participants := result["participants"].([]any) if len(participants) != 1 { t.Errorf("incremental: got %d participants, want 1", len(participants)) } if len(participants) == 1 { p := participants[0].(map[string]any) if p["preferred_name"] != "Lysander" { t.Errorf("preferred_name = %v, want Lysander", p["preferred_name"]) } } } func TestSyncPullIncludesSoftDeleted(t *testing.T) { app := testApp(t) admin := testAdminUser(t, app) token := testToken(t, app, admin) mux := testMux(app) // Backdate admin participant. app.db.Exec(`UPDATE participants SET updated_at = '2026-01-01T00:00:00Z' WHERE id = ?`, admin.ID) p, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"}) app.db.Exec(`UPDATE participants SET updated_at = '2026-01-01T00:00:00Z' WHERE id = ?`, p.ID) since := "2026-01-01T12:00:00Z" // Delete updates updated_at to now(), which is after our since app.deleteParticipant(p.ID) req := testAuthRequest("GET", "/api/sync/pull?since="+since, nil, token) w := httptest.NewRecorder() mux.ServeHTTP(w, req) var result struct { Participants []struct { ID int `json:"id"` DeletedAt *string `json:"deleted_at"` } `json:"participants"` } json.Unmarshal(w.Body.Bytes(), &result) if len(result.Participants) != 1 { t.Fatalf("got %d participants, want 1", len(result.Participants)) } if result.Participants[0].DeletedAt == nil { t.Error("deleted_at should be set for soft-deleted record") } }