package main import ( "encoding/json" "sync" "testing" ) // changelogClean mirrors web/static/format.js changelogParagraphs — these cases // are shared with test/format.test.mjs so the two sides can't drift. func TestChangelogClean(t *testing.T) { cases := []struct{ name, in, want string }{ {"drops the 🤖 authorship marker", "🤖 Unread dots now sync across your devices.", "Unread dots now sync across your devices."}, {"drops a person marker", "👨 Hand-written note.", "Hand-written note."}, { "collapses hard wraps but keeps paragraph breaks", "🤖 Your space can now keep itself up\nto date. Admins get a new\nUpdates section.\n\nUpdates take seconds.", "Your space can now keep itself up to date. Admins get a new Updates section.\n\nUpdates take seconds.", }, {"no marker passes through", "Plain note, no marker.", "Plain note, no marker."}, {"blank collapses to empty", " ", ""}, } for _, c := range cases { if got := changelogClean(c.in); got != c.want { t.Errorf("%s: got %q, want %q", c.name, got, c.want) } } } func TestAnnounceChangelog(t *testing.T) { _, s := newTestServer(t) if len(s.changelog) < 2 { t.Fatal("embedded changelog unexpectedly small") } // First run: no watermark → baseline silently, announce nothing. fresh, sent := s.announceChangelog() if fresh != 0 || sent != 0 { t.Fatalf("first run should baseline: fresh=%d sent=%d", fresh, sent) } raw, ok, _ := s.db.getConfig(changelogAnnouncedKey) if !ok { t.Fatal("baseline did not write the watermark") } var ids []string if err := json.Unmarshal([]byte(raw), &ids); err != nil || len(ids) != len(s.changelog) { t.Fatalf("watermark should hold every entry id: %d != %d (%v)", len(ids), len(s.changelog), err) } // Steady state: nothing new → nothing announced. if fresh, _ = s.announceChangelog(); fresh != 0 { t.Fatalf("no new entries but fresh=%d", fresh) } // A "deploy": the stored watermark misses the two newest entries (as if this // binary shipped them). They announce, and the watermark catches up. trimmed, _ := json.Marshal(ids[:len(ids)-2]) if err := s.db.setConfig(changelogAnnouncedKey, string(trimmed)); err != nil { t.Fatal(err) } fresh, sent = s.announceChangelog() if fresh != 2 { t.Fatalf("expected 2 fresh entries, got %d", fresh) } if sent != 0 { // no push subscriptions in the test DB t.Fatalf("expected 0 sends without subscriptions, got %d", sent) } raw, _, _ = s.db.getConfig(changelogAnnouncedKey) var after []string _ = json.Unmarshal([]byte(raw), &after) if len(after) != len(s.changelog) { t.Fatalf("watermark should catch up: %d != %d", len(after), len(s.changelog)) } // A corrupt watermark re-baselines rather than announcing the backlog. _ = s.db.setConfig(changelogAnnouncedKey, "not-json") if fresh, _ = s.announceChangelog(); fresh != 0 { t.Fatalf("corrupt watermark should re-baseline, got fresh=%d", fresh) } } func TestAdminUserIDs(t *testing.T) { _, s := newTestServer(t) a, _ := s.db.createUser("Ada") b, _ := s.db.createUser("Bob") if err := s.db.setUserRole(a, "admin"); err != nil { t.Fatal(err) } admins, err := s.db.adminUserIDs() if err != nil { t.Fatal(err) } if len(admins) != 1 || admins[0] != a { t.Fatalf("want [%s], got %v", a, admins) } _ = b // stays a member — must not appear } // stubAPNs records sendAlert calls so apnsAnnounce's admin filtering is // observable without any network. type stubAPNs struct { mu sync.Mutex alerts []string // "apnsToken|title|body" } func (s *stubAPNs) send(string, bool, string, string, int64, int64) error { return nil } func (s *stubAPNs) sendAlert(deviceToken string, _ bool, title, body string) error { s.mu.Lock() defer s.mu.Unlock() s.alerts = append(s.alerts, deviceToken+"|"+title+"|"+body) return nil } func TestAPNsAnnounceReachesOnlyAdmins(t *testing.T) { _, s := newTestServer(t) stub := &stubAPNs{} s.apns = stub adminTok := mintDeviceToken(t, s.db, "Ada") memberTok := mintDeviceToken(t, s.db, "Bob") adminUID, _ := s.db.deviceTokenUser(adminTok) if err := s.db.setUserRole(adminUID, "admin"); err != nil { t.Fatal(err) } if err := s.db.addAPNsToken(APNsToken{APNsToken: "apns-ada", Token: adminTok}); err != nil { t.Fatal(err) } if err := s.db.addAPNsToken(APNsToken{APNsToken: "apns-bob", Token: memberTok}); err != nil { t.Fatal(err) } sent := s.apnsAnnounce("What's new", "Notes.", map[string]bool{adminUID: true}) if sent != 1 { t.Fatalf("sent=%d, want 1 (admin only)", sent) } if len(stub.alerts) != 1 || stub.alerts[0] != "apns-ada|What's new|Notes." { t.Fatalf("unexpected alerts: %v", stub.alerts) } }