Skip to content
quarry Artifact [55d6a9835f]
Login

Artifact [55d6a9835f]

Artifact 55d6a9835f9ea5147d3b176448aa110d4ac152dd55c141755fdda96219d81657:


package web

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"net/url"
	"path/filepath"
	"strings"
	"sync"
	"testing"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/index"
	"craftdesign.group/quarry/internal/ops"
)

// memPrefs is the forge-local preference store. It answers both seams —
// the ops' AccountPrefStore and the web layer's Prefs — because in a
// running forge they are one table, and a test that splits them would
// prove the page against a store the stream cannot see.
type memPrefs struct {
	mu sync.Mutex
	v  map[string]string
}

func newMemPrefs() *memPrefs { return &memPrefs{v: map[string]string{}} }

func (m *memPrefs) AccountPref(account, key string) string {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.v[account+"/"+key]
}

func (m *memPrefs) SetAccountPref(account, key, value string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.v[account+"/"+key] = value
	return nil
}

func (m *memPrefs) Pref(account, key string) string { return m.AccountPref(account, key) }
func (m *memPrefs) SetPref(account, key, value string) error {
	return m.SetAccountPref(account, key, value)
}

// memDevices is the push-device store. It hashes the endpoint the way
// the real one does, so the page never sees an endpoint it could post
// back — which is the constraint the control is designed around.
type memDevices struct {
	mu      sync.Mutex
	rows    map[string][]api.PushDevice
	pending map[string]string // hash -> endpoint
}

func newMemDevices() *memDevices {
	return &memDevices{rows: map[string][]api.PushDevice{}, pending: map[string]string{}}
}

func endpointHash(endpoint string) string {
	sum := sha256.Sum256([]byte(endpoint))
	return hex.EncodeToString(sum[:])
}

func (m *memDevices) AddPushDevice(account, endpoint, ua string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	h := endpointHash(endpoint)
	for _, row := range m.rows[account] {
		if row.EndpointHash == h {
			return nil
		}
	}
	m.rows[account] = append(m.rows[account], api.PushDevice{
		EndpointHash: h, UA: ua, At: "2026-08-29T10:00:00Z"})
	return nil
}

func (m *memDevices) DeletePushDevice(account, endpoint string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	h := endpointHash(endpoint)
	kept := m.rows[account][:0]
	for _, row := range m.rows[account] {
		if row.EndpointHash != h {
			kept = append(kept, row)
		}
	}
	m.rows[account] = kept
	return nil
}

func (m *memDevices) MarkPushDevicePendingDelete(account, endpoint string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	h := endpointHash(endpoint)
	m.pending[h] = endpoint
	for i, row := range m.rows[account] {
		if row.EndpointHash == h {
			m.rows[account][i].PendingDelete = true
		}
	}
	return nil
}

func (m *memDevices) PendingPushDeletes(account string) ([]string, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	out := []string{}
	for _, row := range m.rows[account] {
		if row.PendingDelete {
			out = append(out, m.pending[row.EndpointHash])
		}
	}
	return out, nil
}

func (m *memDevices) PushDevices(account string) ([]api.PushDevice, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	return append([]api.PushDevice(nil), m.rows[account]...), nil
}

// fakeNotify is the notification companion as the ops need it.
type fakeNotify struct {
	windows    map[string]api.DigestWindow
	setWindows map[string]*api.DigestWindow
	mu         sync.Mutex
	markErr    error
	key        string
	stored     map[string]map[string]bool
	resolved   bool
	msgs       []api.InboxMessage
	subs       []string
	unsubbed   []string
	subP256dh  string
}

func newFakeNotify() *fakeNotify {
	return &fakeNotify{key: "BPublicKeyBase64Url", stored: map[string]map[string]bool{}, resolved: true}
}

func (f *fakeNotify) Unread(context.Context, string) (int, error) { return 0, nil }
func (f *fakeNotify) List(_ context.Context, _, before string, _ int) ([]api.InboxMessage, string, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if before != "" || len(f.msgs) == 0 {
		return nil, "", nil
	}
	return append([]api.InboxMessage(nil), f.msgs...), "", nil
}
func (f *fakeNotify) Mark(context.Context, string, []string, bool, string) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.markErr
}

func (f *fakeNotify) Preferences(context.Context, string) (map[string]map[string]bool, bool, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	out := map[string]map[string]bool{}
	for wf, row := range f.stored {
		copied := map[string]bool{}
		for k, v := range row {
			copied[k] = v
		}
		out[wf] = copied
	}
	return out, f.resolved, nil
}

