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 "net/http"
5 "net/http/httptest"
6 "net/url"
7 "strings"
8 "testing"
9 "time"
10
11 calendar "amadan.net/oficina/calendar/internal/calendar"
12)
13
14// A reminder must actually be sent, by the path that really sends it.
15//
16// Written after DrainOnRequest shipped as dead code — defined,
17// commented, described in the spec as one of three entrances, and
18// called from nowhere. Every unit around it passed. This asserts the
19// observable end: a message arrived.
20func TestAReminderIsActuallySent(t *testing.T) {
21 // The tick guard is a constant-time comparison against this
22 // instance's own token, and it is false when the environment has
23 // none — so a test that does not set it exercises the refusal
24 // rather than the drain.
25 t.Setenv("CARLOS_ADMIN_TOKEN", "test-token")
26
27 app, session := signedIn(t, "someone@example.com")
28 h := app.Handler()
29
30 // An event soon enough that its default ten-minute reminder is
31 // already due. The reminder is created with the event.
32 //
33 // UTC, because that is the test person's zone. Formatting in the
34 // SERVER's local zone is the mistake this comment exists to stop
35 // coming back: the app reads a submitted wall time in the person's
36 // zone, so on a machine at +01:00 every event landed an hour
37 // further out than intended and no reminder was ever due.
38 start := time.Now().UTC().Add(2 * time.Minute)
39 createEvent(t, h, session, url.Values{
40 "title": {"Imminent standup"},
41 "start": {start.Format("2006-01-02T15:04")},
42 "end": {start.Add(15 * time.Minute).Format("2006-01-02T15:04")},
43 "kind": {"meeting"},
44 "status": {"confirmed"},
45 "repeat": {"none"},
46 })
47
48 // The sign-in link is the last mail so far; anything after this is
49 // the reminder.
50 before, _ := app.LastMailForTest()
51
52 req := httptest.NewRequest(http.MethodPost, "/jobs/reminders", nil)
53 req.Header.Set("Authorization", "Bearer test-token")
54 rec := httptest.NewRecorder()
55 h.ServeHTTP(rec, req)
56
57 if rec.Code != http.StatusNoContent {
58 t.Fatalf("POST /jobs/reminders with the right bearer: status %d, want 204\n%s",
59 rec.Code, rec.Body.String())
60 }
61
62 after, err := app.LastMailForTest()
63 if err != nil {
64 t.Fatalf("LastMailForTest: %v", err)
65 }
66 if after == before {
67 t.Fatal("the drain sent nothing; the reminder never fired")
68 }
69 if !strings.Contains(after, "Imminent standup") {
70 t.Errorf("the reminder does not name the event:\n%s", after)
71 }
72 // The message carries a link back to the event, which is the whole
73 // point of receiving it on a phone.
74 if !strings.Contains(after, "/events/") {
75 t.Errorf("the reminder carries no link to the event:\n%s", after)
76 }
77}
78
79// The same occurrence must not be sent twice by two drains running at
80// once — a tick and a page view, say. The claim is a conditional
81// UPDATE for exactly this reason.
82func TestAReminderIsNotSentTwice(t *testing.T) {
83 t.Setenv("CARLOS_ADMIN_TOKEN", "test-token")
84
85 app, session := signedIn(t, "someone@example.com")
86 h := app.Handler()
87
88 start := time.Now().UTC().Add(2 * time.Minute)
89 createEvent(t, h, session, url.Values{
90 "title": {"Only once"},
91 "start": {start.Format("2006-01-02T15:04")},
92 "end": {start.Add(15 * time.Minute).Format("2006-01-02T15:04")},
93 "kind": {"meeting"},
94 "status": {"confirmed"},
95 "repeat": {"none"},
96 })
97
98 tick := func() {
99 req := httptest.NewRequest(http.MethodPost, "/jobs/reminders", nil)
100 req.Header.Set("Authorization", "Bearer test-token")
101 h.ServeHTTP(httptest.NewRecorder(), req)
102 }
103
104 tick()
105 first, _ := app.LastMailForTest()
106 if !strings.Contains(first, "Only once") {
107 t.Fatalf("the first drain did not send the reminder:\n%s", first)
108 }
109
110 // A second drain has nothing left to do: the row is marked fired,
111 // and a single event has no next occurrence to queue.
112 tick()
113 second, _ := app.LastMailForTest()
114 if second != first {
115 t.Errorf("a second drain sent something else; the occurrence was not marked fired:\n%s", second)
116 }
117}
118
119// NextDue is what the platform asks before hibernating. It must name
120// the earliest unfired reminder, or the instance sleeps through it.
121func TestNextDuePointsAtTheEarliestReminder(t *testing.T) {
122 app, session := signedIn(t, "someone@example.com")
123 h := app.Handler()
124
125 if due := settledNextDue(t, app); !due.IsZero() {
126 t.Errorf("NextDue on an empty calendar = %v, want the zero time", due)
127 }
128
129 start := time.Now().UTC().Add(3 * time.Hour)
130 createEvent(t, h, session, url.Values{
131 "title": {"Later"},
132 "start": {start.Format("2006-01-02T15:04")},
133 "end": {start.Add(30 * time.Minute).Format("2006-01-02T15:04")},
134 "kind": {"meeting"},
135 "status": {"confirmed"},
136 "repeat": {"none"},
137 })
138
139 due := settledNextDue(t, app)
140 if due.IsZero() {
141 t.Fatal("NextDue is zero with a reminder queued; the platform would hibernate through it")
142 }
143 // The default reminder is ten minutes before the start.
144 want := start.Add(-10 * time.Minute)
145 if diff := due.Sub(want); diff > time.Minute || diff < -time.Minute {
146 t.Errorf("NextDue = %v, want about %v (ten minutes before the event)", due, want)
147 }
148}
149
150// A reminder on a repeating event queues the NEXT occurrence after it
151// fires — one row per rule, never a materialised series.
152func TestARecurringReminderRequeuesItself(t *testing.T) {
153 t.Setenv("CARLOS_ADMIN_TOKEN", "test-token")
154
155 app, session := signedIn(t, "someone@example.com")
156 h := app.Handler()
157
158 start := time.Now().UTC().Add(2 * time.Minute)
159 createEvent(t, h, session, url.Values{
160 "title": {"Daily thing"},
161 "start": {start.Format("2006-01-02T15:04")},
162 "end": {start.Add(15 * time.Minute).Format("2006-01-02T15:04")},
163 "kind": {"meeting"},
164 "status": {"confirmed"},
165 "repeat": {"daily"},
166 })
167
168 req := httptest.NewRequest(http.MethodPost, "/jobs/reminders", nil)
169 req.Header.Set("Authorization", "Bearer test-token")
170 h.ServeHTTP(httptest.NewRecorder(), req)
171
172 // After firing, the rule's next occurrence must be queued —
173 // roughly a day later.
174 due := settledNextDue(t, app)
175 if due.IsZero() {
176 t.Fatal("a repeating reminder fired and queued nothing; the series would remind once and stop")
177 }
178 if until := time.Until(due); until < 20*time.Hour || until > 26*time.Hour {
179 t.Errorf("the requeued reminder is %v away, want about a day", until)
180 }
181}
182
183var _ = calendar.MaxReminderAttempts
184
185func settledNextDue(t *testing.T, app *calendar.App) time.Time {
186 t.Helper()
187 deadline := time.Now().Add(time.Second)
188 for {
189 due := app.NextDue()
190 if due.IsZero() || due.After(time.Now().Add(time.Second)) || time.Now().After(deadline) {
191 return due
192 }
193 time.Sleep(time.Millisecond)
194 }
195}
196