internal/store/push_test.go

v1.37.0
gitbay/internal/store/push_test.go history · blame · raw

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}