Commit c9cf3e5403

c9cf3e5403ade1938ac68460d6238a8409872528

parent: 2979e93075

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-20 09:20 UTC

store: fix AddPushDevice id on token-reuse path

LastInsertId is left unchanged by SQLite when ON CONFLICT DO UPDATE
fires instead of the INSERT, so a re-registered token could return an
unrelated row's id. Read the id back by token instead. Strengthen the
test to assert on the returned id across both the fresh-registration
and re-registration paths.

Ref #89
internal/store/push.go +7 −3
@@ -19,16 +19,20 @@ type PushDevice struct {
1919
2020// AddPushDevice registers a token to an account. A token already present
2121// changes hands rather than erroring: Apple reuses tokens, and a reinstall
22// hands the same one to whichever account signs in next.
22// hands the same one to whichever account signs in next. The id is read
23// back by token rather than taken from LastInsertId, which SQLite leaves
24// unchanged when the DO UPDATE arm fires instead of the INSERT.
2325func (s *Store) AddPushDevice(userID int64, token, label string) (int64, error) {
24 res, err := s.DB.Exec(`
26 _, err := s.DB.Exec(`
2527 INSERT INTO push_devices (user_id, token, label) VALUES (?, ?, ?)
2628 ON CONFLICT(token) DO UPDATE SET user_id = excluded.user_id, label = excluded.label`,
2729 userID, token, label)
2830 if err != nil {
2931 return 0, err
3032 }
31 return res.LastInsertId()
33 var id int64
34 err = s.DB.QueryRow("SELECT id FROM push_devices WHERE token = ?", token).Scan(&id)
35 return id, err
3236}
3337
3438func (s *Store) PushDevices(userID int64) ([]PushDevice, error) {
internal/store/push_test.go +17 −3
@@ -18,7 +18,8 @@ func TestPushDevices(t *testing.T) {
1818 t.Fatal(err)
1919 }
2020
21 if _, err := s.AddPushDevice(uid, "tok-a", "iphone"); err != nil {
21 firstID, err := s.AddPushDevice(uid, "tok-a", "iphone")
22 if err != nil {
2223 t.Fatalf("AddPushDevice: %v", err)
2324 }
2425 devices, err := s.PushDevices(uid)
@@ -28,14 +29,21 @@ func TestPushDevices(t *testing.T) {
2829 if len(devices) != 1 || devices[0].Token != "tok-a" || devices[0].Label != "iphone" {
2930 t.Fatalf("got %+v", devices)
3031 }
32 if firstID != devices[0].ID {
33 t.Fatalf("AddPushDevice returned %d, row id is %d", firstID, devices[0].ID)
34 }
3135
3236 // Apple reuses tokens: re-registering updates the label and the owner
33 // rather than erroring, so a reinstall under another account works.
37 // rather than erroring, so a reinstall under another account works. The
38 // returned id must be the existing row's, not an unrelated rowid left
39 // over from SQLite's last real INSERT (the DO UPDATE arm does not
40 // advance last_insert_rowid()).
3441 bob, err := s.CreateUser("bob", false)
3542 if err != nil {
3643 t.Fatal(err)
3744 }
38 if _, err := s.AddPushDevice(bob, "tok-a", "ipad"); err != nil {
45 reregID, err := s.AddPushDevice(bob, "tok-a", "ipad")
46 if err != nil {
3947 t.Fatalf("re-register: %v", err)
4048 }
4149 if d, _ := s.PushDevices(uid); len(d) != 0 {
@@ -45,6 +53,12 @@ func TestPushDevices(t *testing.T) {
4553 if len(d) != 1 || d[0].Label != "ipad" {
4654 t.Fatalf("got %+v", d)
4755 }
56 if reregID != d[0].ID {
57 t.Fatalf("re-register returned %d, existing row id is %d", reregID, d[0].ID)
58 }
59 if reregID != firstID {
60 t.Fatalf("re-register returned %d, want the reused row's original id %d", reregID, firstID)
61 }
4862
4963 // Removal is scoped to the owner: alice cannot remove bob's device.
5064 if err := s.RemovePushDevice(uid, d[0].ID); err != ErrNotFound {