oficina / calendar Public

Clone
git clone https://amadan.net/oficina/calendar

Plain git — no account needed to clone.

Download

Download this file

1package calendartest
2
3import (
4 "encoding/json"
5 "net"
6 "net/http"
7 "os"
8 "path/filepath"
9 "sync"
10 "testing"
11 "time"
12)
13
14type controlSchedules struct {
15 mu sync.Mutex
16 at time.Time
17 writes int
18 fail bool
19 unsupported bool
20}
21
22func controlForTest(t *testing.T) *controlSchedules {
23 t.Helper()
24 dir, err := os.MkdirTemp("/tmp", "cal-control-")
25 if err != nil {
26 t.Fatal(err)
27 }
28 t.Cleanup(func() { os.RemoveAll(dir) })
29 socket := filepath.Join(dir, "control.sock")
30 listener, err := net.Listen("unix", socket)
31 if err != nil {
32 t.Fatal(err)
33 }
34 state := &controlSchedules{}
35 server := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
36 state.mu.Lock()
37 defer state.mu.Unlock()
38 if r.Header.Get("Authorization") != "Bearer test-token" {
39 http.Error(w, "forbidden", 403)
40 return
41 }
42 if state.fail {
43 http.Error(w, "retry", 503)
44 return
45 }
46 if r.URL.Path == "/schedule-capabilities" {
47 if state.unsupported {
48 w.WriteHeader(404)
49 return
50 }
51 json.NewEncoder(w).Encode(map[string]bool{"registration_fence": true})
52 return
53 }
54 if r.URL.Path == "/schedule" {
55 var timers []map[string]any
56 if !state.at.IsZero() {
57 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"})
58 }
59 json.NewEncoder(w).Encode(map[string]any{"timers": timers})
60 return
61 }
62 if r.URL.Path != "/schedule/calendar-next-due" {
63 w.WriteHeader(404)
64 return
65 }
66 switch r.Method {
67 case http.MethodPut:
68 var body struct {
69 At time.Time
70 Path string
71 }
72 if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
73 t.Error(err)
74 w.WriteHeader(400)
75 return
76 }
77 if body.Path != "/jobs/reminders" {
78 t.Errorf("scheduled path = %q", body.Path)
79 }
80 state.at = body.At
81 case http.MethodDelete:
82 state.at = time.Time{}
83 default:
84 w.WriteHeader(405)
85 return
86 }
87 state.writes++
88 w.WriteHeader(204)
89 })}
90 go server.Serve(listener)
91 t.Cleanup(func() { server.Close() })
92 t.Setenv("CARLOS_CONTROL_SOCKET", socket)
93 t.Setenv("CARLOS_ADMIN_TOKEN", "test-token")
94 return state
95}
96
97func (s *controlSchedules) snapshot() (time.Time, int) {
98 s.mu.Lock()
99 defer s.mu.Unlock()
100 return s.at, s.writes
101}
102
103func (s *controlSchedules) waitArmed(t *testing.T) {
104 t.Helper()
105 deadline := time.After(2 * time.Second)
106 for {
107 if at, _ := s.snapshot(); !at.IsZero() {
108 return
109 }
110 select {
111 case <-deadline:
112 t.Fatal("timer was not armed")
113 case <-time.After(time.Millisecond):
114 }
115 }
116}
117