Skip to content
quarry Artifact [079648bf12]
Login

Artifact [079648bf12]

Artifact 079648bf12baff433b83f5f71916dcfd8b6aa0893c35cb724d9b54a37ebe9b53:


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")
	}
}