package calendartest import ( "encoding/json" "net" "net/http" "os" "path/filepath" "sync" "testing" "time" ) type controlSchedules struct { mu sync.Mutex at time.Time writes int fail bool unsupported bool } func controlForTest(t *testing.T) *controlSchedules { t.Helper() dir, err := os.MkdirTemp("/tmp", "cal-control-") if err != nil { t.Fatal(err) } t.Cleanup(func() { os.RemoveAll(dir) }) socket := filepath.Join(dir, "control.sock") listener, err := net.Listen("unix", socket) if err != nil { t.Fatal(err) } state := &controlSchedules{} server := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { state.mu.Lock() defer state.mu.Unlock() if r.Header.Get("Authorization") != "Bearer test-token" { http.Error(w, "forbidden", 403) return } if state.fail { http.Error(w, "retry", 503) return } if r.URL.Path == "/schedule-capabilities" { if state.unsupported { w.WriteHeader(404) return } json.NewEncoder(w).Encode(map[string]bool{"registration_fence": true}) return } if r.URL.Path == "/schedule" { var timers []map[string]any if !state.at.IsZero() { timers = append(timers, map[string]any{"name": "calendar-next-due", "next_at": state.at.Unix(), "due_at": state.at.Unix(), "kind": "once", "path": "/jobs/reminders"}) } json.NewEncoder(w).Encode(map[string]any{"timers": timers}) return } if r.URL.Path != "/schedule/calendar-next-due" { w.WriteHeader(404) return } switch r.Method { case http.MethodPut: var body struct { At time.Time Path string } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Error(err) w.WriteHeader(400) return } if body.Path != "/jobs/reminders" { t.Errorf("scheduled path = %q", body.Path) } state.at = body.At case http.MethodDelete: state.at = time.Time{} default: w.WriteHeader(405) return } state.writes++ w.WriteHeader(204) })} go server.Serve(listener) t.Cleanup(func() { server.Close() }) t.Setenv("CARLOS_CONTROL_SOCKET", socket) t.Setenv("CARLOS_ADMIN_TOKEN", "test-token") return state } func (s *controlSchedules) snapshot() (time.Time, int) { s.mu.Lock() defer s.mu.Unlock() return s.at, s.writes } func (s *controlSchedules) waitArmed(t *testing.T) { t.Helper() deadline := time.After(2 * time.Second) for { if at, _ := s.snapshot(); !at.IsZero() { return } select { case <-deadline: t.Fatal("timer was not armed") case <-time.After(time.Millisecond): } } }