Skip to content
quarry Artifact [770a40ca4f]
Login

Artifact [770a40ca4f]

Artifact 770a40ca4fd33d7b215a15aa6a3f9ed025ecaffa6137bd3942c9e8b068111f01:


package api

import (
	"context"
	"encoding/base64"
	"fmt"
	"strconv"
	"strings"
	"testing"
)

// The page cursor addresses BOTH sources. A cursor that carried only
// notify's position is the bug this replaces: rows the limit truncated
// off the end of page one were never served on page two, because the
// notify cursor had already moved past them.
func TestInboxCursorRoundTrips(t *testing.T) {
	for _, c := range []inboxCursor{
		{},
		{Notify: "n-abc", Skip: 3, Local: 42, LocalSet: true},
		{NotifyDone: true, LocalDone: true},
		// A notify cursor is opaque and may hold the separators the
		// encoding uses; it must survive them.
		{Notify: "a|b:c=d e", Skip: 1},
	} {
		got := decodeInboxCursor(c.encode())
		if got != c {
			t.Fatalf("round trip: %+v -> %q -> %+v", c, c.encode(), got)
		}
	}
	// The empty cursor is the empty string: a first page carries no
	// `before`, and it must not encode to a token that looks like one.
	if s := (inboxCursor{}).encode(); s != "" {
		t.Fatalf("empty cursor = %q", s)
	}
	// Garbage is a first page, not an error: a stale bookmark should
	// show somebody their inbox, not a failure.
	if got := decodeInboxCursor("not-base64!!"); got != (inboxCursor{}) {
		t.Fatalf("garbage cursor = %+v", got)
	}
	// And the token is opaque — no readable sequence numbers in a URL.
	if strings.Contains((inboxCursor{Local: 42, LocalSet: true}).encode(), "42") {
		t.Fatal("the cursor must be opaque")
	}
}

// An absent switch means "unchanged", never "off". The destructive
// reading — a form that posts `toasts` and not `notify_desktop` turning
// desktop notifications off — is what this replaces. Only the three
// values the field's oneof rule admits can reach it.
func TestSwitchValueDistinguishesAbsentFromOff(t *testing.T) {
	for in, want := range map[string]string{
		"": "", "on": "on", "ON": "on", " on ": "on", "off": "off", "OFF": "off",
	} {
		if got := switchValue(in); got != want {
			t.Errorf("switchValue(%q) = %q, want %q", in, got, want)
		}
	}
}

// A cursor's in-page offset is arithmetic on a number that arrived from
// outside, and it is added to the limit handed to notify. A crafted one
// must never reach the companion.
func TestInboxCursorClampsAHostileSkip(t *testing.T) {
	hostile := base64.RawURLEncoding.EncodeToString([]byte("n:|o:2000000000"))
	if got := decodeInboxCursor(hostile); got != (inboxCursor{}) {
		t.Fatalf("a two-billion-row offset must not decode: %+v", got)
	}
	if got := decodeInboxCursor((inboxCursor{Skip: inboxMaxSkip}).encode()); got.Skip != inboxMaxSkip {
		t.Fatalf("the ceiling itself must still decode: %+v", got)
	}
	if got := decodeInboxCursor((inboxCursor{Skip: inboxMaxSkip + 1}).encode()); got != (inboxCursor{}) {
		t.Fatalf("one past the ceiling must not: %+v", got)
	}
}

// The muted set is stored as a JSON array, so a repository name is a
// whole element rather than whatever survives a split on spaces. The
// old space-separated spelling is still read, because an existing forge
// has rows in it.
func TestMuteListEncodingRoundTripsAndReadsTheOldSpelling(t *testing.T) {
	got := decodeMuteList(encodeMuteList([]string{"beta", "alpha"}))
	if len(got) != 2 || got[0] != "alpha" || got[1] != "beta" {
		t.Fatalf("round trip = %+v (sorted)", got)
	}
	if got := decodeMuteList(""); len(got) != 0 {
		t.Fatalf("empty = %+v", got)
	}
	if got := decodeMuteList("alpha beta"); len(got) != 2 || got[0] != "alpha" {
		t.Fatalf("legacy space-separated = %+v", got)
	}
}

// fixedPageInbox is an InboxPlane whose companion serves a FIXED page
// size and ignores the limit it is asked for — which real notify does
// today (internal/inboxapi returns the whole list). It records every
// limit it was handed, so a test can prove nothing unbounded reaches it.
type fixedPageInbox struct {
	msgs     []InboxMessage
	pageSize int
	limits   []int
	// honourLimit makes it serve exactly what it was asked for, which
	// is the shape notify#301 promises and the one an offset that never
	// rebases grows without bound against.
	honourLimit bool
}

func (f *fixedPageInbox) List(ctx context.Context, account, before string, limit int) ([]InboxMessage, string, error) {
	f.limits = append(f.limits, limit)
	from := 0
	if before != "" {
		n, err := strconv.Atoi(strings.TrimPrefix(before, "p"))
		if err != nil {
			return nil, "", fmt.Errorf("bad cursor %q", before)
		}
		from = n
	}
	if from > len(f.msgs) {
		from = len(f.msgs)
	}
	page := f.msgs[from:]
	size := f.pageSize
	if f.honourLimit && limit > 0 {
		size = limit
	}
	if len(page) > size {
		page = page[:size]
	}
	next := ""
	if from+len(page) < len(f.msgs) {
		next = "p" + strconv.Itoa(from+len(page))
	}
	return page, next, nil
}