func (f *fakeNotify) DigestWindows(_ context.Context, _ string) (map[string]api.DigestWindow, error) {
	return f.windows, nil
}

// MERGES what it is given, because notify does: "a workflow absent from
// the map keeps whatever it had" (internal/notify/inbox.go). This used
// to replace the map wholesale, which made the double disagree with the
// thing it doubles on exactly the question these tests ask — a quarry
// that correctly sent only the families it was asked to change would
// have looked, here, like a quarry that had wiped the rest.
func (f *fakeNotify) SetPreferences(_ context.Context, _ string, prefs map[string]map[string]bool,
	digests map[string]*api.DigestWindow) error {
	f.setWindows = digests
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.stored == nil {
		f.stored = map[string]map[string]bool{}
	}
	for wf, row := range prefs {
		f.stored[wf] = row
	}
	return nil
}

func (f *fakeNotify) PushKey(context.Context) (string, error) { return f.key, nil }

func (f *fakeNotify) PushSubscribe(_ context.Context, _, endpoint, p256dh, _ string) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.subs = append(f.subs, endpoint)
	f.subP256dh = p256dh
	return nil
}

func (f *fakeNotify) PushUnsubscribe(_ context.Context, _, endpoint string) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.unsubbed = append(f.unsubbed, endpoint)
	return nil
}

// memRead is the attention read pointer. Forward-only, like the store
// it stands in for: a stale page must not resurrect dismissed mail.
type memRead struct {
	mu sync.Mutex
	at map[string]uint64
}

func (m *memRead) MarkAttentionRead(account string, seq uint64) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if seq > m.at[account] {
		m.at[account] = seq
	}
	return nil
}

func (m *memRead) AttentionReadSeq(account string) uint64 {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.at[account]
}

type prefsForge struct {
	h       http.Handler
	prefs   *memPrefs
	devices *memDevices
	notify  *fakeNotify
	attn    *attnStub
}

// prefsForge builds the whole preferences surface: the ops behind it and
// the web handler in front. notify nil is a forge with no companion,
// which is the state the push control has to say something honest about.
func newPrefsForge(t *testing.T, notify *fakeNotify) prefsForge {
	t.Helper()
	ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	prefs, devices, attn := newMemPrefs(), newMemDevices(), &attnStub{}
	deps := api.Deps{Version: "t",
		Repos:       api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix, Code: "c1"}}),
		Attention:   attn,
		AttnRead:    &memRead{at: map[string]uint64{}},
		Prefs:       prefs,
		PushDevices: devices,
	}
	if notify != nil {
		deps.Inbox = notify
	}
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	h := HandlerWith(r, nil, nil, []Option{
		WithSessions(fakeSessions{user: "dan"}), WithPrefs(prefs)})
	return prefsForge{h: h, prefs: prefs, devices: devices, notify: notify, attn: attn}
}

func (f prefsForge) get(t *testing.T, path string) (int, string) {
	t.Helper()
	rec := httptest.NewRecorder()
	f.h.ServeHTTP(rec, withCookie(path))
	return rec.Code, rec.Body.String()
}

