2026-03-03 12:50:24 -06:00
|
|
|
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)
|
|
|
|
|
|
2026-03-04 15:27:03 -06:00
|
|
|
app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"})
|
2026-03-03 12:50:24 -06:00
|
|
|
dept, _ := app.createDepartment(Department{Name: "Gate"})
|
|
|
|
|
deptID := dept.ID
|
|
|
|
|
app.createVolunteer(Volunteer{Name: "Titania", 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")
|
|
|
|
|
}
|
2026-03-04 15:27:03 -06:00
|
|
|
participants := result["participants"].([]any)
|
|
|
|
|
if len(participants) != 1 {
|
|
|
|
|
t.Errorf("participants = %d, want 1", len(participants))
|
2026-03-03 12:50:24 -06:00
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
|
2026-03-04 15:27:03 -06:00
|
|
|
p1, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"})
|
2026-03-03 12:50:24 -06:00
|
|
|
// Backdate Titania so she falls before the "since" cutoff
|
2026-03-04 15:27:03 -06:00
|
|
|
app.db.Exec(`UPDATE participants SET updated_at = '2026-01-01T00:00:00Z' WHERE id = ?`, p1.ID)
|
2026-03-03 12:50:24 -06:00
|
|
|
|
|
|
|
|
since := "2026-01-01T12:00:00Z"
|
|
|
|
|
|
|
|
|
|
// Oberon created with default updated_at (now), which is after our since
|
2026-03-04 15:27:03 -06:00
|
|
|
app.createParticipant(Participant{PreferredName: "Oberon", Email: "oberon@example.com"})
|
2026-03-03 12:50:24 -06:00
|
|
|
|
|
|
|
|
req := testAuthRequest("GET", "/api/sync/pull?since="+since, nil, token)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
mux.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
result := parseJSON(t, w)
|
2026-03-04 15:27:03 -06:00
|
|
|
participants := result["participants"].([]any)
|
2026-03-03 12:50:24 -06:00
|
|
|
// Should only include Oberon (created after `since`)
|
2026-03-04 15:27:03 -06:00
|
|
|
if len(participants) != 1 {
|
|
|
|
|
t.Errorf("incremental: got %d participants, want 1", len(participants))
|
2026-03-03 12:50:24 -06:00
|
|
|
}
|
2026-03-04 15:27:03 -06:00
|
|
|
if len(participants) == 1 {
|
|
|
|
|
p := participants[0].(map[string]any)
|
|
|
|
|
if p["preferred_name"] != "Oberon" {
|
|
|
|
|
t.Errorf("preferred_name = %v, want Oberon", p["preferred_name"])
|
2026-03-03 12:50:24 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSyncPullIncludesSoftDeleted(t *testing.T) {
|
|
|
|
|
app := testApp(t)
|
|
|
|
|
admin := testAdminUser(t, app)
|
|
|
|
|
token := testToken(t, app, admin)
|
|
|
|
|
mux := testMux(app)
|
|
|
|
|
|
2026-03-04 15:27:03 -06:00
|
|
|
p, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"})
|
2026-03-03 12:50:24 -06:00
|
|
|
// Backdate Titania's creation so the since cutoff is between creation and deletion
|
2026-03-04 15:27:03 -06:00
|
|
|
app.db.Exec(`UPDATE participants SET updated_at = '2026-01-01T00:00:00Z' WHERE id = ?`, p.ID)
|
2026-03-03 12:50:24 -06:00
|
|
|
|
|
|
|
|
since := "2026-01-01T12:00:00Z"
|
|
|
|
|
|
|
|
|
|
// Delete updates updated_at to now(), which is after our since
|
2026-03-04 15:27:03 -06:00
|
|
|
app.deleteParticipant(p.ID)
|
2026-03-03 12:50:24 -06:00
|
|
|
|
|
|
|
|
req := testAuthRequest("GET", "/api/sync/pull?since="+since, nil, token)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
mux.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
var result struct {
|
2026-03-04 15:27:03 -06:00
|
|
|
Participants []struct {
|
2026-03-03 12:50:24 -06:00
|
|
|
ID int `json:"id"`
|
|
|
|
|
DeletedAt *string `json:"deleted_at"`
|
2026-03-04 15:27:03 -06:00
|
|
|
} `json:"participants"`
|
2026-03-03 12:50:24 -06:00
|
|
|
}
|
|
|
|
|
json.Unmarshal(w.Body.Bytes(), &result)
|
|
|
|
|
|
2026-03-04 15:27:03 -06:00
|
|
|
if len(result.Participants) != 1 {
|
|
|
|
|
t.Fatalf("got %d participants, want 1", len(result.Participants))
|
2026-03-03 12:50:24 -06:00
|
|
|
}
|
2026-03-04 15:27:03 -06:00
|
|
|
if result.Participants[0].DeletedAt == nil {
|
2026-03-03 12:50:24 -06:00
|
|
|
t.Error("deleted_at should be set for soft-deleted record")
|
|
|
|
|
}
|
|
|
|
|
}
|