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 "context"
5 "errors"
6 "io"
7 "log/slog"
8 "net/http"
9 "net/http/httptest"
10 "net/url"
11 "path/filepath"
12 "strings"
13 "testing"
14 "time"
15
16 calendar "amadan.net/oficina/calendar/internal/calendar"
17 "amadan.net/rastrillo/rastrillo/carlos"
18)
19
20func TestPersistedWorkRegistersAtBootBeforeAnyRequest(t *testing.T) {
21 control := controlForTest(t)
22 dir := t.TempDir()
23 app := calendar.NewForTest(t, dir)
24 due := time.Now().Add(-time.Minute).Unix()
25 if _, err := app.DBForTest().Exec(`INSERT INTO reminder_queues (reminder_id, occurrence_key, due_at, attempts, late_by_sec) VALUES (1, 'boot', ?, 0, 0)`, due); err != nil {
26 t.Fatal(err)
27 }
28 if err := app.Close(); err != nil {
29 t.Fatal(err)
30 }
31 app = calendar.NewForTest(t, dir)
32 at, _ := control.snapshot()
33 if at.Unix() < due || at.After(time.Now()) || at.IsZero() {
34 t.Fatalf("boot did not register persisted work: got %v, want %d", at, due)
35 }
36 if body, _ := app.LastMailForTest(); body != "" {
37 t.Fatal("startup delivered mail outside a held request")
38 }
39}
40
41func TestScheduledHTTPDeliversWithoutAVisitorAndRearmsRecurrence(t *testing.T) {
42 control := controlForTest(t)
43 app, session := signedIn(t, "someone@example.com")
44 start := time.Now().UTC().Add(2 * time.Minute)
45 createEvent(t, app.Handler(), session, url.Values{
46 "title": {"Scheduled standup"}, "start": {start.Format("2006-01-02T15:04")}, "end": {start.Add(15 * time.Minute).Format("2006-01-02T15:04")},
47 "kind": {"meeting"}, "status": {"confirmed"}, "repeat": {"daily"},
48 })
49 expected := app.StoreForTest().NextDueAt(context.Background())
50 control.waitArmed(t)
51 before, _ := control.snapshot()
52 if before.Before(expected) || before.After(time.Now()) {
53 t.Fatalf("event POST failed to arm its overdue reminder: %v, expected %v", before, expected)
54 }
55 req := httptest.NewRequest(http.MethodPost, "/jobs/reminders", nil)
56 req.Header.Set("Authorization", "Bearer test-token")
57 req.Header.Set("X-Carlos-Schedule", "calendar-next-due")
58 rec := httptest.NewRecorder()
59 app.Handler().ServeHTTP(rec, req)
60 if rec.Code != http.StatusNoContent {
61 t.Fatalf("scheduled request = %d: %s", rec.Code, rec.Body.String())
62 }
63 body, _ := app.LastMailForTest()
64 if !strings.Contains(body, "Scheduled standup") {
65 t.Fatal("scheduled HTTP did not deliver the reminder")
66 }
67 next, _ := control.snapshot()
68 expected = app.StoreForTest().NextDueAt(context.Background())
69 if !next.Equal(expected) || !next.After(time.Now()) {
70 t.Fatalf("next recurrence not armed before completion: %v, expected %v", next, expected)
71 }
72}
73
74func TestPublicBookingPostRegistersItsHoldAndMail(t *testing.T) {
75 control := controlForTest(t)
76 app, _, path, _ := publishedPage(t)
77 _, starts := futureDaySlots(t, app.Handler(), path)
78 strangerPost(t, app.Handler(), path, holdForm(starts[0], "A", "a@example.com"))
79 expected := app.StoreForTest().NextDueAt(context.Background())
80 control.waitArmed(t)
81 at, _ := control.snapshot()
82 if expected.IsZero() || !at.Equal(expected) {
83 t.Fatalf("public booking did not register scheduled work: got %v, expected %v", at, expected)
84 }
85}
86
87func TestTickRegistrationFailureReturnsRetryAndRecoversWithoutAVisitor(t *testing.T) {
88 control := controlForTest(t)
89 app := calendar.NewForTest(t, t.TempDir())
90 due := time.Now().Add(time.Hour).Unix()
91 if _, err := app.DBForTest().Exec(`INSERT INTO reminder_queues (reminder_id, occurrence_key, due_at, attempts, late_by_sec) VALUES (1, 'retry', ?, 0, 0)`, due); err != nil {
92 t.Fatal(err)
93 }
94 control.mu.Lock()
95 control.fail = true
96 control.mu.Unlock()
97 rec := tick(t, app.Handler())
98 if rec.Code != http.StatusInternalServerError || strings.TrimSpace(rec.Body.String()) != "retry" {
99 t.Fatalf("registration failure acknowledged: %d %s", rec.Code, rec.Body.String())
100 }
101 control.mu.Lock()
102 control.fail = false
103 control.mu.Unlock()
104 deadline := time.After(4 * time.Second)
105 for {
106 at, _ := control.snapshot()
107 if at.Unix() == due {
108 break
109 }
110 select {
111 case <-deadline:
112 t.Fatal("registration did not recover without a visitor")
113 case <-time.After(10 * time.Millisecond):
114 }
115 }
116}
117
118func TestDatabaseFailureCannotCancelTheTimer(t *testing.T) {
119 control := controlForTest(t)
120 app := calendar.NewForTest(t, t.TempDir())
121 due := time.Now().Add(time.Hour).Unix()
122 if _, err := app.DBForTest().Exec(`INSERT INTO reminder_queues (reminder_id, occurrence_key, due_at, attempts, late_by_sec) VALUES (1, 'failure', ?, 0, 0)`, due); err != nil {
123 t.Fatal(err)
124 }
125 if got := app.NextDue(); got.Unix() != due {
126 t.Fatal(got)
127 }
128 if _, err := app.DBForTest().Exec(`DROP TABLE booking_mails`); err != nil {
129 t.Fatal(err)
130 }
131 got := app.NextDue()
132 at, _ := control.snapshot()
133 if got.IsZero() || got.After(time.Now()) || at.Unix() != due {
134 t.Fatalf("failed due query erased wake: probe %v, timer %v", got, at)
135 }
136}
137
138func TestProductionRefusesMissingControlSocket(t *testing.T) {
139 t.Setenv("CARLOS_CONTROL_SOCKET", "")
140 _, err := calendar.New(calendar.Config{DBPath: filepath.Join(t.TempDir(), "calendar.db"), Env: "production", Origin: calendar.TestOrigin,
141 InstanceKey: "01234567890123456789012345678901", DEK: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", AllowedAddresses: "person@example.com",
142 Mailer: &pickyMailer{}, Logger: slog.New(slog.NewTextHandler(io.Discard, nil))})
143 if !errors.Is(err, carlos.ErrNotOnCarlos) {
144 t.Fatalf("production startup error = %v, want missing platform scheduling", err)
145 }
146}
147
148func TestScheduledHTTPRearmsAFullSweepBacklog(t *testing.T) {
149 control := controlForTest(t)
150 app := calendar.NewForTest(t, t.TempDir())
151 due := time.Now().Add(-time.Minute).Unix()
152 _, err := app.DBForTest().Exec(`WITH RECURSIVE rows(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM rows WHERE n<501)
153 INSERT INTO bookings (page_id,event_id,token_hash,state,booker_address_fp,booker_zone,hold_expires_at,cancelled_by,ip_fp,ip_forget_at,reschedules,created_at,updated_at)
154 SELECT 0,0,CAST(n AS TEXT),'expired','','UTC',0,'','test',?,0,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP FROM rows`, due)
155 if err != nil {
156 t.Fatal(err)
157 }
158 if rec := tick(t, app.Handler()); rec.Code != http.StatusNoContent {
159 t.Fatalf("tick = %d", rec.Code)
160 }
161 var remaining int
162 if err := app.DBForTest().QueryRow(`SELECT COUNT(*) FROM bookings WHERE ip_fp IS NOT NULL`).Scan(&remaining); err != nil {
163 t.Fatal(err)
164 }
165 at, _ := control.snapshot()
166 if remaining != 1 || at.Unix() < due || at.After(time.Now()) || at.IsZero() {
167 t.Fatalf("backlog = %d, timer = %v", remaining, at)
168 }
169 if rec := tick(t, app.Handler()); rec.Code != http.StatusNoContent {
170 t.Fatalf("second tick = %d", rec.Code)
171 }
172 if at, _ := control.snapshot(); !at.IsZero() {
173 t.Fatalf("finished backlog remains armed at %v", at)
174 }
175}
176
177func TestScheduledHTTPKeepsRetryAndLeaseDeadlines(t *testing.T) {
178 control := controlForTest(t)
179 app := calendar.NewForTest(t, t.TempDir())
180 now := time.Now().Unix()
181 _, err := app.DBForTest().Exec(`INSERT INTO reminder_queues (reminder_id,occurrence_key,due_at,claimed_at,retry_at,attempts,late_by_sec) VALUES (1,'leased',?,?,?,0,0)`, now-60, now, now+600)
182 if err != nil {
183 t.Fatal(err)
184 }
185 if rec := tick(t, app.Handler()); rec.Code != http.StatusNoContent {
186 t.Fatalf("tick = %d", rec.Code)
187 }
188 if at, _ := control.snapshot(); at.Unix() != now+600 {
189 t.Fatalf("retry time lost: %v", at)
190 }
191 if _, err := app.DBForTest().Exec(`UPDATE reminder_queues SET retry_at = NULL`); err != nil {
192 t.Fatal(err)
193 }
194 if rec := tick(t, app.Handler()); rec.Code != http.StatusNoContent {
195 t.Fatalf("second tick = %d", rec.Code)
196 }
197 if at, _ := control.snapshot(); at.Unix() != now+300 {
198 t.Fatalf("lease time lost: %v", at)
199 }
200}
201