internal/store/push_test.go
314 lines · 9199 bytes
1package store
2
3import (
4 "testing"
5 "time"
6)
7
8func pushFixture(t *testing.T) *Store {
9 t.Helper()
10 s := open(t)
11 if err := s.MigrateUp(); err != nil {
12 t.Fatal(err)
13 }
14 return s
15}
16
17func TestPushDevices(t *testing.T) {
18 s := pushFixture(t)
19 uid, err := s.CreateUser("alice", false)
20 if err != nil {
21 t.Fatal(err)
22 }
23
24 firstID, err := s.AddPushDevice(uid, "tok-a", "iphone")
25 if err != nil {
26 t.Fatalf("AddPushDevice: %v", err)
27 }
28 devices, err := s.PushDevices(uid)
29 if err != nil {
30 t.Fatalf("PushDevices: %v", err)
31 }
32 if len(devices) != 1 || devices[0].Token != "tok-a" || devices[0].Label != "iphone" {
33 t.Fatalf("got %+v", devices)
34 }
35 if firstID != devices[0].ID {
36 t.Fatalf("AddPushDevice returned %d, row id is %d", firstID, devices[0].ID)
37 }
38
39 // Apple reuses tokens: re-registering updates the label and the owner
40 // rather than erroring, so a reinstall under another account works. The
41 // returned id must be the existing row's, not an unrelated rowid left
42 // over from SQLite's last real INSERT (the DO UPDATE arm does not
43 // advance last_insert_rowid()).
44 bob, err := s.CreateUser("bob", false)
45 if err != nil {
46 t.Fatal(err)
47 }
48 reregID, err := s.AddPushDevice(bob, "tok-a", "ipad")
49 if err != nil {
50 t.Fatalf("re-register: %v", err)
51 }
52 if d, _ := s.PushDevices(uid); len(d) != 0 {
53 t.Fatalf("token still owned by alice: %+v", d)
54 }
55 d, _ := s.PushDevices(bob)
56 if len(d) != 1 || d[0].Label != "ipad" {
57 t.Fatalf("got %+v", d)
58 }
59 if reregID != d[0].ID {
60 t.Fatalf("re-register returned %d, existing row id is %d", reregID, d[0].ID)
61 }
62 if reregID != firstID {
63 t.Fatalf("re-register returned %d, want the reused row's original id %d", reregID, firstID)
64 }
65
66 // Removal is scoped to the owner: alice cannot remove bob's device.
67 if err := s.RemovePushDevice(uid, d[0].ID); err != ErrNotFound {
68 t.Fatalf("cross-account remove: got %v, want ErrNotFound", err)
69 }
70 if err := s.RemovePushDevice(bob, d[0].ID); err != nil {
71 t.Fatalf("RemovePushDevice: %v", err)
72 }
73 if d, _ := s.PushDevices(bob); len(d) != 0 {
74 t.Fatalf("device survived removal: %+v", d)
75 }
76}
77
78func TestPushEnabledDefaultsOn(t *testing.T) {
79 s := pushFixture(t)
80 uid, err := s.CreateUser("alice", false)
81 if err != nil {
82 t.Fatal(err)
83 }
84 on, err := s.PushEnabled(uid)
85 if err != nil {
86 t.Fatalf("PushEnabled: %v", err)
87 }
88 if !on {
89 t.Fatal("notify_push should default on")
90 }
91 if err := s.SetPushEnabled(uid, false); err != nil {
92 t.Fatalf("SetPushEnabled: %v", err)
93 }
94 if on, _ := s.PushEnabled(uid); on {
95 t.Fatal("SetPushEnabled(false) did not stick")
96 }
97}
98
99func TestEnqueuePush(t *testing.T) {
100 s := pushFixture(t)
101 uid, err := s.CreateUser("alice", false)
102 if err != nil {
103 t.Fatal(err)
104 }
105 s.AddPushDevice(uid, "tok-a", "iphone")
106 s.AddPushDevice(uid, "tok-b", "ipad")
107
108 // One row per device, so a retry to the phone does not resend to the
109 // iPad.
110 if err := s.EnqueuePush(uid, "krz/gitbay", "cmc opened issue #12", "krz/gitbay/issues/12"); err != nil {
111 t.Fatalf("EnqueuePush: %v", err)
112 }
113 due, err := s.DuePush(20)
114 if err != nil {
115 t.Fatalf("DuePush: %v", err)
116 }
117 if len(due) != 2 {
118 t.Fatalf("want a row per device, got %d", len(due))
119 }
120 if due[0].Token == "" || due[0].Body != "cmc opened issue #12" {
121 t.Fatalf("got %+v", due[0])
122 }
123
124 // Sent rows stop being due.
125 if err := s.MarkPushSent(due[0].ID); err != nil {
126 t.Fatalf("MarkPushSent: %v", err)
127 }
128 if due, _ := s.DuePush(20); len(due) != 1 {
129 t.Fatalf("sent row still due")
130 }
131
132 // A failure with a next attempt in the future is not due yet.
133 next := time.Now().Add(time.Hour)
134 if err := s.MarkPushFailed(due[1].ID, "503", &next); err != nil {
135 t.Fatalf("MarkPushFailed: %v", err)
136 }
137 if due, _ := s.DuePush(20); len(due) != 0 {
138 t.Fatalf("backed-off row is due too early")
139 }
140}
141
142func TestEnqueuePushRespectsSettingAndDevices(t *testing.T) {
143 s := pushFixture(t)
144 uid, err := s.CreateUser("alice", false)
145 if err != nil {
146 t.Fatal(err)
147 }
148
149 // No devices: nothing queued, no error.
150 if err := s.EnqueuePush(uid, "t", "b", "p"); err != nil {
151 t.Fatalf("EnqueuePush with no devices: %v", err)
152 }
153 if due, _ := s.DuePush(20); len(due) != 0 {
154 t.Fatalf("queued for an account with no devices")
155 }
156
157 // Setting off: nothing queued.
158 s.AddPushDevice(uid, "tok-a", "iphone")
159 s.SetPushEnabled(uid, false)
160 if err := s.EnqueuePush(uid, "t", "b", "p"); err != nil {
161 t.Fatalf("EnqueuePush with push off: %v", err)
162 }
163 if due, _ := s.DuePush(20); len(due) != 0 {
164 t.Fatalf("queued with notify_push off")
165 }
166}
167
168// A token changing hands takes its undelivered queue with it. The row id
169// survives the upsert, so anything queued for the previous owner would
170// otherwise be delivered to a phone that now belongs to someone else —
171// and an alert carries the repository name and item number in full. The
172// iOS app calls device add on every sign-in, which is exactly when
173// ownership changes.
174func TestAddPushDeviceDropsThePreviousOwnersQueue(t *testing.T) {
175 s := pushFixture(t)
176 alice, err := s.CreateUser("alice", false)
177 if err != nil {
178 t.Fatal(err)
179 }
180 bob, err := s.CreateUser("bob", false)
181 if err != nil {
182 t.Fatal(err)
183 }
184 id, err := s.AddPushDevice(alice, "tok-a", "iphone")
185 if err != nil {
186 t.Fatal(err)
187 }
188 if err := s.EnqueuePush(alice, "alice/secret", "alice opened issue #1", "alice/secret/issues/1"); err != nil {
189 t.Fatal(err)
190 }
191 due, err := s.DuePush(20)
192 if err != nil || len(due) != 1 {
193 t.Fatalf("DuePush: %v %+v", err, due)
194 }
195 // A second row, already sent: history, not a pending delivery.
196 if err := s.EnqueuePush(alice, "alice/secret", "alice closed issue #1", "alice/secret/issues/1"); err != nil {
197 t.Fatal(err)
198 }
199 sent, _ := s.DuePush(20)
200 if err := s.MarkPushSent(sent[len(sent)-1].ID); err != nil {
201 t.Fatal(err)
202 }
203
204 if _, err := s.AddPushDevice(bob, "tok-a", "iphone"); err != nil {
205 t.Fatalf("re-register: %v", err)
206 }
207 if due, _ := s.DuePush(20); len(due) != 0 {
208 t.Fatalf("alice's pending push survived the handover: %+v", due)
209 }
210 var kept int
211 s.DB.QueryRow("SELECT COUNT(*) FROM push_queue WHERE device_id = ? AND sent_at IS NOT NULL", id).Scan(&kept)
212 if kept != 1 {
213 t.Fatalf("delivered rows deleted too: %d remain", kept)
214 }
215
216 // Re-registering to the same owner leaves the queue alone: the app
217 // calls device add on every launch.
218 if err := s.EnqueuePush(bob, "bob/app", "bob opened issue #2", "bob/app/issues/2"); err != nil {
219 t.Fatal(err)
220 }
221 if _, err := s.AddPushDevice(bob, "tok-a", "iphone"); err != nil {
222 t.Fatal(err)
223 }
224 if due, _ := s.DuePush(20); len(due) != 1 {
225 t.Fatalf("re-registering to the same owner dropped its own queue: %+v", due)
226 }
227}
228
229func TestDeletePushDeviceByTokenTakesItsQueue(t *testing.T) {
230 s := pushFixture(t)
231 uid, err := s.CreateUser("alice", false)
232 if err != nil {
233 t.Fatal(err)
234 }
235 s.AddPushDevice(uid, "tok-a", "iphone")
236 s.EnqueuePush(uid, "t", "b", "p")
237
238 if err := s.DeletePushDeviceByToken("tok-a"); err != nil {
239 t.Fatalf("DeletePushDeviceByToken: %v", err)
240 }
241 if d, _ := s.PushDevices(uid); len(d) != 0 {
242 t.Fatalf("device survived")
243 }
244 // push_queue.device_id is ON DELETE CASCADE, so the queued rows go
245 // with it rather than being retried at a dead token forever.
246 if due, _ := s.DuePush(20); len(due) != 0 {
247 t.Fatalf("queued rows outlived their device")
248 }
249}
250
251// A queued push carries the recipient's username, so the alert can name
252// the account it belongs to. A device token is one install, and one
253// install registers against every account signed in on it; without the
254// username the client cannot tell which of them a push is for.
255func TestDuePushCarriesTheUsername(t *testing.T) {
256 s := pushFixture(t)
257 uid, err := s.CreateUser("alice", false)
258 if err != nil {
259 t.Fatal(err)
260 }
261 if _, err := s.AddPushDevice(uid, "tok-a", "iphone"); err != nil {
262 t.Fatal(err)
263 }
264 if err := s.EnqueuePush(uid, "alice/app", "bob opened issue #1", "alice/app/issues/1"); err != nil {
265 t.Fatal(err)
266 }
267 due, err := s.DuePush(20)
268 if err != nil {
269 t.Fatal(err)
270 }
271 if len(due) != 1 {
272 t.Fatalf("want one queued push, got %d", len(due))
273 }
274 if due[0].Username != "alice" {
275 t.Fatalf("Username = %q, want alice", due[0].Username)
276 }
277}
278
279// The queue row carries the recipient's unread count, so the alert can
280// badge the app icon. Counted at send rather than at enqueue: an inbox
281// cleared in the seconds before delivery is reflected.
282func TestDuePushCarriesTheUnreadCount(t *testing.T) {
283 s := pushFixture(t)
284 uid, err := s.CreateUser("alice", false)
285 if err != nil {
286 t.Fatal(err)
287 }
288 repoID, err := s.CreateRepo("user", uid, "app", "public")
289 if err != nil {
290 t.Fatal(err)
291 }
292 if _, err := s.AddPushDevice(uid, "tok-a", "iphone"); err != nil {
293 t.Fatal(err)
294 }
295 // Two unread inbox rows, then a queued push.
296 for i := 0; i < 2; i++ {
297 if err := s.AddNotice(uid, repoID, "issue", "bob", "opened issue #1", "alice/app/issues/1"); err != nil {
298 t.Fatal(err)
299 }
300 }
301 if err := s.EnqueuePush(uid, "alice/app", "bob opened issue #1", "alice/app/issues/1"); err != nil {
302 t.Fatal(err)
303 }
304 due, err := s.DuePush(20)
305 if err != nil {
306 t.Fatal(err)
307 }
308 if len(due) != 1 {
309 t.Fatalf("want one queued push, got %d", len(due))
310 }
311 if due[0].Badge != 2 {
312 t.Fatalf("Badge = %d, want 2", due[0].Badge)
313 }
314}