package web
import (
"bytes"
"net/http"
"net/http/httptest"
"net/url"
"regexp"
"strings"
"testing"
"time"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
// The rail is built from the same list the sections are, so each rail
// entry is a section on the page and they come in the same order.
func TestSettingsRailNamesEverySectionInOrder(t *testing.T) {
body := renderSettings(t, pageData{Repo: "alpha", HasHooks: true, Caps: &capsPanel{}})
rail := body[strings.Index(body, `class="set-rail"`):strings.Index(body, `class="set-body"`)]
var hrefs []string
for _, m := range regexp.MustCompile(`href="#([a-z-]+)"`).FindAllStringSubmatch(rail, -1) {
hrefs = append(hrefs, m[1])
}
var ids []string
for _, m := range regexp.MustCompile(`<(?:section|h3) id="([a-z-]+)"`).FindAllStringSubmatch(body, -1) {
ids = append(ids, m[1])
}
if len(hrefs) < 5 || strings.Join(hrefs, " ") != strings.Join(ids, " ") {
t.Fatalf("rail %v\nsections %v", hrefs, ids)
}
}
// Every destructive button asks through the one dialog; the dialog is
// an enhancement, so each such button is a real submit inside a real
// post form and works with no script at all.
func TestSettingsConfirmButtonsAreRealSubmits(t *testing.T) {
body := renderSettings(t, pageData{Repo: "alpha", HasHooks: true, Caps: &capsPanel{},
Hooks: []HookSub{{ID: 1, URL: "https://x/y"}}, Protect: &protectPanel{Rules: []protectRule{{Branch: "trunk"}}},
Roster: &rosterPanel{Rows: []rosterRow{{Name: "bob", Caps: "v"}}}})
if n := strings.Count(body, `<dialog id="confirm"`); n != 1 {
t.Fatalf("%d confirm dialogs, want one", n)
}
confirms := regexp.MustCompile(`<button[^>]*data-confirm=`).FindAllString(body, -1)
if len(confirms) < 3 {
t.Fatalf("only %d data-confirm buttons in %q", len(confirms), confirms)
}
for _, b := range confirms {
if !strings.Contains(b, `type="submit"`) {
t.Errorf("data-confirm on a non-submit: %s", b)
}
}
for _, m := range regexp.MustCompile(`(?s)<form[^>]*>.*?</form>`).FindAllString(body, -1) {
if strings.Contains(m, "data-confirm") && !strings.Contains(m, `method="post"`) {
t.Errorf("confirm button outside a post form: %.80s", m)
}
}
}
// The footer flows on Settings — the last Save used to sit under the
// fixed one — and nowhere else.
func TestSettingsFooterFlows(t *testing.T) {
plane := &memCaps{stored: map[string]string{"dan": "a"}}
srv := peopleServer(t, plane)
if body := asDan(t, srv, "/r/alpha/settings"); !strings.Contains(body, `<body class="flow-footer"`) {
t.Fatal("settings body does not carry flow-footer")
}
if body := asDan(t, srv, "/"); strings.Contains(body, "flow-footer") {
t.Fatal("home page carries flow-footer")
}
for _, want := range []string{"body.flow-footer footer { position: static;", ".set-body .caps-table tr { display: grid;"} {
if !strings.Contains(sheetLiteral, want) {
t.Errorf("sheet lacks %q", want)
}
}
}
// A row's Remove posts the person page's own revoke with back=settings:
// the role goes, the page comes back to People. The last admin's row
// has no working Remove, and posting it anyway is refused by the guard
// and reported on the settings page, the role untouched.
func TestSettingsRowRemoveRevokesAndReturns(t *testing.T) {
plane := &memCaps{stored: map[string]string{"dan": "a", "bob": "v"}}
srv := peopleServer(t, plane)
body := asDan(t, srv, "/r/alpha/settings")
if !strings.Contains(body, `<button type="submit" disabled>Remove</button>`) {
t.Fatal("the only admin's Remove is not disabled")
}
resp := postAsDan(t, srv, "/r/alpha/people/bob", url.Values{"revoke": {"1"}, "back": {"settings"}})
if resp.StatusCode != http.StatusSeeOther || !strings.HasSuffix(resp.Header.Get("Location"), "/r/alpha/settings#people") {
t.Fatalf("%d -> %s", resp.StatusCode, resp.Header.Get("Location"))
}
if plane.stored["bob"] != "" {
t.Fatalf("bob still holds %q", plane.stored["bob"])
}
resp = postAsDan(t, srv, "/r/alpha/people/dan", url.Values{"revoke": {"1"}, "back": {"settings"}})
loc, _ := url.Parse(resp.Header.Get("Location"))
if resp.StatusCode != http.StatusSeeOther || loc.Path != "/r/alpha/settings" || loc.Query().Get("e") == "" || loc.Fragment != "people" {
t.Fatalf("refusal went %d -> %s", resp.StatusCode, resp.Header.Get("Location"))
}
if plane.stored["dan"] != "a" {
t.Fatalf("dan's admin was removed: %q", plane.stored["dan"])
}
}
type recProtect struct {
rules map[string][]oltp.Protection
set map[string][]string
}
func (p *recProtect) MayJoin(string, string, string) bool { return true }
func (p *recProtect) Protections(repo string) ([]oltp.Protection, error) { return p.rules[repo], nil }
func (p *recProtect) SetProtection(_, branch string, owners []string) error {
p.set[branch] = owners
return nil
}
// A rule's Remove names its row with drop=N; that row is cleared and the
// others are written as the form holds them.
func TestProtectDropRemovesOneRule(t *testing.T) {
plane := &recProtect{set: map[string][]string{}, rules: map[string][]oltp.Protection{
"alpha": {{Branch: "trunk", Owners: []string{"alice"}}, {Branch: "release-*", Owners: []string{"bob"}}}}}
r := ops.NewRegistry()
if err := api.RegisterAll(r, api.Deps{Version: "v",
Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: landingIndex(t, false)}}), Protect: plane}); 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(&memCaps{stored: map[string]string{"dan": "a"}}), WithProtect(plane)}))
t.Cleanup(srv.Close)
resp := postAsDan(t, srv, "/r/alpha/settings/protect", url.Values{
"branch-0": {"trunk"}, "owners-0": {"alice"},
"branch-1": {"release-*"}, "owners-1": {"bob"},
"branch-2": {""}, "owners-2": {""}, "drop": {"0"}})
if resp.StatusCode != http.StatusSeeOther {
t.Fatalf("status %d", resp.StatusCode)
}
if got := plane.set["trunk"]; len(got) != 0 {
t.Fatalf("trunk kept owners %v", got)
}
if got := strings.Join(plane.set["release-*"], " "); got != "bob" {
t.Fatalf("release-* owners %q", got)
}
}
// Every picker names a datalist, and every named datalist is on the
// page when there is anything to list — an id that dangles is a picker
// that silently offers nothing.
func TestSettingsPickersNameDatalistsOnThePage(t *testing.T) {
body := renderSettings(t, pageData{Repo: "alpha", HasHooks: true, Caps: &capsPanel{},
Protect: &protectPanel{Rules: []protectRule{{Branch: "trunk"}}, Blank: 1},
Roster: &rosterPanel{}, Logins: []string{"dan"}, CheckNames: []string{"go"}, TopicNames: []string{"ops"},
EventNouns: []string{"push"}})
ids := map[string]bool{}
for _, m := range regexp.MustCompile(`<datalist id="([a-z-]+)"`).FindAllStringSubmatch(body, -1) {
ids[m[1]] = true
}
refs := regexp.MustCompile(`<input[^>]* list="([a-z-]+)"`).FindAllStringSubmatch(body, -1)
if len(refs) < 5 {
t.Fatalf("only %d inputs name a list", len(refs))
}
for _, m := range refs {
if !ids[m[1]] {
t.Errorf("input names list %q, no datalist has that id", m[1])
}
}
}
// A chip picker is script over a plain named text field: the field
// keeps its name and its space-separated value, so the post the handler
// reads is the same one it read before the chips.
func TestSettingsChipInputsArePlainTextFields(t *testing.T) {
body := renderSettings(t, pageData{Repo: "alpha", HasHooks: true, Caps: &capsPanel{},
Protect: &protectPanel{Rules: []protectRule{{Branch: "trunk", Owners: "dan bob", Checks: "go vet"}}, Blank: 1},
Hooks: []HookSub{{ID: 1, URL: "https://x/y", Events: "push"}}, EventNouns: []string{"push", "ticket"}})
chips := regexp.MustCompile(`<input[^>]*data-chips[^>]*>`).FindAllString(body, -1)
if len(chips) < 5 {
t.Fatalf("only %d chip inputs: %q", len(chips), chips)
}
for _, in := range chips {
if !strings.Contains(in, ` name="`) || strings.Contains(in, `type="hidden"`) {
t.Errorf("chip input is not a named text field: %s", in)
}
if !strings.Contains(in, ` list="`) {
t.Errorf("chip input without a list: %s", in)
}
}
if !strings.Contains(body, `value="dan bob"`) || !strings.Contains(body, `value="go vet"`) {
t.Error("owners or checks lost their space-separated value")
}
}
type seenChecks struct {
recProtect
seen []string
rules []oltp.CheckRule
}
func (s *seenChecks) CheckNamesSeen(string, time.Time) ([]string, error) { return s.seen, nil }
func (s *seenChecks) CheckRules(string) ([]oltp.CheckRule, error) { return s.rules, nil }
// The check picker offers what CI has reported and what a rule already
// requires, once each, sorted — a rule can name a check that has not
// run yet, and a check that has run may be required by no rule.
func TestCheckNamesUnionSeenAndRequired(t *testing.T) {
p := &seenChecks{seen: []string{"vet", "go"}, rules: []oltp.CheckRule{{Branch: "trunk", Checks: []string{"go", "lint"}}}}
got := checkNamesFor(p, "alpha")
if strings.Join(got, " ") != "go lint vet" {
t.Fatalf("got %v", got)
}
if got := checkNamesFor(&recProtect{}, "alpha"); len(got) != 0 {
t.Fatalf("plane with no checks gave %v", got)
}
}
// The fossil filter earns its place only on a long list.
func TestFossilFilterOnlyOnLongLists(t *testing.T) {
rows := func(n int) []fossilRow {
out := make([]fossilRow, n)
for i := range out {
out[i].Name = "s" + strings.Repeat("x", i)
}
return out
}
short := renderSettings(t, pageData{Repo: "alpha", Caps: &capsPanel{}, Fossil: &fossilPanel{Supported: true, Rows: rows(5)}})
long := renderSettings(t, pageData{Repo: "alpha", Caps: &capsPanel{}, Fossil: &fossilPanel{Supported: true, Rows: rows(20)}})
if strings.Contains(short, `data-filter="fossil-rows"`) {
t.Error("filter on a short list")
}
if !strings.Contains(long, `data-filter="fossil-rows"`) || !strings.Contains(long, `id="fossil-rows"`) {
t.Error("long list has no filter, or the filter names a body that is not there")
}
}
// A rule is read as a sentence and edited under a disclosure, but the
// fields the handler indexes are unchanged: branch/owners/checks/premerge
// at the row's own number, and the blank row at Blank.
func TestRulesKeepTheirIndexedFields(t *testing.T) {
body := renderSettings(t, pageData{Repo: "alpha", Caps: &capsPanel{},
Protect: &protectPanel{Blank: 2, Rules: []protectRule{
{Branch: "trunk", Owners: "dan", OwnerList: []string{"dan"}},
{Branch: "release-*", Checks: "go", CheckList: []string{"go"}, Premerge: true},
}}})
for _, name := range []string{"branch-0", "owners-0", "checks-0", "premerge-0",
"branch-1", "owners-1", "checks-1", "premerge-1",
"branch-2", "owners-2", "checks-2", "premerge-2"} {
if !strings.Contains(body, `name="`+name+`"`) {
t.Errorf("no field named %q", name)
}
}
if n := strings.Count(body, `class="rule-said"`); n != 2 {
t.Errorf("%d rules read as sentences, want 2", n)
}
// Each sentence names its own branch and the people it lets in.
said := regexp.MustCompile(`(?s)<p class="rule-said">(.*?)</p>`).FindAllStringSubmatch(body, -1)
if len(said) != 2 || !strings.Contains(said[0][1], "trunk") || !strings.Contains(said[0][1], "dan") {
t.Errorf("sentence does not name its branch and owner: %v", said)
}
if !strings.Contains(said[1][1], "go") {
t.Error("a required check is not in the rule's sentence")
}
}
// The two ways a rule is quietly dead are said on the rule. With no
// roster to compare against, no owner is accused.
func TestRuleWarnsOnOwnersAndChecksThatCannotWork(t *testing.T) {
p := &protectPanel{Blank: 1, Rules: []protectRule{
{Branch: "trunk", Owners: "dan ghost", Checks: "go stale"}}}
roster := &rosterPanel{Rows: []rosterRow{{Name: "dan"}}}
annotateRules(p, roster, []string{"go"})
r := p.Rules[0]
if strings.Join(r.Unknown, " ") != "ghost" {
t.Errorf("unknown owners %v, want just the one with no role", r.Unknown)
}
if strings.Join(r.Stale, " ") != "stale" {
t.Errorf("stale checks %v, want the one nothing reported", r.Stale)
}
body := renderSettings(t, pageData{Repo: "alpha", Caps: &capsPanel{}, Protect: p, Roster: roster})
if n := strings.Count(body, `class="rule-warn"`); n != 2 {
t.Errorf("%d warnings drawn, want one per kind", n)
}
// No roster is not the same fact as "this owner holds no role".
blind := &protectPanel{Rules: []protectRule{{Branch: "trunk", Owners: "dan"}}}
annotateRules(blind, nil, nil)
if len(blind.Rules[0].Unknown) != 0 {
t.Errorf("owners accused with no roster to check against: %v", blind.Rules[0].Unknown)
}
}
// Every matching rule applies, so the probe answers with the union and
// matches globs the way the gate does.
func TestProbeUnionsEveryMatchingRule(t *testing.T) {
p := &protectPanel{Rules: []protectRule{
{Branch: "release-*", Owners: "dan", Checks: "go"},
{Branch: "release-1.2", Owners: "bob", Checks: "vet"},
{Branch: "trunk", Owners: "eve"},
}}
annotateRules(p, nil, nil)
got := probeFor(p, "release-1.2")
if !got.Matched || len(got.Rules) != 2 {
t.Fatalf("matched %v", got.Rules)
}
if strings.Join(got.Owners, " ") != "bob dan" || strings.Join(got.Checks, " ") != "go vet" {
t.Errorf("owners %v checks %v: not the union of both rules", got.Owners, got.Checks)
}
if miss := probeFor(p, "topic"); miss == nil || miss.Matched {
t.Errorf("a branch no rule names came back governed: %v", miss)
}
if probeFor(p, "") != nil || probeFor(nil, "trunk") != nil {
t.Error("probe answered a question nobody asked")
}
// The probe is a GET form, so the answer survives with no script.
body := renderSettings(t, pageData{Repo: "alpha", Caps: &capsPanel{}, Protect: p, Probe: got})
if !strings.Contains(body, `<form method="get"`) || !strings.Contains(body, `class="probe-said"`) {
t.Error("the probe is not a plain GET form with its answer on the page")
}
}
// A role changes from the roster row, by the same fields the person
// page posts: was-role is what the row was rendered from, so the two
// take one path through the guard and the row returns to settings.
func TestRosterRowChangesRoleInline(t *testing.T) {
body := renderSettings(t, pageData{Repo: "alpha", Caps: &capsPanel{},
Roster: &rosterPanel{Admins: 2, Presets: oltp.Presets, Rows: []rosterRow{
{Name: "dan", Role: "Developer", Caps: "vw", Plus: []string{"landings"}},
{Name: "bob", Role: "Admin", Caps: "a", Admin: true},
}}})
rows := regexp.MustCompile(`(?s)<form[^>]*people/dan"[^>]*>(.*?)</form>`).FindStringSubmatch(body)
if rows == nil {
t.Fatal("no row form for dan")
}
row := rows[1]
for _, want := range []string{`name="back" value="settings"`, `name="was-role" value="Developer"`,
`<select name="role"`, `value="Developer" selected`} {
if !strings.Contains(row, want) {
t.Errorf("row is missing %q", want)
}
}
// Every preset is offered, and the one the person holds is the one
// selected — a picker that opens on the wrong answer is a trap.
if n := strings.Count(row, "<option "); n != len(oltp.Presets) {
t.Errorf("%d options for %d presets", n, len(oltp.Presets))
}
if strings.Count(row, " selected") != 1 {
t.Error("not exactly one selected option")
}
// A person whose string differs from the preset is told what the
// change costs, rather than losing the difference silently.
if !strings.Contains(row, `class="set-why"`) {
t.Error("a row with chips does not say the change replaces them")
}
plain := regexp.MustCompile(`(?s)<form[^>]*people/bob"[^>]*>(.*?)</form>`).FindStringSubmatch(body)
if plain != nil && strings.Contains(plain[1], `class="set-why">changing`) {
t.Error("a row with no chips warns about chips")
}
}
// A reporter token says whether it is in service. "Never used" is the
// fact that makes a list of credentials revocable, so it is stated
// rather than left as an absence.
func TestReporterTokensSayWhenTheyLastReported(t *testing.T) {
var panel ciTokenPanel
remarshal(map[string]any{"tokens": []map[string]string{
{"hash": strings.Repeat("a", 64), "label": "runner", "created": "2026-01-01T00:00:00Z", "used": "2026-08-30T00:00:00Z"},
{"hash": strings.Repeat("b", 64), "label": "old", "created": "2026-01-01T00:00:00Z"},
}}, &panel)
if len(panel.Tokens) != 2 || panel.Tokens[0].Used == "" || panel.Tokens[1].Used != "" {
t.Fatalf("the op's used field did not reach the page: %+v", panel.Tokens)
}
body := renderSettings(t, pageData{Repo: "alpha", Caps: &capsPanel{}, CITokens: &panel})
rows := regexp.MustCompile(`(?s)<li>\s*<form[^>]*ci-tokens.*?</form>\s*</li>`).FindAllString(body, -1)
if len(rows) != 2 {
t.Fatalf("%d token rows", len(rows))
}
if !strings.Contains(rows[0], panel.Tokens[0].Used) {
t.Error("a token in service does not say when it last reported")
}
if strings.Contains(rows[1], panel.Tokens[0].Used) {
t.Error("an unused token borrowed another's last-used")
}
}
// The hook row answers "did it fire" from the delivery log, and offers
// the two actions that answer it another way: send that one again, or
// send a test now. A forge with no log shows the endpoint and no
// history, rather than an empty history reading as "nothing was sent".
func TestHookRowShowsWhatTheEndpointReceived(t *testing.T) {
hooks := []HookSub{{ID: 7, URL: "https://x/y", Active: true, Events: "*"}}
with := renderSettings(t, pageData{Repo: "alpha", Caps: &capsPanel{}, HasHooks: true,
Hooks: hooks, HookLog: map[int64][]HookDelivery{7: {
{ID: 3, Kind: "checkin", At: "2026-09-04T10:00:00Z", Status: 500, Ms: 91, Attempt: 2, Err: "endpoint answered 500"},
{ID: 2, Kind: "ticket", At: "2026-09-04T09:00:00Z", Status: 200, Ms: 42, Attempt: 1},
}}})
if !strings.Contains(with, `class="hook-log"`) {
t.Fatal("the delivery log is not shown")
}
// The status is the fact, and success and failure read differently.
if !strings.Contains(with, `class="d-st bad"`) || !strings.Contains(with, `class="d-st ok"`) {
t.Error("an accepted and a refused delivery look the same")
}
if !strings.Contains(with, "endpoint answered 500") {
t.Error("the reason a delivery failed is not shown")
}
// Redeliver names the DELIVERY; ping names the endpoint.
if !strings.Contains(with, `name="do" value="redeliver"`) {
t.Error("no way to send a recorded delivery again")
}
if !strings.Contains(with, `name="do" value="ping"`) {
t.Error("no way to test the endpoint")
}
if !strings.Contains(with, `name="id" value="3"`) {
t.Error("redeliver does not name which delivery")
}
// No log kept: the endpoint still renders, with no history section.
none := renderSettings(t, pageData{Repo: "alpha", Caps: &capsPanel{}, HasHooks: true, Hooks: hooks})
if strings.Contains(none, `class="hook-log"`) {
t.Error("a forge with no delivery log claims an empty history")
}
if !strings.Contains(none, "https://x/y") {
t.Error("the endpoint vanished with its history")
}
}
// The trail shows what was changed HERE: which field, and what it held
// before.
//
// The kind whitelist that used to guard this is gone, and nothing
// replaces it. It existed because the trail was built from the event
// stream, where admitting a noun like `mirror.` would have turned the
// section into a pull log with the changes buried in it. The durable
// log records settings fields and nothing else, so there is no longer
// anything to keep out -- the property is structural instead of a list
// somebody has to remember to update.
func TestSettingsTrailShowsWhichFieldChanged(t *testing.T) {
var many []SettingsChange
for i := 0; i < 25; i++ {
many = append(many, SettingsChange{Field: "description", Was: "old", Now: "new", Actor: "dan"})
}
store := &fixedSettings{rows: many}
settingsChanges(store, "alpha")
if store.asked != 10 {
t.Errorf("the page asked the store for %d rows, want 10: the limit belongs in the "+
"query, not in a slice of everything it returned", store.asked)
}
// Nothing to show is no section, not an empty one — and a forge
// with no log renders the rest of settings unharmed.
if got := settingsChanges(nil, "alpha"); got != nil {
t.Error("a forge with no settings log invented a trail")
}
body := renderSettings(t, pageData{Repo: "alpha", Caps: &capsPanel{}})
if strings.Contains(body, `id="trail"`) {
t.Error("the trail section is drawn with nothing in it")
}
// What a reader came for: the field, and what it was before. The
// record this replaced said "settings changed" and left both out.
with := renderSettings(t, pageData{Repo: "alpha", Caps: &capsPanel{},
Trail: []SettingsChange{{Field: "mirrorable", Was: "true", Now: "false", Actor: "dan"}}})
if !strings.Contains(with, `id="trail"`) {
t.Error("the trail is not rendered")
}
for _, want := range []string{"mirrorable", "true", "false"} {
if !strings.Contains(with, want) {
t.Errorf("the trail does not show %q, so it cannot say what changed or "+
"what it was before", want)
}
}
if !strings.Contains(with, `data-field="mirrorable"`) {
t.Error("the row does not carry its field as a hook")
}
// The rail names it, so it is reachable rather than only scrollable.
if !strings.Contains(with, `href="#trail"`) {
t.Error("the rail does not name the trail")
}
}
// A field going from nothing to something, and back, reads as setting
// and clearing rather than as an arrow from an empty string.
func TestSettingsTrailWordsEmptyValues(t *testing.T) {
set := renderSettings(t, pageData{Repo: "alpha", Caps: &capsPanel{},
Trail: []SettingsChange{{Field: "homepage", Was: "", Now: "https://x", Actor: "dan"}}})
if !strings.Contains(set, "set to") {
t.Error("a field given its first value does not read as being set")
}
cleared := renderSettings(t, pageData{Repo: "alpha", Caps: &capsPanel{},
Trail: []SettingsChange{{Field: "homepage", Was: "https://x", Now: "", Actor: "dan"}}})
if !strings.Contains(cleared, "cleared") {
t.Error("a field emptied does not read as being cleared")
}
}
// An unattributed write says so rather than rendering a blank byline.
// Empty is a real answer — an unauthenticated path — and the log
// records it as such instead of inventing an account.
func TestSettingsTrailNamesAnUnidentifiedCaller(t *testing.T) {
body := renderSettings(t, pageData{Repo: "alpha", Caps: &capsPanel{},
Trail: []SettingsChange{{Field: "chat", Was: "true", Now: "false"}}})
if !strings.Contains(body, "an unidentified caller") {
t.Error("a settings change with no actor renders an empty byline")
}
}
type fixedSettings struct {
rows []SettingsChange
asked int
}
func (f *fixedSettings) Changes(_ string, limit int) ([]SettingsChange, error) {
f.asked = limit
return f.rows, nil
}
// An owner bringing their own machines starts with NO tokens — that is
// the whole premise. The Worker tokens section, which holds the mint
// form, was shown only once a token already existed, so the first one
// could never be minted through the page. The Runners section above it
// meanwhile said "no runner has attached to this repository yet",
// which was true and unactionable.
//
// Rendered rather than reasoned about: the bug was a template
// condition, and it shipped because every check I ran was against a
// repository that already had what it needed.
func TestTheFirstWorkerTokenCanBeMinted(t *testing.T) {
var buf bytes.Buffer
if err := noHooks.template(settingsPage).ExecuteTemplate(&buf, "content", pageData{
Repo: "orchard", WorkerGroup: "orchard",
}); err != nil {
t.Fatal(err)
}
got := buf.String()
for _, hook := range []string{`id="worker-tokens"`, `data-hook="mint-worker-token"`} {
if !strings.Contains(got, hook) {
t.Errorf("FAIL no %s with zero tokens — the first one cannot be minted", hook)
} else {
t.Logf("ok %s present with zero tokens", hook)
}
}
// The page must have rendered around the control, not merely
// contained the string: a template placed outside its own define
// block once produced 350 bytes that still matched a substring.
if len(got) < 2000 {
t.Fatalf("FAIL settings page rendered %d bytes — too short to be the page", len(got))
}
t.Logf("ok page rendered (%d bytes)", len(got))
}
// Without an engine there is nothing for a worker to attach to, so the
// group is empty and neither section appears — no mint form that could
// not work if pressed.
func TestNoEngineMeansNoRunnerSections(t *testing.T) {
var buf bytes.Buffer
if err := noHooks.template(settingsPage).ExecuteTemplate(&buf, "content", pageData{Repo: "orchard"}); err != nil {
t.Fatal(err)
}
got := buf.String()
for _, hook := range []string{`id="worker-tokens"`, `id="runners"`, `data-hook="mint-worker-token"`} {
if strings.Contains(got, hook) {
t.Errorf("FAIL %s shown with no engine", hook)
} else {
t.Logf("ok %s absent with no engine", hook)
}
}
}
// A rule that can never fire has to SAY so on the page.
//
// The whole defect was silence: protection keyed on a branch name that
// check-ins do not resolve to refuses nothing, and the settings page
// reported it as protected anyway. An operator set it, believed it, and
// the branch was open (d76e42b832f7). The warning sits beside the two
// that were already there — an owner with no role, a check nothing
// reports — because it is the same kind of quiet breakage.
func TestAnAliasRuleSaysItRefusesNothing(t *testing.T) {
var buf bytes.Buffer
if err := noHooks.template(settingsPage).ExecuteTemplate(&buf, "content", pageData{
Repo: "dantest-alias",
Protect: &protectPanel{Rules: []protectRule{
{Branch: "trunk", Owners: "jva", OwnerList: []string{"jva"}, Alias: "main"},
{Branch: "main", Owners: "jva", OwnerList: []string{"jva"}},
}},
}); err != nil {
t.Fatal(err)
}
got := buf.String()
if !strings.Contains(got, `data-hook="rule-alias"`) {
t.Fatal("FAIL: a rule naming an alias renders no warning — the page still says protected")
}
// Once, not on every rule: the real branch's own rule is fine and
// must not be marked broken.
if n := strings.Count(got, `data-hook="rule-alias"`); n != 1 {
t.Errorf("FAIL: %d alias warnings for one alias rule, want 1", n)
}
t.Log("ok: the alias rule is marked, the real one is not")
}
// Beside Import sit the downloads that make it safe to use: one per
// configuration area zit knows (4b8508f3). None when the store cannot
// be written, since there is then no Import to restore through either.
func TestConfigurationAreasOfferAnExportBesideImport(t *testing.T) {
on := renderSettings(t, pageData{Repo: "alpha", Caps: &capsPanel{}, Fossil: &fossilPanel{Supported: true}})
i := strings.Index(on, `data-hook="config-export"`)
if i < 0 {
t.Fatal("FAIL: no export control")
}
block := on[i : strings.Index(on[i:], "</p>")+i]
for _, area := range []string{"user", "shun"} {
if !strings.Contains(block, `/r/alpha/settings/config/export/`+area+`"`) {
t.Errorf("FAIL: no export link for %s", area)
}
}
off := renderSettings(t, pageData{Repo: "alpha", Caps: &capsPanel{}, Fossil: &fossilPanel{Note: "no store"}})
if strings.Contains(off, `data-hook="config-export"`) {
t.Fatal("FAIL: export offered where the store cannot be reached")
}
t.Log("ok: user and shun export links beside Import; none when unsupported")
}
// A mirror's settings answer its administrator's questions where they
// ask them (4b8508f3): the upstream and interval, the last pull and how
// it went, Pull now and the write switch for a repository admin, and a
// fleet link only for someone who can open the fleet. A native
// repository has no such section, in the rail or the body.
func TestAMirrorIsRunFromItsOwnSettings(t *testing.T) {
d := pageData{Repo: "alpha", Caps: &capsPanel{}, RepoAdmin: true}
d.Posture.Mirror.Tracking, d.Posture.Mirror.URL, d.Posture.Mirror.Every = true, "https://sqlite.org/src", 3600
d.Posture.Mirror.State, d.Posture.Mirror.LastAt, d.Posture.Mirror.LastOK = "failing", "2026-09-23T16:00:00Z", "2026-09-22T16:00:00Z"
d.Posture.Mirror.LastErr, d.Posture.Mirror.Failures = "host sqlite.org does not resolve", 3
body := renderSettings(t, d)
i := strings.Index(body, `data-hook="mirror-settings"`)
if i < 0 || !strings.Contains(body, `href="#mirror"`) {
t.Fatal("FAIL: a tracking mirror has no Mirror section, or the rail does not list it")
}
rail := body[strings.Index(body, `class="set-rail"`):strings.Index(body, `class="set-body"`)]
var hrefs, ids []string
for _, m := range regexp.MustCompile(`href="#([a-z-]+)"`).FindAllStringSubmatch(rail, -1) {
hrefs = append(hrefs, m[1])
}
for _, m := range regexp.MustCompile(`<(?:section|h3) id="([a-z-]+)"`).FindAllStringSubmatch(body, -1) {
ids = append(ids, m[1])
}
if strings.Join(hrefs, " ") != strings.Join(ids, " ") {
t.Fatalf("FAIL: with a mirror the rail %v and the sections %v disagree", hrefs, ids)
}
sec := body[i : strings.Index(body[i:], "</section>")+i]
for _, want := range []string{`https://sqlite.org/src`, "every hour", `data-hook="mirror-last" data-state="failing"`,
"3 in a row", "does not resolve", `data-hook="mirror-pull-now"`, `action="/r/alpha/settings/mirror/run"`,
`action="/r/alpha/settings/mirror/writes"`, `name="accept" value="1">`} {
if !strings.Contains(sec, want) {
t.Errorf("FAIL: the section lacks %q", want)
}
}
if strings.Contains(sec, `href="/fleet`) {
t.Error("FAIL: a repository admin who cannot open the fleet is linked to it")
}
d.IsAdmin, d.Posture.Mirror.AcceptWrites = true, true
sec2 := renderSettings(t, d)
if !strings.Contains(sec2, `href="/fleet?tab=mirrors"`) || !strings.Contains(sec2, `name="accept" value="1" checked>`) {
t.Error("FAIL: a site admin gets no fleet link, or the switch does not show writes are on")
}
d.RepoAdmin, d.IsAdmin = false, false
if ro := renderSettings(t, d); strings.Contains(ro, `data-hook="mirror-pull-now"`) || strings.Contains(ro, `data-hook="mirror-writes"`) {
t.Error("FAIL: controls offered to a viewer who does not administer the repository")
}
native := renderSettings(t, pageData{Repo: "alpha", Caps: &capsPanel{}, RepoAdmin: true})
if strings.Contains(native, `data-hook="mirror-settings"`) || strings.Contains(native, `href="#mirror"`) {
t.Error("FAIL: a native repository shows a Mirror section")
}
t.Log("ok: upstream, hourly, failing ×3 with its cause, Pull now and the write switch for an admin; fleet link for site admins only; nothing on a native repository")
}