package storage import "testing" // W9: healing the Matrix handle onto a subscription stored before the column // existed. Those rows can never match an owner-scoped adventure alert, and their // owners have no way to notice — the browser only re-subscribes on a click. // // The trap this exists to avoid is worth stating plainly, because the obvious // implementation is a one-liner that reuses AddPushSubscription with the same // arguments: that upsert resets BOTH watermarks to now. The heal runs from the // page, so it would fire far more often than a subscribe does, and every run // would push the digest's own "last told them about" stamp forward — a reader who // visits daily would silently stop receiving digests and adventure alerts alike, // from a change made to fix notifications. func findSub(t *testing.T, endpoint string) PushSubscription { t.Helper() subs, err := ListPushSubscriptions() if err != nil { t.Fatal(err) } for _, s := range subs { if s.Endpoint == endpoint { return s } } t.Fatalf("no subscription for %q", endpoint) return PushSubscription{} } func TestHealFillsAnEmptyLocalpartAndNothingElse(t *testing.T) { setupTestDB(t) const ep = "https://push.example/ep-old" // A row as a pre-W6 build left it: no Matrix handle. if err := AddPushSubscription("sub-1", "", ep, "p256", "auth"); err != nil { t.Fatal(err) } before := findSub(t, ep) if before.Localpart != "" { t.Fatalf("seed carries a localpart %q; the test isn't testing anything", before.Localpart) } // Move both watermarks off "now" so a reset would be visible rather than // coincidentally equal. if err := TouchPushSubscription(ep, 1000); err != nil { t.Fatal(err) } if err := TouchAdvPushSubscription(ep, 2000); err != nil { t.Fatal(err) } if err := HealPushSubscriptionLocalpart("sub-1", ep, "josie"); err != nil { t.Fatal(err) } got := findSub(t, ep) if got.Localpart != "josie" { t.Fatalf("localpart = %q, want josie", got.Localpart) } // The whole point: the clocks did not move. if got.LastNotifiedAt != 1000 { t.Fatalf("digest watermark = %d, want 1000 — a heal that resets it silences the digest", got.LastNotifiedAt) } if got.LastAdvNotifiedAt != 2000 { t.Fatalf("adventure watermark = %d, want 2000 — a heal that resets it silences the alerts", got.LastAdvNotifiedAt) } if got.P256dh != "p256" || got.Auth != "auth" { t.Fatal("the heal rewrote the encryption keys; it must touch one column") } } func TestHealNeverOverwritesAKnownHandle(t *testing.T) { setupTestDB(t) const ep = "https://push.example/ep-good" if err := AddPushSubscription("sub-1", "josie", ep, "p256", "auth"); err != nil { t.Fatal(err) } // A later session whose username resolved differently must not be able to // rewrite a handle that is already good — the heal is for empty rows only, so // it is a no-op the moment one has succeeded. if err := HealPushSubscriptionLocalpart("sub-1", ep, "someone-else"); err != nil { t.Fatal(err) } if got := findSub(t, ep); got.Localpart != "josie" { t.Fatalf("localpart = %q, want the original josie", got.Localpart) } } func TestHealIsScopedToTheCaller(t *testing.T) { setupTestDB(t) const ep = "https://push.example/ep-theirs" if err := AddPushSubscription("sub-owner", "", ep, "p256", "auth"); err != nil { t.Fatal(err) } // Somebody else presenting the endpoint string writes nothing. Endpoints are // not secrets and the client hands one straight up, so this is the guard that // stops a stranger attaching their own handle to another account's device. if err := HealPushSubscriptionLocalpart("sub-attacker", ep, "attacker"); err != nil { t.Fatal(err) } if got := findSub(t, ep); got.Localpart != "" { t.Fatalf("localpart = %q; another account healed a row it does not own", got.Localpart) } } func TestHealWithNoHandleIsANoOp(t *testing.T) { setupTestDB(t) const ep = "https://push.example/ep-nouser" if err := AddPushSubscription("sub-1", "", ep, "p256", "auth"); err != nil { t.Fatal(err) } // A session minted before the game economy existed carries no username. There // is nothing to heal with, and writing "" over "" is not worth a statement. if err := HealPushSubscriptionLocalpart("sub-1", ep, ""); err != nil { t.Fatal(err) } if got := findSub(t, ep); got.Localpart != "" { t.Fatalf("localpart = %q, want empty", got.Localpart) } }