Files
Pete/internal/storage/push_heal_test.go
T
prosolis b07abc1d13 adventure: let a player back out from the web, not only in Matrix
Three verbs to match gogobee's: call off an expedition, turn back out of
somebody else's party, send the pet sitter home. Which one this page
offers is derived here rather than pushed — leadership is already legible
in the party seats and the sitter's standing is already in the babysit
offer, so nothing new crosses the wire.

Two things running it turned up that no test would have. An applied
abandon left "Pull out of the run" sitting under a verdict saying the
expedition was over, so an applied verb now also hides the other verbs it
just made untrue. And a party member was being offered that same button
in the first place, beside the one that actually works — Pete knows from
the seat it just read that gogobee would refuse it, so it is withheld.

Also: heal the Matrix handle onto push rows stored before the column
existed, on its own endpoint rather than through the subscribe upsert,
which resets both watermarks and would have silenced the digest for
anybody who reads the site regularly. And stack the board row below sm —
four flex columns that wrapped to six lines on a phone, pre-existing.
2026-07-24 21:42:59 -07:00

123 lines
4.3 KiB
Go

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)
}
}