Artifact a69daccee46aeb4773a06154d26b351ba7ee9d1b0a857d5e901f2f70b01c29a9:
- File
internal/web/peoplepage_test.go
— part of check-in
[2f0c4fcb50]
at
2026-08-29 01:14:56
on branch trunk
— settings: every panel confirms the same way, in the palette's green
The description form got a Save/Saved swap; the other panels still said nothing at all. Worse, three of them redirected to ?saved=1, which had quietly stopped meaning anything when the flash cookie replaced it — a regression I left behind an hour ago. Six exit paths across five handlers were still writing it.
They all set a named flash now. The name matters: a page carries six Save buttons, and a bare boolean would light every one of them for a single save, telling the reader that everything was written when one thing was.
Green comes from --add, the token that already marks a passing run and an open glyph, so 'it worked' is one colour across the forge.
Webhooks is deliberately untouched: it is a LIST with Save, Pause and Delete per row, so lighting 'the' Save would need to know which row. Different shape, not this pattern.
The visibility test pinned ?saved=1; it now asserts the flash, which is the contract rather than the spelling of a mechanism. (user: dmestas, size: 17231) [annotate] [blame] [check-ins using] [more...]
package web import ( "io" "net/http" "net/http/httptest" "net/url" "sort" "strings" "testing" "craftdesign.group/quarry/internal/api" "craftdesign.group/quarry/internal/oltp" "craftdesign.group/quarry/internal/ops" ) // memCaps is a caps plane in a map: stored strings are the effective // set (no categories), and the guard refuses self-demotion from a. type memCaps struct { stored map[string]string cats map[string]string // "repo/category" → string; fleet under "" } func (m *memCaps) cat(repo, c string) string { if v, ok := m.cats[repo+"/"+c]; ok { return v } if v, ok := m.cats["/"+c]; ok { return v } return map[string]string{"nobody": "gjorzh2", "anonymous": "mnc3C"}[c] } func (m *memCaps) CapsOf(acct, _ string) CapSet { return oltp.CapSet(m.stored[acct]) } func (m *memCaps) CapHolders(string) ([]CapHolder, error) { names := make([]string, 0, len(m.stored)) for n := range m.stored { names = append(names, n) } sort.Strings(names) var out []CapHolder for _, n := range names { out = append(out, CapHolder{Account: n, Caps: m.stored[n]}) } return out, nil } func (m *memCaps) CategoryCaps(repo, cat string) string { return m.cat(repo, cat) } func (m *memCaps) SetCaps(acct, _, caps string) error { m.stored[acct] = caps; return nil } func (m *memCaps) SetCategoryCaps(repo, cat, caps string) error { if m.cats == nil { m.cats = map[string]string{} } m.cats[repo+"/"+cat] = caps return nil } func (m *memCaps) EffectiveWith(_, _, own string) CapSet { return oltp.CapSet(own) } func (m *memCaps) GuardCaps(actor, acct, repo, caps string) (string, error) { if actor == acct && oltp.CapSet(m.stored[acct]).HasAny("a") && !oltp.CapSet(caps).HasAny("a") { return "", oltp.Refusal{Reason: "that would remove your own admin on " + repo} } return "", nil } func peopleServer(t *testing.T, plane *memCaps) *httptest.Server { t.Helper() r := ops.NewRegistry() if err := api.RegisterAll(r, api.Deps{Version: "v", Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: landingIndex(t, false)}})}); err != nil { t.Fatal(err) } srv := httptest.NewServer(HandlerWith(r, nil, nil, []Option{ WithSessions(fakeSessions{user: "dan", roles: map[string]ops.Auth{"alpha": ops.Admin}}), WithCaps(plane)})) t.Cleanup(srv.Close) return srv } func postAsDan(t *testing.T, srv *httptest.Server, path string, form url.Values) *http.Response { t.Helper() req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"}) client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } resp, err := client.Do(req) if err != nil { t.Fatal(err) } t.Cleanup(func() { resp.Body.Close() }) return resp } // RFC 0008 §IA: the People section reads each string as a role and a // sentence; the raw per-account inputs are gone from Settings. func TestSettingsReadsPeopleAsRolesAndSentences(t *testing.T) { plane := &memCaps{stored: map[string]string{"dan": "a", "bob": oltp.Maintain.Caps() + "e", "eve": ""}} srv := peopleServer(t, plane) body := asDan(t, srv, "/r/alpha/settings") for _, want := range []string{`href="/r/alpha/people/bob"`, `<span class="chip role">Maintainer</span>`, `chip plus">+ `, "bob can "} { if !strings.Contains(body, want) { t.Fatalf("settings lacks %q", want) } } if strings.Contains(body, `name="acct-bob"`) { t.Fatal("raw per-account inputs are still on Settings") } if strings.Contains(body, `href="/r/alpha/people/eve"`) { t.Fatal("eve holds nothing here and should not be listed") } } // The editor: a role radio, the abilities as boxes, the string under a // disclosure. Saving a role change writes the preset and says so. func TestPersonEditorSavesARoleChange(t *testing.T) { plane := &memCaps{stored: map[string]string{"dan": "a", "bob": oltp.Write.Caps()}} srv := peopleServer(t, plane) body := asDan(t, srv, "/r/alpha/people/bob") for _, want := range []string{`value="Developer" checked`, `name="ab"`, `name="raw"`, `name="was-role" value="Developer"`} { if !strings.Contains(body, want) { t.Fatalf("editor lacks %q", want) } } resp := postAsDan(t, srv, "/r/alpha/people/bob", url.Values{ "role": {"Maintainer"}, "was-role": {"Developer"}, "raw": {oltp.Write.Caps()}, "was-raw": {oltp.Write.Caps()}, "ab": {"u"}, "was-ab": {"u"}}) if resp.StatusCode != http.StatusSeeOther { t.Fatalf("status %d", resp.StatusCode) } if plane.stored["bob"] != oltp.Maintain.Caps() { t.Fatalf("stored %q", plane.stored["bob"]) } loc := resp.Header.Get("Location") if !strings.Contains(loc, "Developer+%E2%86%92+Maintainer") { t.Fatalf("redirect does not say what changed: %s", loc) } if got := asDan(t, srv, loc); !strings.Contains(got, "Saved. Developer → Maintainer") { t.Fatal("the change line is not shown after save") } } // Bulletproofing: the guard's refusal is shown where the save was, and // nothing is written. func TestPersonEditorShowsARefusalInsteadOfSaving(t *testing.T) { plane := &memCaps{stored: map[string]string{"dan": "a"}} srv := peopleServer(t, plane) resp := postAsDan(t, srv, "/r/alpha/people/dan", url.Values{ "role": {"Developer"}, "was-role": {"Admin"}, "raw": {"a"}, "was-raw": {"a"}}) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("status %d", resp.StatusCode) } b, _ := io.ReadAll(resp.Body) if !strings.Contains(string(b), "Not saved: that would remove your own admin") { t.Fatal("refusal not shown") } if plane.stored["dan"] != "a" { t.Fatalf("saved anyway: %q", plane.stored["dan"]) } } // Adding from the roster form writes the role's string and lands on // the person's page; an unknown login is refused in words. func TestRosterAddsAPersonByRole(t *testing.T) { plane := &memCaps{stored: map[string]string{"dan": "a", "eve": ""}} srv := peopleServer(t, plane) resp := postAsDan(t, srv, "/r/alpha/people", url.Values{"name": {"eve"}, "role": {"Reader"}}) if resp.StatusCode != http.StatusSeeOther || !strings.HasPrefix(resp.Header.Get("Location"), "/r/alpha/people/eve?changed=added+as+Reader") { t.Fatalf("%d %s", resp.StatusCode, resp.Header.Get("Location")) } if plane.stored["eve"] != oltp.Read.Caps() { t.Fatalf("stored %q", plane.stored["eve"]) } resp = postAsDan(t, srv, "/r/alpha/people", url.Values{"name": {"ghost"}, "role": {"Reader"}}) if loc := resp.Header.Get("Location"); !strings.Contains(loc, "no+account+named+ghost") { t.Fatalf("unknown login: %s", loc) } } // nextCaps: the string wins when edited; a new role replaces; boxes // are the string; untouched boxes keep the stored bytes. func TestNextCapsReadsTheFormInOrder(t *testing.T) { dev := oltp.Write.Caps() base := url.Values{"role": {"Developer"}, "was-role": {"Developer"}, "raw": {dev}, "was-raw": {dev}, "was-ab": {dev}, "ab": strings.Split(dev, "")} if got := nextCaps(base, dev); got != dev { t.Fatalf("untouched form wrote %q", got) } f := url.Values{} for k, v := range base { f[k] = v } f.Set("raw", "iy") if got := nextCaps(f, dev); got != "iy" { t.Fatalf("edited string lost: %q", got) } f = url.Values{} for k, v := range base { f[k] = v } f["ab"] = append(f["ab"], "5") // moderate the forum if got := nextCaps(f, dev); oltp.Closure(got) != oltp.Closure(dev+"5") || strings.Contains(got, "3") { t.Fatalf("ticked box: %q", got) } f.Set("revoke", "1") if got := nextCaps(f, dev); got != "" { t.Fatalf("revoke: %q", got) } } // RFC 0008 §Visibility: Settings asks who can see this first, as // presets; a preset writes the two category strings. func TestVisibilityPresetsWriteTheCategoryStrings(t *testing.T) { plane := &memCaps{stored: map[string]string{"dan": "a"}} srv := peopleServer(t, plane) body := asDan(t, srv, "/r/alpha/settings") if !strings.Contains(body, `value="Public project" checked`) || strings.Contains(body, `name="private"`) { t.Fatal("settings should open on the visibility presets, without the old checkbox") } resp := postAsDan(t, srv, "/r/alpha/settings/visibility", url.Values{"visibility": {"Members only"}}) if resp.StatusCode != http.StatusSeeOther { t.Fatalf("status %d", resp.StatusCode) } if plane.cat("alpha", "nobody") != "" || plane.cat("alpha", "anonymous") != "gjorzh2mnc3C" { t.Fatalf("strings after Members only: %q / %q", plane.cat("alpha", "nobody"), plane.cat("alpha", "anonymous")) } if body := asDan(t, srv, "/r/alpha/settings"); !strings.Contains(body, `value="Members only" checked`) { t.Fatal("the preset does not read back") } // Hand-typed strings that match no preset read as Custom, decoded. plane.cats["alpha/nobody"], plane.cats["alpha/anonymous"] = "o", "o" if body := asDan(t, srv, "/r/alpha/settings"); !strings.Contains(body, `value="Custom" checked`) || !strings.Contains(body, "visitors can ") { t.Fatal("Custom is not shown decoded") } } // Private is a wizard: whoever reaches the repository only through // the categories is named and offered a role before the strings blank. func TestTakingPrivateOffersRolesToWhoWouldLoseAccess(t *testing.T) { plane := &memCaps{stored: map[string]string{"dan": "a", "eve": "", "bob": oltp.Write.Caps()}} srv := peopleServer(t, plane) resp := postAsDan(t, srv, "/r/alpha/settings/visibility", url.Values{"visibility": {"Private"}}) if loc := resp.Header.Get("Location"); loc != "/r/alpha/settings/private" { t.Fatalf("went to %s, not the wizard", loc) } if plane.cat("alpha", "nobody") != "gjorzh2" { t.Fatal("the strings blanked before anyone was asked") } body := asDan(t, srv, "/r/alpha/settings/private") if !strings.Contains(body, `name="role-eve"`) || strings.Contains(body, `name="role-bob"`) { t.Fatal("the wizard should name eve and not bob") } resp = postAsDan(t, srv, "/r/alpha/settings/private", url.Values{"confirm": {"1"}, "role-eve": {"Reader"}}) if resp.StatusCode != http.StatusSeeOther { t.Fatalf("status %d", resp.StatusCode) } if plane.stored["eve"] != oltp.Read.Caps() || plane.cat("alpha", "nobody") != "" || plane.cat("alpha", "anonymous") != "" { t.Fatalf("after confirm: eve=%q nobody=%q anon=%q", plane.stored["eve"], plane.cat("alpha", "nobody"), plane.cat("alpha", "anonymous")) } // With nobody to lose access, Private is one step. plane.stored["eve"] = oltp.Read.Caps() plane.cats = map[string]string{} resp = postAsDan(t, srv, "/r/alpha/settings/visibility", url.Values{"visibility": {"Private"}}) if loc := resp.Header.Get("Location"); !strings.HasPrefix(loc, "/r/alpha/settings") { t.Fatalf("went to %s", loc) } // The confirmation travels as a one-shot flash, not in the URL: a // query parameter stays in the address bar and a reload replays it. // Asserting the cookie is asserting the contract; the old check // pinned the spelling of a mechanism instead. if got := strings.Join(resp.Header.Values("Set-Cookie"), " "); !strings.Contains(got, "q_flash=visibility") { t.Errorf("visibility saved without a flash: %q", got) } } // RFC 0008 §IA: Settings reads top to bottom in the order of the // questions — who can see this, people, branches, repository, // integrations, advanced, danger — and the raw category strings live // under the Advanced disclosure with a decoder beside each. func TestSettingsSectionsAreInIAOrder(t *testing.T) { plane := &memCaps{stored: map[string]string{"dan": "a"}} srv := peopleServer(t, plane) body := asDan(t, srv, "/r/alpha/settings") // protected and integrations are conditional on planes this fixture // does not wire; the rest prove the top-to-bottom order. order := []string{ `id="visibility"`, `id="people"`, `id="about"`, `id="access"`, "Danger zone", } at := -1 for _, marker := range order { i := strings.Index(body, marker) if i < 0 { t.Fatalf("section %q missing", marker) } if i < at { t.Fatalf("section %q is out of IA order", marker) } at = i } // The raw strings are Advanced now, decoded in place. if !strings.Contains(body, "Advanced (fossil)") || !strings.Contains(body, "<summary>Capability letters") { t.Fatal("category strings are not under the Advanced disclosure") } if !strings.Contains(body, "— can ") { t.Fatal("no decoder beside the category inputs") } } // RFC 0008 rung 7: the live preview recomputes the whole region from // the form and morphs it — the sentence, the checkboxes and the // string are one signal — without writing anything. Ticking a box the // string does not yet grant comes back with the string widened. func TestPreviewFragRecomputesWithoutWriting(t *testing.T) { plane := &memCaps{stored: map[string]string{"dan": "a", "bob": oltp.Write.Caps()}} srv := peopleServer(t, plane) // The page arms the controls when the plane can answer what-if. page := asDan(t, srv, "/r/alpha/people/bob") if !strings.Contains(page, "preview.frag") || !strings.Contains(page, `id="person-live"`) { t.Fatal("the editor is not armed for live preview") } // Tick "moderate the forum" (5): the string was Developer's. form := url.Values{"role": {"Developer"}, "was-role": {"Developer"}, "raw": {oltp.Write.Caps()}, "was-raw": {oltp.Write.Caps()}, "was-ab": {oltp.Write.Caps()}} for _, l := range strings.Split(oltp.Write.Caps(), "") { form.Add("ab", l) } form.Add("ab", "5") req, _ := http.NewRequest("GET", srv.URL+"/r/alpha/people/bob/preview.frag?"+form.Encode(), nil) req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"}) resp, err := srv.Client().Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/event-stream") { t.Fatalf("content-type %q", ct) } b, _ := io.ReadAll(resp.Body) body := string(b) if !strings.Contains(body, "datastar-patch-elements") || !strings.Contains(body, `id="person-live"`) { t.Fatal("not a person-live morph") } // The widened string is echoed back and the change line is shown; // nothing was written. if !strings.Contains(body, "moderate the forum") { t.Fatal("the widened ability is not named in the preview") } i := strings.Index(body, `id="person-raw"`) if i < 0 || !strings.Contains(body[i:i+120], "5") { t.Fatal("the string input does not reflect the ticked ability") } if plane.stored["bob"] != oltp.Write.Caps() { t.Fatalf("preview wrote to the store: %q", plane.stored["bob"]) } } // A candidate the guard refuses is previewed with the reason, still // without writing. func TestPreviewFragShowsARefusal(t *testing.T) { plane := &memCaps{stored: map[string]string{"dan": "a"}} srv := peopleServer(t, plane) form := url.Values{"role": {"Developer"}, "was-role": {"Admin"}, "raw": {"a"}, "was-raw": {"a"}} req, _ := http.NewRequest("GET", srv.URL+"/r/alpha/people/dan/preview.frag?"+form.Encode(), nil) req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"}) resp, err := srv.Client().Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) if !strings.Contains(string(b), "remove your own admin") { t.Fatal("refusal not previewed") } if plane.stored["dan"] != "a" { t.Fatal("preview wrote") } } // RFC 0008 rung 8: the Advanced section carries the fossil settings and // configuration surfaces. On a store with no working tree they render // the honest blocked-on-upstream note (zit#153), never a fake editor. func TestFossilSettingsShowTheUpstreamNote(t *testing.T) { plane := &memCaps{stored: map[string]string{"dan": "a"}} srv := peopleServer(t, plane) // its repo has an Index but no Zit client body := asDan(t, srv, "/r/alpha/settings") if !strings.Contains(body, "Fossil settings") || !strings.Contains(body, "Configuration areas") { t.Fatal("the Advanced fossil surfaces are missing") } if !strings.Contains(body, "no store to read settings") { t.Fatalf("the no-store fallback note is not shown") } // No editable settings table when the store cannot answer. if strings.Contains(body, `name="set-`) { t.Fatal("a settings editor was drawn without a store to back it") } } // Audit follow-up (ticket ed5bdd89c39a): the raw acct- fields left the // Settings form in rung 6, but the endpoint still accepts them — so a // stale page or a hand-built POST meets the same guard the editor has. func TestRawCapsEndpointHonorsTheGuard(t *testing.T) { plane := &memCaps{stored: map[string]string{"dan": "a", "bob": oltp.Write.Caps()}} srv := peopleServer(t, plane) resp := postAsDan(t, srv, "/r/alpha/settings/caps", url.Values{"acct-dan": {"uv"}}) if resp.StatusCode != http.StatusSeeOther { t.Fatalf("status %d", resp.StatusCode) } if loc := resp.Header.Get("Location"); !strings.Contains(loc, "e=") || !strings.Contains(loc, "your+own+admin") { t.Fatalf("refusal not surfaced: %s", loc) } if plane.stored["dan"] != "a" { t.Fatalf("saved through the guard: %q", plane.stored["dan"]) } // A permitted raw save still goes through. resp = postAsDan(t, srv, "/r/alpha/settings/caps", url.Values{"acct-bob": {"u"}}) if resp.StatusCode != http.StatusSeeOther || plane.stored["bob"] != "u" { t.Fatalf("permitted save refused: %d %q", resp.StatusCode, plane.stored["bob"]) } }