func (f prefsForge) post(t *testing.T, path string, form url.Values) *httptest.ResponseRecorder {
	t.Helper()
	req := httptest.NewRequest("POST", path, strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
	rec := httptest.NewRecorder()
	f.h.ServeHTTP(rec, req)
	return rec
}

// fullMatrix is what the page itself posts: every cell, both states.
func fullMatrix() url.Values {
	form := url.Values{}
	for _, family := range []string{"mention", "reply", "landing", "activity"} {
		for _, ch := range []string{"inapp", "push", "email"} {
			form.Add(family+"."+ch, "off")
			form.Add(family+"."+ch, "on")
		}
	}
	form.Set("toasts", "off")
	form.Set("notify_desktop", "off")
	return form
}

// RFC 0011 §6's matrix: four families by three channels, and every cell
// posts an EXPLICIT state. A checkbox alone posts nothing when it is
// unticked, which reads at the op as "no channels on" only by accident —
// the hidden off beside it is what makes the absence deliberate.
func TestPrefsPageRendersTwelveExplicitCells(t *testing.T) {
	f := newPrefsForge(t, newFakeNotify())
	code, body := f.get(t, "/me")
	if code != http.StatusOK {
		t.Fatalf("/me/notifications answered %d", code)
	}
	for _, family := range []string{"mention", "reply", "landing", "activity"} {
		for _, ch := range []string{"inapp", "push", "email"} {
			name := family + "." + ch
			if !strings.Contains(body, `data-cell="`+name+`"`) {
				t.Errorf("no cell for %s", name)
			}
			if !strings.Contains(body, `<input type="hidden" name="`+name+`" value="off">`) {
				t.Errorf("cell %s posts no explicit off", name)
			}
			if !strings.Contains(body, `name="`+name+`" value="on"`) {
				t.Errorf("cell %s posts no on", name)
			}
		}
	}
	if n := strings.Count(body, `data-cell="`); n != 12 {
		t.Errorf("%d cells, want 12", n)
	}
	if !strings.Contains(body, `action="/me/notifications"`) {
		t.Error("the matrix posts nowhere")
	}
}

// A family notify could not be read is shown as UNKNOWN, not as on: the
// values on screen are quarry's default, and saying "on" would report a
// setting off the strength of a lookup that never ran.
func TestPrefsPageMarksUnknownFamilies(t *testing.T) {
	n := newFakeNotify()
	n.resolved = false
	f := newPrefsForge(t, n)
	_, body := f.get(t, "/me")
	if !strings.Contains(body, `data-unknown="1"`) {
		t.Error("no unknown hook on a family notify could not resolve")
	}
	if !strings.Contains(body, "notify is unreachable") {
		t.Error("the page does not say why the cells are unknown")
	}
	// A forge whose companion answers reports nothing unknown.
	known := newPrefsForge(t, newFakeNotify())
	if _, ok := known.get(t, "/me"); strings.Contains(ok, `data-unknown="1"`) {
		t.Error("a readable companion still reported unknown families")
	}
}

// The mute list is the per-repository half of RFC 0011 §6: named repos
// with a way out, and the repos this account can see offered to add.
func TestPrefsPageListsMutesAndOffersRepos(t *testing.T) {
	f := newPrefsForge(t, newFakeNotify())
	if err := f.prefs.SetAccountPref("dan", "mute", `["tcl"]`); err != nil {
		t.Fatal(err)
	}
	_, body := f.get(t, "/me")
	if !strings.Contains(body, `data-muted="tcl"`) {
		t.Error("the muted repository is not listed")
	}
	if !strings.Contains(body, `name="unmute" value="tcl"`) {
		t.Error("no unmute control for a muted repository")
	}
	if !strings.Contains(body, `name="mute"`) {
		t.Error("no way to mute a repository")
	}
	if !strings.Contains(body, `<option value="alpha">`) {
		t.Error("a repository this account can see was not offered")
	}
}

// The form round-trips through me.prefs.set: what comes back on the page
// is what the store now holds, and the banner says which halves took.
func TestPrefsFormRoundTrips(t *testing.T) {
	notify := newFakeNotify()
	f := newPrefsForge(t, notify)
	form := fullMatrix()
	form.Del("mention.email") // every state of that cell: off, and nothing else
	form.Add("mention.email", "off")
	form.Del("mention.push")
	form.Add("mention.push", "off")
	rec := f.post(t, "/me/notifications", form)
	if rec.Code != http.StatusOK {
		t.Fatalf("saving preferences answered %d", rec.Code)
	}
	body := rec.Body.String()
	if !strings.Contains(body, `data-applied-local="1"`) {
		t.Error("the banner does not report the forge-local half")
	}
	if !strings.Contains(body, `data-applied-notify="1"`) {
		t.Error("the banner does not report the notify half")
	}
	notify.mu.Lock()
	row := notify.stored["quarry-mention"]
	notify.mu.Unlock()
	if row["push"] || row["email"] {
		t.Errorf("the unticked channels stayed on: %+v", row)
	}
	if !row["in_app"] {
		t.Errorf("the ticked channel did not take: %+v", row)
	}
	if got := f.prefs.AccountPref("dan", "toasts"); got != "off" {
		t.Errorf("the toasts switch stored %q, want off", got)
	}
	// And the page redrawn from the store agrees with what was saved.
	_, page := f.get(t, "/me")
	i := strings.Index(page, `data-cell="mention.push"`)
	if i < 0 {
		t.Fatal("the redrawn page lost the cell")
	}
	if strings.Contains(page[i:i+220], "checked") {
		t.Error("a channel saved off came back ticked")
	}
}

// A forge with no companion — or one that did not answer. The local
// half must still take, and the page must say the other half did not.
func TestPrefsBannerIsHonestWhenNotifyIsAbsent(t *testing.T) {
	f := newPrefsForge(t, nil)
	rec := f.post(t, "/me/notifications", fullMatrix())
	body := rec.Body.String()
	if !strings.Contains(body, `data-applied-local="1"`) {
		t.Error("the forge-local half did not report as saved")
	}
	if !strings.Contains(body, `data-applied-notify="0"`) {
		t.Error("the page claimed notify took the write")
	}
}

// The push control states, all from what the SERVER knows: whether this
// forge has a companion at all, and whether the account has devices.
func TestPushControlStates(t *testing.T) {
	none := newPrefsForge(t, nil)
	if _, body := none.get(t, "/me"); !strings.Contains(body, `data-push="unsupported"`) {
		t.Error("a forge with no companion did not say push is unavailable")
	}
	f := newPrefsForge(t, newFakeNotify())
	_, body := f.get(t, "/me")
	if !strings.Contains(body, `data-push="off"`) {
		t.Error("an account with no devices did not read as not enabled")
	}
	if err := f.devices.AddPushDevice("dan", "https://push.example/x1", "Firefox on Linux"); err != nil {
		t.Fatal(err)
	}
	_, body = f.get(t, "/me")
	if !strings.Contains(body, `data-push="on"`) {
		t.Error("an account with a device did not read as enabled")
	}
	if !strings.Contains(body, "Firefox on Linux") {
		t.Error("the device is not listed")
	}
	if !strings.Contains(body, `data-device="`+endpointHash("https://push.example/x1")+`"`) {
		t.Error("the device row carries no hook")
	}
}

// RFC 0011 §11: push is the ONE recorded exception to Principle 3, and
// the control says so rather than failing quietly.
func TestPushControlSaysItNeedsScript(t *testing.T) {
	f := newPrefsForge(t, newFakeNotify())
	_, body := f.get(t, "/me")
	for _, want := range []string{
		"Needs JavaScript and a browser that supports push.",
		"add quarry to the Home Screen first",
		"navigator.serviceWorker.register('/sw.js')",
		"Notification.requestPermission()",
		"pushManager.subscribe",
		"/me/push/key",
		"@get('/me/push.frag')",
	} {
		if !strings.Contains(body, want) {
			t.Errorf("the push control misses %q", want)
		}
	}
	// No script FILE, ever: the exception is one expression on one
	// control, not a bundle (RFC 0001 Principle 3).
	if strings.Contains(body, "<script src=\"/me/push") {
		t.Error("the push control pulled in a script file")
	}
}

// The key route is what both the control and the service worker read.
// It needs a session: a worker offers a cookie, an anonymous prober
// offers nothing, and the two must not be answered the same way.
func TestPushKeyNeedsASessionAndRelaysTheKey(t *testing.T) {
	f := newPrefsForge(t, newFakeNotify())
	rec := httptest.NewRecorder()
	f.h.ServeHTTP(rec, httptest.NewRequest("GET", "/me/push/key", nil))
	if rec.Code != http.StatusUnauthorized {
		t.Fatalf("anonymous /me/push/key answered %d, want 401", rec.Code)
	}
	code, body := f.get(t, "/me/push/key")
	if code != http.StatusOK {
		t.Fatalf("/me/push/key answered %d", code)
	}
	var out struct {
		PublicKey string `json:"publicKey"`
	}
	if err := json.Unmarshal([]byte(body), &out); err != nil {
		t.Fatalf("the key is not JSON: %v (%s)", err, body)
	}
	if out.PublicKey != "BPublicKeyBase64Url" {
		t.Errorf("relayed key %q", out.PublicKey)
	}
}

// The subscription is posted as the browser's own PushSubscription JSON,
// verbatim — reshaping it in the page is how a field goes missing.
func TestPushSubscribeForwardsAndRecordsTheDevice(t *testing.T) {
	notify := newFakeNotify()
	f := newPrefsForge(t, notify)
	body := `{"endpoint":"https://push.example/abc","expirationTime":null,` +
		`"keys":{"p256dh":"BPk","auth":"AUTH"}}`
	req := httptest.NewRequest("POST", "/me/push", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) Firefox/140.0")
	req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
	rec := httptest.NewRecorder()
	f.h.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("POST /me/push answered %d: %s", rec.Code, rec.Body.String())
	}
	notify.mu.Lock()
	subs, p := append([]string(nil), notify.subs...), notify.subP256dh
	notify.mu.Unlock()
	if len(subs) != 1 || subs[0] != "https://push.example/abc" {
		t.Fatalf("notify received %v", subs)
	}
	if p != "BPk" {
		t.Errorf("the p256dh key did not reach notify: %q", p)
	}
	devs, err := f.devices.PushDevices("dan")
	if err != nil {
		t.Fatal(err)
	}
	if len(devs) != 1 {
		t.Fatalf("%d devices recorded, want 1", len(devs))
	}
	if devs[0].UA == "" {
		t.Error("the device was recorded with no label")
	}
	// Anonymous posts are refused: a subscription belongs to somebody.
	anon := httptest.NewRecorder()
	req = httptest.NewRequest("POST", "/me/push", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	f.h.ServeHTTP(anon, req)
	if anon.Code != http.StatusUnauthorized {
		t.Errorf("anonymous POST /me/push answered %d, want 401", anon.Code)
	}
}

// Turning push off drops the credential in notify and forgets the row.
func TestPushDeleteDropsTheDevice(t *testing.T) {
	notify := newFakeNotify()
	f := newPrefsForge(t, notify)
	if err := f.devices.AddPushDevice("dan", "https://push.example/abc", "Firefox"); err != nil {
		t.Fatal(err)
	}
	rec := f.post(t, "/me/push/delete", url.Values{"endpoint": {"https://push.example/abc"}})
	if rec.Code != http.StatusSeeOther {
		t.Fatalf("POST /me/push/delete answered %d", rec.Code)
	}
	notify.mu.Lock()
	gone := append([]string(nil), notify.unsubbed...)
	notify.mu.Unlock()
	if len(gone) != 1 || gone[0] != "https://push.example/abc" {
		t.Fatalf("notify was asked to drop %v", gone)
	}
	devs, err := f.devices.PushDevices("dan")
	if err != nil {
		t.Fatal(err)
	}
	if len(devs) != 0 {
		t.Errorf("%d devices left, want 0", len(devs))
	}
}

// The fragment is how the control redraws after the browser subscribed,
// with no page reload and no second source of truth for its state.
func TestPushFragmentRedrawsTheControl(t *testing.T) {
	f := newPrefsForge(t, newFakeNotify())
	if err := f.devices.AddPushDevice("dan", "https://push.example/abc", "Firefox"); err != nil {
		t.Fatal(err)
	}
	code, body := f.get(t, "/me/push.frag")
	if code != http.StatusOK {
		t.Fatalf("/me/push.frag answered %d", code)
	}
	for _, want := range []string{
		"event: datastar-patch-elements",
		`id="push-control"`,
		`data-push="on"`,
	} {
		if !strings.Contains(body, want) {
			t.Errorf("the fragment misses %q:\n%s", want, body)
		}
	}
}

// The preferences page is reachable from the two places somebody looks:
// the account menu and the inbox's own settings fold.
func TestPreferencesAreLinkedFromTheInbox(t *testing.T) {
	f := newPrefsForge(t, newFakeNotify())
	_, body := f.get(t, "/inbox")
	if !strings.Contains(body, `href="/me#notifications"`) {
		t.Error("the inbox does not link to the preferences page")
	}
	if strings.Contains(body, `action="/inbox/notify"`) {
		t.Error("the switches are still on the inbox as well as the preferences page")
	}
}

// The inbox pages through `inbox.list`'s composite cursor: the "older"
// link carries it verbatim, and the page it leads to is a different set
// of rows rather than the same ones again.
func TestInboxOlderLinkCarriesTheCursor(t *testing.T) {
	f := newPrefsForge(t, nil)
	for i := 0; i < 60; i++ {
		if _, err := f.attn.Attend("dan", api.AttentionEvent{
			Kind: "mention", Repo: "alpha", From: "eve",
			Summary: "note", Link: "/r/alpha/forum/abc",
		}); err != nil {
			t.Fatal(err)
		}
	}
	_, body := f.get(t, "/inbox")
	i := strings.Index(body, `class="inbox-older-link"`)
	if i < 0 {
		t.Fatalf("60 rows and no older link:\n%s", body)
	}
	href := body[strings.Index(body[i:], `href="`)+i+6:]
	href = href[:strings.Index(href, `"`)]
	if !strings.Contains(href, "before=") {
		t.Fatalf("the older link carries no cursor: %q", href)
	}
	if !strings.Contains(body, `data-id="local:60"`) {
		t.Error("the first page does not start at the newest row")
	}
	_, older := f.get(t, strings.ReplaceAll(href, "&amp;", "&"))
	if strings.Contains(older, `data-id="local:60"`) {
		t.Error("the older page repeated the newest row")
	}
	if !strings.Contains(older, `data-id="local:10"`) {
		t.Errorf("the older page does not carry the rows the first one cut off:\n%s", older)
	}
}

// The row control reaches `inbox.mark` with the state that half of the
// list can actually reach: a notify message is archived (done), one of
// quarry's own attention events has no per-item archive, so it is
// marked read. Without script the same form gets a redirect back to the
// filter AND the page it came from (Principle 3).
func TestInboxRowControlMarksWhatEachHalfCanReach(t *testing.T) {
	notify := newFakeNotify()
	notify.msgs = []api.InboxMessage{{ID: "m1", Workflow: "quarry-mention",
		Subject: "alice mentioned you", At: "2026-08-29T12:00:00Z"}}
	f := newPrefsForge(t, notify)
	if _, err := f.attn.Attend("dan", api.AttentionEvent{
		Kind: "mention", Repo: "alpha", From: "eve", Link: "/r/alpha/forum/abc",
		At: "2026-08-29T11:00:00Z",
	}); err != nil {
		t.Fatal(err)
	}
	_, body := f.get(t, "/inbox")
	local := body[strings.Index(body, `data-id="local:1"`):]
	local = local[:strings.Index(local, "</li>")]
	if !strings.Contains(local, `name="as" value="read"`) {
		t.Errorf("a local row offers an archive it cannot perform:\n%s", local)
	}
	notifyRow := body[strings.Index(body, `data-id="m1"`):]
	notifyRow = notifyRow[:strings.Index(notifyRow, "</li>")]
	if !strings.Contains(notifyRow, `name="as" value="done"`) {
		t.Errorf("a notify row does not archive:\n%s", notifyRow)
	}
	rec := f.post(t, "/inbox/mark", url.Values{
		"id": {"local:1"}, "as": {"read"}, "f": {"mentions"}, "before": {"CURSOR"}})
	if rec.Code != http.StatusSeeOther {
		t.Fatalf("a form submit answered %d, want 303", rec.Code)
	}
	if got := rec.Header().Get("Location"); got != "/inbox?f=mentions&before=CURSOR" {
		t.Errorf("redirected to %q, want the filter and page it came from", got)
	}
	_, body = f.get(t, "/inbox")
	if !strings.Contains(body, `data-unread="0">`) {
		t.Errorf("the row did not come back read:\n%s", body)
	}
}

// The live answer removes a row only where a reload would also stop
// showing it: an archived notify row anywhere, a row marked read in the
// unread tab. Removing it anywhere else would show something a refresh
// contradicts.
func TestInboxLiveRemovalMatchesAReload(t *testing.T) {
	f := newPrefsForge(t, newFakeNotify())
	if _, err := f.attn.Attend("dan", api.AttentionEvent{Kind: "mention", Repo: "alpha"}); err != nil {
		t.Fatal(err)
	}
	live := func(form url.Values) string {
		t.Helper()
		req := httptest.NewRequest("POST", "/inbox/mark", strings.NewReader(form.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.Header.Set("datastar-request", "true")
		req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
		rec := httptest.NewRecorder()
		f.h.ServeHTTP(rec, req)
		return rec.Body.String()
	}
	got := live(url.Values{"id": {"local:1"}, "as": {"read"}, "f": {"unread"}})
	for _, want := range []string{
		"event: datastar-patch-elements", "data: mode remove", "data: selector #attn-local-1",
	} {
		if !strings.Contains(got, want) {
			t.Errorf("the unread tab did not drop a row it just read: missing %q:\n%s", want, got)
		}
	}
	if got := live(url.Values{"id": {"local:1"}, "as": {"read"}, "f": {"all"}}); strings.Contains(got, "mode remove") {
		t.Errorf("a read row was removed from a tab that still lists it:\n%s", got)
	}
}

// A mark that did not land must not look like one that did.
func TestInboxMarkFailureRemovesNothing(t *testing.T) {
	notify := newFakeNotify()
	notify.markErr = errors.New("notify is down")
	f := newPrefsForge(t, notify)
	form := url.Values{"id": {"m1"}, "as": {"done"}, "f": {"all"}}
	rec := f.post(t, "/inbox/mark", form)
	if rec.Code != http.StatusSeeOther {
		t.Fatalf("a failed mark answered %d", rec.Code)
	}
	if got := rec.Header().Get("Location"); !strings.Contains(got, "err=mark") {
		t.Errorf("the redirect says nothing about the failure: %q", got)
	}
	_, body := f.get(t, "/inbox?f=all&err=mark")
	if !strings.Contains(body, `id="inbox-error" class="attn-none" data-error="1"`) {
		t.Errorf("the page does not report the failed mark:\n%s", body)
	}
	req := httptest.NewRequest("POST", "/inbox/mark", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("datastar-request", "true")
	req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
	live := httptest.NewRecorder()
	f.h.ServeHTTP(live, req)
	got := live.Body.String()
	if strings.Contains(got, "mode remove") {
		t.Errorf("a failed mark still took the row off the page:\n%s", got)
	}
	if !strings.Contains(got, `data-error="1"`) {
		t.Errorf("the live answer does not report the failure:\n%s", got)
	}
}

// The id is echoed into a CSS selector, so anything that is not a row
// this page could have drawn never reaches one.
func TestInboxMarkRefusesAForeignID(t *testing.T) {
	f := newPrefsForge(t, newFakeNotify())
	form := url.Values{"id": {`x" onerror="alert(1)`}, "as": {"done"}, "f": {"all"}}
	req := httptest.NewRequest("POST", "/inbox/mark", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("datastar-request", "true")
	req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
	rec := httptest.NewRecorder()
	f.h.ServeHTTP(rec, req)
	if strings.Contains(rec.Body.String(), "onerror") {
		t.Errorf("a hand-written id reached the selector:\n%s", rec.Body.String())
	}
}

// A family notify could not be read is not posted and not written: the
// values on screen are quarry's default, and saving them would store
// that default as though somebody had chosen it.
func TestPrefsUnknownFamiliesAreNeverWritten(t *testing.T) {
	n := newFakeNotify()
	n.resolved = false
	f := newPrefsForge(t, n)
	_, body := f.get(t, "/me")
	if strings.Contains(body, `name="mention.inapp"`) {
		t.Error("an unreadable family still posts its cells")
	}
	rec := f.post(t, "/me/notifications", fullMatrix())
	if rec.Code != http.StatusOK {
		t.Fatalf("saving answered %d", rec.Code)
	}
	n.mu.Lock()
	stored := len(n.stored)
	n.mu.Unlock()
	if stored != 0 {
		t.Errorf("a matrix quarry could not read was written anyway: %+v", n.stored)
	}
	if !strings.Contains(rec.Body.String(), `data-applied-notify="0"`) {
		t.Error("the banner claimed notify took a write it never made")
	}
}

// The two forge-local switches default OFF, and the preferences page,
// the inbox fold and the stream must all say so: a page that showed
// them on would describe a forge the reader is not using.
func TestLocalSwitchesDefaultOff(t *testing.T) {
	f := newPrefsForge(t, newFakeNotify())
	_, prefs := f.get(t, "/me")
	i := strings.Index(prefs, `id="attn-toasts"`)
	if i < 0 {
		t.Fatal("no toasts switch")
	}
	if strings.Contains(prefs[i:i+80], "checked") {
		t.Error("in-page banners default on")
	}
	if j := strings.Index(prefs, `id="notify-desktop"`); strings.Contains(prefs[j:j+80], "checked") {
		t.Error("desktop notifications default on")
	}
	_, inbox := f.get(t, "/inbox")
	for _, want := range []string{
		`data-pref="toasts">In-page banners: <strong>off`,
		`data-pref="notify_desktop">Desktop notifications: <strong>off`,
	} {
		if !strings.Contains(inbox, want) {
			t.Errorf("the inbox fold disagrees with the default: missing %q", want)
		}
	}
}

// Every way the push control can fail ends at the fragment, worded by
// the server — a click that silently does nothing is the failure mode
// this replaces.
func TestPushControlReportsWhatWentWrong(t *testing.T) {
	f := newPrefsForge(t, newFakeNotify())
	_, body := f.get(t, "/me")
	for _, want := range []string{
		"'serviceWorker' in navigator", "'PushManager' in window",
		"r.ok ? r.json() : Promise.reject", "@get('/me/push.frag?err=blocked')",
		"@get('/me/push.frag?err=failed')",
	} {
		if !strings.Contains(body, want) {
			t.Errorf("the push expression misses %q", want)
		}
	}
	for code, want := range map[string]string{
		"blocked":     "blocked",
		"unsupported": "cannot do push",
		"failed":      "Try again",
	} {
		_, frag := f.get(t, "/me/push.frag?err="+code)
		if !strings.Contains(frag, `data-push-err="1"`) || !strings.Contains(frag, want) {
			t.Errorf("the %s fragment does not say what happened:\n%s", code, frag)
		}
	}
	// An unknown code says nothing rather than echoing itself back.
	if _, frag := f.get(t, "/me/push.frag?err=%3Cscript%3E"); strings.Contains(frag, "data-push-err") {
		t.Errorf("a hand-written code was echoed into the control:\n%s", frag)
	}
}

// Clearing every box in a family still turns that family off.
//
// This is the mirror of the bug above and the reason the page names the
// families it drew. A family with nothing ticked posts exactly what a
// family nobody drew posts, so if "which families" were inferred from
// the channels alone, unticking the last box would quietly do nothing —
// the page would redraw it off and the store would still have it on.
func TestClearingAFamilyStillTurnsItOff(t *testing.T) {
	notify := newFakeNotify()
	f := newPrefsForge(t, notify)
	if rec := f.post(t, "/me/notifications", fullMatrix()); rec.Code != http.StatusOK {
		t.Fatalf("first save answered %d", rec.Code)
	}
	notify.mu.Lock()
	on := notify.stored["quarry-forum-reply"]["in_app"]
	notify.mu.Unlock()
	if !on {
		t.Fatal("the fixture did not start with reply/in_app on")
	}

	cleared := fullMatrix()
	for _, col := range prefColumns {
		cleared.Del("reply." + col.Key)
	}
	if rec := f.post(t, "/me/notifications", cleared); rec.Code != http.StatusOK {
		t.Fatalf("clearing answered %d", rec.Code)
	}
	notify.mu.Lock()
	row := notify.stored["quarry-forum-reply"]
	notify.mu.Unlock()
	for ch, still := range row {
		if still {
			t.Errorf("reply/%s survived a save with every box unticked: clearing a family "+
				"does nothing if the page does not name it", ch)
		}
	}
}

// The test control says what happened, in words, and names a signature
// refusal as the server's problem rather than the phone's.
func TestThePushTestSaysWhatHappened(t *testing.T) {
	partial := pushOutcomeWords("partial", 1, 3, "403 BadJwtToken; timed out")
	if !strings.Contains(partial, "Delivered to 1 of 3 devices") || !strings.Contains(partial, "not with your phone") {
		t.Errorf("FAIL: partial reads %q", partial)
	}
	if got := pushOutcomeWords("unknown", 0, 0, ""); got != pushTestWords {
		t.Errorf("FAIL: no answer from notify reads %q", got)
	}
}

// The forge is installable: a manifest, linked from every page, that
// opens standalone over the whole site.
func TestTheForgeIsAnInstallableApp(t *testing.T) {
	var m struct {
		Display  string `json:"display"`
		Scope    string `json:"scope"`
		StartURL string `json:"start_url"`
		Icons    []struct {
			Src string `json:"src"`
		} `json:"icons"`
	}
	if err := json.Unmarshal(webManifest, &m); err != nil {
		t.Fatal(err)
	}
	if m.Display != "standalone" || m.Scope != "/" || m.StartURL != "/" || len(m.Icons) < 2 {
		t.Fatalf("FAIL: manifest %+v", m)
	}
	if !strings.Contains(renderBase(t, pageData{}), `<link rel="manifest" href="/manifest.webmanifest">`) {
		t.Fatal("FAIL: pages do not link the manifest")
	}
}

// The push control lists each device's outcome under the test, one row
// per device, with the outcome as data.
func TestThePushTestListsEachDevice(t *testing.T) {
	f := newPrefsForge(t, newFakeNotify())
	if err := f.devices.AddPushDevice("dan", "https://push.example/abc", "Firefox"); err != nil {
		t.Fatal(err)
	}
	code, frag := f.get(t, "/me/push.frag?sent=1&outcome=partial&got=1&of=2"+
		"&dev=fcm.googleapis.com%7Cdelivered%7C0%7C&dev=web.push.apple.com%7Cretired%7C403%7CBadJwtToken")
	if code != http.StatusOK {
		t.Fatalf("FAIL: answered %d", code)
	}
	for _, want := range []string{
		`data-push-result="delivered"`, `data-push-result="retired"`,
		`data-host="web.push.apple.com"`,
	} {
		if !strings.Contains(frag, want) {
			t.Errorf("FAIL: fragment lacks %s", want)
		}
	}
	if n := strings.Count(frag, `data-push-result=`); n != 2 {
		t.Errorf("FAIL: %d device rows, want 2", n)
	}
}