package web
import (
"regexp"
"strings"
"testing"
)
// hookAttr is the value of attr on the element carrying data-hook=hook.
func hookAttr(t *testing.T, html, hook, attr string) (string, bool) {
t.Helper()
tag := regexp.MustCompile(`<[a-z0-9]+ [^>]*data-hook="` + regexp.QuoteMeta(hook) + `"[^>]*>`).FindString(html)
if tag == "" {
return "", false
}
m := regexp.MustCompile(`\b` + regexp.QuoteMeta(attr) + `="([^"]*)"`).FindStringSubmatch(tag)
if m == nil {
return "", true
}
return m[1], true
}
func renderSetup(t *testing.T, s meSignOff) string {
t.Helper()
var b strings.Builder
if err := noHooks.template(signOffPageTmpl).ExecuteTemplate(&b, "content", pageData{Title: "Approve?", SignOff: &s}); err != nil {
t.Fatal(err)
}
return b.String()
}
// RFC 0030 step 4: the setup request's page carries a hook each for the
// scope, the repositories, the ceiling, the expiry and the collector,
// with the values of the stored request, before the buttons.
func TestASetupRequestShowsEveryLineOfTheCredentialBeforeApproval(t *testing.T) {
code := "3f9a81c207de" + strings.Repeat("0", 52)
push := renderSetup(t, meSignOff{ID: "abc", Agent: "zeitgeist", Summary: "Set up agent suit-push to push to dmestas/suit", State: "pending",
Requested: "2026-11-13T10:00:00Z", Pickup: code,
Setup: &meSetupView{Agent: "suit-push", Kind: "push", Belongs: "repository", Owner: "dmestas/suit", Repos: []string{"dmestas/suit"},
Role: "developer", RoleSentence: "Checks in, on any branch that is not protected.", Scopes: []string{"sync"},
TokenRepos: []string{"dmestas/suit"}, Ceiling: "uv3bBC78", Days: 0, Pickup: code}})
for _, c := range []struct{ hook, attr, want string }{
{"setup", "data-kind", "push"},
{"setup-agent", "data-agent", "suit-push"},
{"setup-agent", "data-belongs", "repository"},
{"setup-agent", "data-owner", "dmestas/suit"},
{"setup-repos", "data-repos", "dmestas/suit"},
{"setup-role", "data-role", "developer"},
{"setup-scope", "data-scopes", "sync"},
{"setup-reach", "data-repos", "dmestas/suit"},
{"setup-ceiling", "data-ceiling", "uv3bBC78"},
{"setup-expires", "data-days", "0"},
{"setup-collector", "data-pickup", code[:12]},
} {
if got, ok := hookAttr(t, push, c.hook, c.attr); !ok || got != c.want {
t.Errorf("FAIL: %s %s is %q (present %v), want %q", c.hook, c.attr, got, ok, c.want)
}
}
for _, hook := range []string{"setup-lapse", "setup-checkins", "signoff-approve", "signoff-deny"} {
if _, ok := hookAttr(t, push, hook, "data-hook"); !ok {
t.Errorf("FAIL: the page has no %s", hook)
}
}
// Every line is before the buttons.
if strings.Index(push, `data-hook="setup-collector"`) > strings.Index(push, `data-hook="signoff-approve"`) {
t.Errorf("FAIL: the credential's lines come after the buttons")
}
if strings.Contains(push, `data-hook="signoff-pickup"`) || strings.Contains(push, "qat_") {
t.Errorf("FAIL: the pickup line is drawn twice, or a value is on the page")
}
// A read client for two repositories, asked without a pickup code.
read := renderSetup(t, meSignOff{ID: "def", Agent: "zeitgeist", State: "pending", Requested: "2026-11-13T10:00:00Z",
Setup: &meSetupView{Agent: "e-desktop", Kind: "read", Belongs: "person", Owner: "dmestas", Repos: []string{"dmestas/suit", "dmestas/context"},
Role: "reader", RoleSentence: "Reads.", Scopes: []string{"api"}, TokenRepos: []string{"dmestas/suit", "dmestas/context"},
Ceiling: "gjorzh28", Days: 30}})
for _, c := range []struct{ hook, attr, want string }{
{"setup-agent", "data-belongs", "person"},
{"setup-repos", "data-repos", "dmestas/suit,dmestas/context"},
{"setup-scope", "data-scopes", "api"},
{"setup-reach", "data-repos", "dmestas/suit,dmestas/context"},
{"setup-ceiling", "data-ceiling", "gjorzh28"},
{"setup-expires", "data-days", "30"},
} {
if got, ok := hookAttr(t, read, c.hook, c.attr); !ok || got != c.want {
t.Errorf("FAIL: read: %s %s is %q (present %v), want %q", c.hook, c.attr, got, ok, c.want)
}
}
if got, ok := hookAttr(t, read, "setup-collector", "data-hook"); !ok || strings.Contains(read, `data-hook="setup-collector" data-pickup`) {
t.Errorf("FAIL: a request with no pickup code names a code (%q)", got)
}
if _, ok := hookAttr(t, read, "setup-checkins", "data-hook"); ok {
t.Errorf("FAIL: a read client's page talks of check-ins")
}
// After approval: the same lines, the outcome, no buttons.
done := renderSetup(t, meSignOff{ID: "abc", Agent: "zeitgeist", State: "approved", Requested: "2026-11-13T10:00:00Z", Decided: "2026-11-13T10:05:00Z", Pickup: code,
Setup: &meSetupView{Agent: "suit-push", Kind: "push", Belongs: "repository", Owner: "dmestas/suit", Repos: []string{"dmestas/suit"},
Role: "developer", RoleSentence: "Checks in.", Scopes: []string{"sync"}, TokenRepos: []string{"dmestas/suit"}, Ceiling: "uv3bBC78",
Pickup: code, Done: true, Token: "setup 4be19c02"}})
if got, _ := hookAttr(t, done, "setup", "data-done"); got != "true" {
t.Errorf("FAIL: an approved request is not marked done")
}
if got, ok := hookAttr(t, done, "setup-scope", "data-scopes"); !ok || got != "sync" {
t.Errorf("FAIL: the outcome has no scope")
}
if _, ok := hookAttr(t, done, "setup-token", "data-hook"); !ok || strings.Contains(done, "<form") {
t.Errorf("FAIL: the outcome names no token, or can still be answered")
}
// After a failure: what did not happen, and why.
failed := renderSetup(t, meSignOff{ID: "abc", Agent: "zeitgeist", State: "failed", Err: "suit-push is already an account.", Requested: "2026-11-13T10:00:00Z",
Setup: &meSetupView{Agent: "suit-push", Kind: "push", Repos: []string{"dmestas/suit"}, Role: "developer", Scopes: []string{"sync"},
TokenRepos: []string{"dmestas/suit"}, Ceiling: "uv3bBC78"}})
if _, ok := hookAttr(t, failed, "setup-undone", "data-hook"); !ok {
t.Errorf("FAIL: a failed setup does not say nothing was made")
}
if _, ok := hookAttr(t, failed, "signoff-err", "data-hook"); !ok || strings.Contains(failed, "<form") {
t.Errorf("FAIL: a failed setup hides the reason, or can still be answered")
}
// A failed setup that could not be fully undone says what remains,
// and one whose outcome was never recorded says so; neither claims
// that nothing was made.
for outcome, left := range map[string][]string{"left": {"the agent suit-push", "its token ABC"}, "unknown": nil} {
page := renderSetup(t, meSignOff{ID: "abc", Agent: "zeitgeist", State: "failed", Err: "disk I/O error", Requested: "2026-11-13T10:00:00Z",
Setup: &meSetupView{Agent: "suit-push", Kind: "push", Repos: []string{"dmestas/suit"}, Role: "developer", Scopes: []string{"sync"},
TokenRepos: []string{"dmestas/suit"}, Ceiling: "uv3bBC78", Outcome: outcome, Left: left}})
if got, ok := hookAttr(t, page, "setup-left", "data-outcome"); !ok || got != outcome {
t.Errorf("FAIL: outcome %s: hook says %q (present %v)", outcome, got, ok)
}
if _, ok := hookAttr(t, page, "setup-undone", "data-hook"); ok {
t.Errorf("FAIL: outcome %s still says nothing was made", outcome)
}
if outcome == "left" && !strings.Contains(page, "its token ABC") {
t.Errorf("FAIL: what remains is not named")
}
}
if _, ok := hookAttr(t, failed, "setup-lapse", "data-hook"); ok {
t.Errorf("FAIL: a failed setup talks of collection")
}
// A request that cannot run now says why, in an alert.
stuck := renderSetup(t, meSignOff{ID: "abc", Agent: "zeitgeist", State: "pending", Requested: "2026-11-13T10:00:00Z",
Setup: &meSetupView{Agent: "x", Kind: "read", Repos: []string{"a"}, Role: "reader", Scopes: []string{"api"}, TokenRepos: []string{"a"},
Ceiling: "gjorzh28", Problem: "No repository a."}})
if _, ok := hookAttr(t, stuck, "setup-problem", "role"); !ok {
t.Errorf("FAIL: a request that cannot run says nothing")
}
if !t.Failed() {
t.Log("ok")
}
}