func (f *fixedPageInbox) Unread(context.Context, string) (int, error) { return 0, nil }
func (f *fixedPageInbox) Mark(context.Context, string, []string, bool, string) error {
	return nil
}
func (f *fixedPageInbox) Preferences(context.Context, string) (map[string]map[string]bool, bool, error) {
	return nil, true, nil
}
func (f *fixedPageInbox) SetPreferences(context.Context, string, map[string]map[string]bool) error {
	return nil
}
func (f *fixedPageInbox) PushKey(context.Context) (string, error) { return "", nil }
func (f *fixedPageInbox) PushSubscribe(context.Context, string, string, string, string) error {
	return nil
}
func (f *fixedPageInbox) PushUnsubscribe(context.Context, string, string) error { return nil }

// The cursor must REBASE onto the companion's own next page rather than
// growing an offset against a page it has already left behind. Without
// it the offset climbs every page, each fetch asks for limit+offset
// rows, and a long read gets quadratically more expensive while the
// page-boundary paths never run at all.
func TestNotifyWindowRebasesOntoTheNextPage(t *testing.T) {
	var msgs []InboxMessage
	for i := 0; i < 9; i++ {
		msgs = append(msgs, InboxMessage{ID: "m" + strconv.Itoa(i), At: "2026-08-01T00:00:00Z"})
	}
	fake := &fixedPageInbox{msgs: msgs, pageSize: 2}
	d := Deps{Inbox: fake}
	var seen []string
	cur := inboxCursor{}
	for page := 0; page < 10; page++ {
		win, err := buildNotifyWindow(context.Background(), d, "dan", cur, 3)
		if err != nil {
			t.Fatal(err)
		}
		if len(win.rows) == 0 {
			break
		}
		for _, m := range win.rows {
			seen = append(seen, m.ID)
		}
		cur = win.after[len(win.after)-1]
		if cur.Skip > fake.pageSize {
			t.Fatalf("page %d left the offset at %d, past a whole page — it never rebased", page, cur.Skip)
		}
		if cur.NotifyDone {
			break
		}
	}
	if len(seen) != len(msgs) {
		t.Fatalf("walked %d rows (%v), want %d", len(seen), seen, len(msgs))
	}
	for i, id := range seen {
		if id != msgs[i].ID {
			t.Fatalf("row %d = %q, want %q (%v)", i, id, msgs[i].ID, seen)
		}
	}
	// And nothing unbounded ever reached the companion.
	for _, l := range fake.limits {
		if l > 3+inboxMaxSkip {
			t.Fatalf("asked the companion for %d rows", l)
		}
	}
}

// A crafted cursor is refused before it can become a `?limit=`.
func TestHostileCursorNeverReachesTheCompanion(t *testing.T) {
	fake := &fixedPageInbox{msgs: []InboxMessage{{ID: "m0"}}, pageSize: 2}
	d := Deps{Inbox: fake}
	hostile := base64.RawURLEncoding.EncodeToString([]byte("n:|o:2000000000"))
	if _, err := buildNotifyWindow(context.Background(), d, "dan", decodeInboxCursor(hostile), 3); err != nil {
		t.Fatal(err)
	}
	if len(fake.limits) != 1 || fake.limits[0] != 3 {
		t.Fatalf("limits handed to the companion = %v, want just the page limit", fake.limits)
	}
}

// The same walk against a companion that HONOURS the limit. This is
// where a non-rebasing offset shows itself: every page would ask for
// limit+offset rows with the offset climbing, so a long read gets
// quadratically more expensive against a cursor it has already passed.
// Rebased, every request is the page limit exactly.
func TestNotifyWindowDoesNotGrowItsRequests(t *testing.T) {
	var msgs []InboxMessage
	for i := 0; i < 12; i++ {
		msgs = append(msgs, InboxMessage{ID: "m" + strconv.Itoa(i), At: "2026-08-01T00:00:00Z"})
	}
	fake := &fixedPageInbox{msgs: msgs, pageSize: 3, honourLimit: true}
	d := Deps{Inbox: fake}
	var seen []string
	cur := inboxCursor{}
	for page := 0; page < 10; page++ {
		win, err := buildNotifyWindow(context.Background(), d, "dan", cur, 3)
		if err != nil {
			t.Fatal(err)
		}
		if len(win.rows) == 0 {
			break
		}
		for _, m := range win.rows {
			seen = append(seen, m.ID)
		}
		cur = win.after[len(win.after)-1]
		if cur.NotifyDone {
			break
		}
	}
	if len(seen) != len(msgs) {
		t.Fatalf("walked %d rows (%v), want %d", len(seen), seen, len(msgs))
	}
	for _, l := range fake.limits {
		if l != 3 {
			t.Fatalf("requests = %v: an offset that never rebases makes them grow", fake.limits)
		}
	}
}