Skip to content
quarry Artifact [28d5abf2c9]
Login

Artifact [28d5abf2c9]

Artifact 28d5abf2c9c0cb2cd0410ce12c29562c2af09b5ce40de2948a56ac94126acae0:


package api

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"

	"craftdesign.group/quarry/internal/cisecret"
	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
)

// setupRig is a forge with real letters in the store: dmestas (a site
// admin, admin of suit, ctx and the private secret), jva (a reader),
// their agents, a forge agent answerable to dmestas and a repository's
// own agent.
type setupRig struct {
	t    *testing.T
	db   *oltp.DB
	deps Deps
	r    *ops.Registry
	h    http.Handler
	rec  *settingRecorder
	coll *PickupCollector
	// blind makes the api-level reading gate say yes to everyone, so a
	// request can reach the store with a repository the approver lost.
	blind bool
	// failRefs and failFinish fail the stores that hold an approved
	// request's result, as a database error would.
	failRefs, failFinish bool
	// failRetract makes taking a setup back fail too.
	failRetract bool
	prov        *recProvision
}

// recProvision records the notify provisioning calls a setup makes.
type recProvision struct {
	mu    sync.Mutex
	calls []string
}

func (p *recProvision) note(c string) {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.calls = append(p.calls, c)
}
func (p *recProvision) EnsureSubscriber(_ context.Context, a string) error {
	p.note("ensure " + a)
	return nil
}
func (p *recProvision) RemoveSubscriber(_ context.Context, a string) error {
	p.note("remove " + a)
	return nil
}
func (p *recProvision) SetTopicMember(_ context.Context, repo, a string, m bool) error {
	p.note(fmt.Sprintf("topic %s %s %v", repo, a, m))
	return nil
}
func (p *recProvision) seen() []string {
	p.mu.Lock()
	defer p.mu.Unlock()
	return append([]string(nil), p.calls...)
}

// retractFailing is the account store whose RetractSetup can be made to
// fail.
type retractFailing struct {
	adminAdapter
	rig *setupRig
}

func (a retractFailing) RetractSetup(agent, tokenID string) error {
	if a.rig.failRetract {
		return errors.New("disk I/O error")
	}
	return a.adminAdapter.RetractSetup(agent, tokenID)
}

type setupSignOffs struct {
	SignOffStore
	rig *setupRig
}

func (s setupSignOffs) SetSignOffRefs(id, refs string) error {
	if s.rig.failRefs {
		s.rig.failRefs = false // one transient failure
		return errors.New("disk I/O error")
	}
	return s.SignOffStore.SetSignOffRefs(id, refs)
}

func (s setupSignOffs) FinishSignOff(id, result, secret string) error {
	if s.rig.failFinish {
		s.rig.failFinish = false // one transient failure
		return errors.New("disk I/O error")
	}
	return s.SignOffStore.FinishSignOff(id, result, secret)
}

func newSetupRig(t *testing.T, box ...cisecret.Box) *setupRig {
	t.Helper()
	db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	for _, who := range []string{"dmestas", "jva"} {
		if err := db.AddAccount(who, "a-long-enough-password", who == "dmestas"); err != nil {
			t.Fatal(err)
		}
	}
	repos := map[string]*Repo{}
	for _, name := range []string{"suit", "ctx", "secret"} {
		if err := db.AddRepo(name, t.TempDir(), "", oltp.Public); err != nil {
			t.Fatal(err)
		}
		repos[name] = &Repo{Index: wikiIndex(t), Code: "code-" + name}
		if err := db.Grant("dmestas", name, oltp.RepoAdmin); err != nil {
			t.Fatal(err)
		}
	}
	if err := db.SetRepoVisibility("secret", oltp.Private); err != nil {
		t.Fatal(err)
	}
	for agent, person := range map[string]string{"dmestas-bot": "dmestas", "jva-bot": "jva"} {
		if err := db.NewAgent(agent, person, -1); err != nil {
			t.Fatal(err)
		}
	}
	if err := db.NewForgeAgent("zeitgeist", "dmestas"); err != nil {
		t.Fatal(err)
	}
	if err := db.NewDedicatedAgent("suit-ci", "repo:suit", "dmestas"); err != nil {
		t.Fatal(err)
	}
	g := &setupRig{t: t, db: db, rec: &settingRecorder{}, prov: &recProvision{}}
	sbox := testBox(t)
	if len(box) > 0 {
		sbox = box[0]
	}
	holds := func(who, repo string) CapSet { return db.EffectiveCaps(who, repo) }
	canRead := func(ctx context.Context, repo string) bool {
		if g.blind {
			return true
		}
		if tok, ok := ops.TokenOf(ctx); ok && tok.Reaches != nil && !tok.Reaches(repo) {
			return false
		}
		return db.EffectiveCaps(ops.User(ctx), repo).HasAny("gjoh2r")
	}
	deps := Deps{Version: "v", SettingLog: g.rec, Provision: g.prov, SecretBox: sbox, Acts: db, Prefs: db, CapsFor: holds, CanRead: canRead,
		Origin: "https://forge.test", AgentsPerPerson: 5,
		Repos:    NewRepos(repos),
		Accounts: retractFailing{adminAdapter{acctAdapter{db}}, g}, SignOffs: setupSignOffs{testSignOffs{db}, g}}
	r := ops.NewRegistry()
	if err := RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	h, err := RouterWithCaps(r, "v", holds, nil)
	if err != nil {
		t.Fatal(err)
	}
	lim := &missLimiter{miss: map[string]int{}}
	g.deps, g.r, g.h, g.coll = deps, r, h, NewPickupCollector(deps, lim, nil)
	return g
}

// tokenCtx is a call on one of agent's tokens, limited to repos when any
// are named.
func tokenCtx(agent string, repos ...string) context.Context {
	tok := ops.TokenAuth{ID: "T", Caps: func(string) ops.Caps { return oltp.CapSet("") }}
	if len(repos) > 0 {
		tok.Limited = true
		tok.Reaches = func(r string) bool {
			for _, have := range repos {
				if have == r {
					return true
				}
			}
			return false
		}
	}
	return ops.WithToken(ops.WithUser(context.Background(), agent), tok)
}

// post sends body to path as ctx's caller; ask adds Quarry-Ask and code
// adds Quarry-Pickup.
func (g *setupRig) post(ctx context.Context, body string, ask bool, code string) (int, string) {
	g.t.Helper()
	req := httptest.NewRequest("POST", "/api/v1/agents/setup", strings.NewReader(body)).WithContext(ctx)
	req.Header.Set("Content-Type", "application/json")
	if ask {
		req.Header.Set("Quarry-Ask", "suit needs a pushing agent for its CI")
	}
	if code != "" {
		req.Header.Set("Quarry-Pickup", code)
	}
	w := httptest.NewRecorder()
	g.h.ServeHTTP(w, req)
	return w.Code, w.Body.String()
}

// ask files a setup request as agent and returns its id; the request
// must have been taken.
func (g *setupRig) ask(agent, body, code string, repos ...string) string {
	g.t.Helper()
	status, out := g.post(tokenCtx(agent, repos...), body, true, code)
	var asked struct {
		SignOff SignOffRequest `json:"signoff"`
	}
	if err := json.Unmarshal([]byte(out), &asked); err != nil || status != 202 || asked.SignOff.ID == "" {
		g.t.Fatalf("FAIL: asking: %d", status)
	}
	return asked.SignOff.ID
}

func (g *setupRig) decide(person, id string, approve bool) error {
	g.t.Helper()
	_, err := g.r.Invoke(asUser(person), "signoff.decide", signOffDecideIn{ID: id, Approve: approve})
	return err
}

func (g *setupRig) get(who, id string) signOffGetOut {
	g.t.Helper()
	out, err := g.r.Invoke(asUser(who), "signoff.get", signOffGetIn{ID: id})
	if err != nil {
		g.t.Fatal(err)
	}
	return out.(signOffGetOut)
}

// nothingMade fails the test when anything a setup writes exists under
// name: the account, its grants, its tokens, or a retired name.
func (g *setupRig) nothingMade(name string) {
	g.t.Helper()
	if _, ok := g.db.AccountByName(name); ok {
		g.t.Errorf("FAIL: an account row for %s exists", name)
	}
	if grants, _ := g.db.GrantsFor(name); len(grants) != 0 {
		g.t.Errorf("FAIL: %s holds grants: %v", name, grants)
	}
	if toks, _ := g.db.Tokens(name); len(toks) != 0 {
		g.t.Errorf("FAIL: %s holds %d tokens", name, len(toks))
	}
	if g.db.Tombstoned(name) {
		g.t.Errorf("FAIL: the name %s was retired", name)
	}
}

const pushBody = `{"name":"suit-push","kind":"push","repos":"suit"}`

// RFC 0030 step 3: an approved request yields an agent, a grant and a
// token whose scopes are sync and whose repositories are the one named;
// the value is in the secret half, never the public answer, and the
// agent's own signoff.get collects it once.
func TestAnApprovedPushSetupYieldsAnAgentAGrantAndANarrowToken(t *testing.T) {
	g := newSetupRig(t)
	id := g.ask("zeitgeist", pushBody, "")
	if _, ok := g.db.AccountByName("suit-push"); ok {
		t.Fatalf("FAIL: asking made the agent")
	}
	if err := g.decide("dmestas", id, true); err != nil {
		t.Fatal(err)
	}
	a, ok := g.db.AccountByName("suit-push")
	if !ok || a.Operator != "repo:suit" || a.Answerable != "dmestas" {
		t.Fatalf("FAIL: the agent: %+v", a)
	}
	if stored, _ := g.db.CapsOf("suit-push", "suit"); stored != oltp.Write.Caps() {
		t.Errorf("FAIL: the grant is %q", stored)
	}
	toks, _ := g.db.Tokens("suit-push")
	if len(toks) != 1 || strings.Join(toks[0].Scopes, ",") != "sync" || strings.Join(toks[0].Repos, ",") != "suit" ||
		toks[0].Ceiling != oltp.Write.Caps() || toks[0].Expires != "" || !strings.HasPrefix(toks[0].Name, "setup ") {
		t.Fatalf("FAIL: the token: %+v", toks)
	}
	got := g.get("zeitgeist", id)
	if strings.Contains(string(got.Result), "qat_") || got.Setup == nil || !got.Setup.Done {
		t.Errorf("FAIL: the public half holds a value, or no setup view")
	}
	var sec struct{ Value string }
	if err := json.Unmarshal(got.Secret, &sec); err != nil || !strings.HasPrefix(sec.Value, "qat_") {
		t.Fatalf("FAIL: the asker's first read lacks the value")
	}
	if again := g.get("zeitgeist", id); len(again.Secret) != 0 {
		t.Errorf("FAIL: the value was released twice")
	}
	var res agentSetupOut
	if err := json.Unmarshal(got.Result, &res); err != nil || len(res.Remotes) != 1 || res.Remotes[0].ProjectCode != "code-suit" ||
		res.Remotes[0].URL != "https://forge.test/r/suit" || res.Token.ID != toks[0].ID {
		t.Errorf("FAIL: the answer: %v %+v", err, res)
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// A read client is the approver's own agent, api scope only, named
// repositories, the read-only ceiling, and granted reader on each; none
// of its token can push.
func TestAReadSetupIsTheApproversOwnAgentWithTheReadCeiling(t *testing.T) {
	g := newSetupRig(t)
	id := g.ask("dmestas-bot", `{"name":"e-desktop","kind":"read","repos":"suit,secret"}`, "")
	if err := g.decide("dmestas", id, true); err != nil {
		t.Fatal(err)
	}
	a, _ := g.db.AccountByName("e-desktop")
	if a.Operator != "dmestas" || a.Inherit || a.Scope != "" {
		t.Errorf("FAIL: the client: operator=%q inherit=%v scope=%q", a.Operator, a.Inherit, a.Scope)
	}
	toks, _ := g.db.Tokens("e-desktop")
	if len(toks) != 1 || strings.Join(toks[0].Scopes, ",") != "api" || strings.Join(toks[0].Repos, ",") != "suit,secret" ||
		toks[0].Ceiling != oltp.SetupReadCeiling {
		t.Fatalf("FAIL: the token: %+v", toks)
	}
	for _, repo := range []string{"suit", "secret"} {
		if eff := g.db.EffectiveCaps("e-desktop", repo); !eff.HasAny("o") || eff.HasAny("i") || eff.HasAny("a") {
			t.Errorf("FAIL: the client holds the wrong letters on %s", repo)
		}
	}
	if tc := g.db.TokenCaps(toks[0], "suit"); tc.HasAny("i") || tc.HasAny("n") || !tc.HasAny("8") {
		t.Errorf("FAIL: the token can write or cannot see CI")
	}
	// A client of one repository is scoped to it.
	id = g.ask("dmestas-bot", `{"name":"e-one","kind":"read","repos":"suit"}`, "")
	if err := g.decide("dmestas", id, true); err != nil {
		t.Fatal(err)
	}
	if a, _ := g.db.AccountByName("e-one"); a.Scope != "repo:suit" {
		t.Errorf("FAIL: a one-repository client is scoped %q", a.Scope)
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// A pushing agent belongs to the approver, scoped to the repository,
// when the approver does not administer it; the role must be theirs to
// give, and the owner's own letters bound the grant.
func TestAPushSetupBelongsToTheApproverWhenTheyAdministerNothingThere(t *testing.T) {
	g := newSetupRig(t)
	if err := g.db.Grant("jva", "ctx", oltp.Write); err != nil {
		t.Fatal(err)
	}
	id := g.ask("jva-bot", `{"name":"ctx-push","kind":"push","repos":"ctx"}`, "")
	if err := g.decide("jva", id, true); err != nil {
		t.Fatal(err)
	}
	a, _ := g.db.AccountByName("ctx-push")
	if a.Operator != "jva" || a.Scope != "repo:ctx" || a.Inherit {
		t.Errorf("FAIL: the agent: operator=%q scope=%q inherit=%v", a.Operator, a.Scope, a.Inherit)
	}
	// jva holds only developer on suit's neighbour; a maintainer role
	// would be cut, so it is refused when asked.
	status, _ := g.post(tokenCtx("jva-bot"), `{"name":"ctx-maint","kind":"push","repos":"ctx","role":"maintainer"}`, true, "")
	if status != 403 {
		t.Errorf("FAIL: a role the approver cannot give was asked: %d", status)
	}
	g.nothingMade("ctx-maint")
	// And belongs_to=repository where jva holds no admin letter is refused.
	if status, _ := g.post(tokenCtx("jva-bot"), `{"name":"ctx-repo","kind":"push","repos":"ctx","belongs_to":"repository"}`, true, ""); status != 403 {
		t.Errorf("FAIL: a repository agent without the admin letter: %d", status)
	}
	// belongs_to=person on a repository dmestas administers makes the
	// agent dmestas's own.
	id = g.ask("zeitgeist", `{"name":"suit-mine","kind":"push","repos":"suit","belongs_to":"person"}`, "")
	if err := g.decide("dmestas", id, true); err != nil {
		t.Fatal(err)
	}
	if a, _ := g.db.AccountByName("suit-mine"); a.Operator != "dmestas" || a.Scope != "repo:suit" {
		t.Errorf("FAIL: belongs_to person: %+v", a)
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// Who may ask: an agent its person operates and the forge agent; a
// person's token, a repository's own agent and an agent without
// Quarry-Ask are refused as any credential op refuses a token.
func TestOnlyAnAgentWithAPersonMayAskForSetup(t *testing.T) {
	g := newSetupRig(t)
	for _, agent := range []string{"zeitgeist", "dmestas-bot"} {
		if status, _ := g.post(tokenCtx(agent), `{"name":"n-`+agent+`","kind":"push","repos":"suit"}`, true, ""); status != 202 {
			t.Errorf("FAIL: %s could not ask: %d", agent, status)
		}
	}
	waiting := func() int {
		rows, _ := g.db.SignOffsFor("dmestas", 50)
		return len(rows)
	}
	before := waiting()
	refused := map[string]context.Context{
		"a person's token":          tokenCtx("dmestas"),
		"a repository's own agent":  tokenCtx("suit-ci"),
		"another person's agent":    tokenCtx("jva-bot"),
		"an agent for a stranger":   tokenCtx("nobody-at-all"),
		"a token limited elsewhere": tokenCtx("dmestas-bot", "ctx"),
	}
	for name, ctx := range refused {
		if status, _ := g.post(ctx, `{"name":"x-`+strings.ReplaceAll(name, " ", "")+`","kind":"push","repos":"suit"}`, true, ""); status != 403 && status != 401 {
			t.Errorf("FAIL: %s got %d, want a refusal", name, status)
		}
	}
	if waiting() != before {
		t.Errorf("FAIL: a refused asker left a request")
	}
	// A token limited to the repository it names may ask.
	if status, _ := g.post(tokenCtx("dmestas-bot", "suit"), `{"name":"n-limited","kind":"push","repos":"suit"}`, true, ""); status != 202 {
		t.Errorf("FAIL: a token limited to suit could not ask for suit: %d", status)
	}
	// Without Quarry-Ask, the same refusal agent.token.new gives.
	st, body := g.post(tokenCtx("dmestas-bot"), pushBody, false, "")
	st2, body2 := func() (int, string) {
		req := httptest.NewRequest("POST", "/api/v1/agents/tokens", strings.NewReader(`{"agent":"x","name":"n","scopes":"sync"}`)).WithContext(tokenCtx("dmestas-bot"))
		req.Header.Set("Content-Type", "application/json")
		w := httptest.NewRecorder()
		g.h.ServeHTTP(w, req)
		return w.Code, w.Body.String()
	}()
	if st != st2 || st != 403 || !strings.Contains(body, "needs the account itself") || !strings.Contains(body2, "needs the account itself") {
		t.Errorf("FAIL: the plain call: %d %s vs %d %s", st, body, st2, body2)
	}
	// And no one may run it, a session included: it is only approved.
	if _, err := g.r.Invoke(asUser("dmestas"), setupOp, agentSetupIn{Name: "direct", Kind: "push", Repos: "suit"}); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: a session ran the setup directly: %v", err)
	}
	g.nothingMade("direct")
	if !t.Failed() {
		t.Log("ok")
	}
}

// Asking is judged as the owner would run it, and says no more than
// refusing: a name that exists, a repository the asker cannot read, one
// that is not there, and one the owner does not reach are each the
// plain refusal, and a repository the asker cannot read answers exactly
// as a missing one.
func TestASetupTheOwnerCouldNotRunIsRefusedWhenAsked(t *testing.T) {
	g := newSetupRig(t)
	count := func() int { rows, _ := g.db.SignOffsFor("jva", 50); return len(rows) }
	ask := func(body string) (int, string) { return g.post(tokenCtx("jva-bot"), body, true, "") }
	// The account names a person: taken.
	taken, _ := ask(`{"name":"dmestas","kind":"read","repos":"suit"}`)
	// A repository only dmestas reaches, and one nobody made.
	hidden, hiddenBody := ask(`{"name":"e-hid","kind":"read","repos":"secret"}`)
	missing, missingBody := ask(`{"name":"e-hid","kind":"read","repos":"nonesuch"}`)
	if hidden != 403 || missing != 403 || taken != 403 {
		t.Errorf("FAIL: asked anyway: taken=%d hidden=%d missing=%d", taken, hidden, missing)
	}
	if hiddenBody != missingBody {
		t.Errorf("FAIL: a repository the asker cannot read answered differently from a missing one:\n%s\n%s", hiddenBody, missingBody)
	}
	// Read and role shape, said in words: admin is never asked for.
	st, body := g.post(tokenCtx("dmestas-bot"), `{"name":"adm","kind":"push","repos":"suit","role":"admin"}`, true, "")
	if st != 400 && st != 422 || !strings.Contains(body, "admin") {
		t.Errorf("FAIL: admin was not refused in words: %d %s", st, body)
	}
	for name, body := range map[string]string{
		"two pushed repositories":  `{"name":"p2","kind":"push","repos":"suit,ctx"}`,
		"a role on a read client":  `{"name":"r1","kind":"read","repos":"suit","role":"developer"}`,
		"a kind that is not one":   `{"name":"k1","kind":"admin","repos":"suit"}`,
		"twenty-one repositories":  `{"name":"r21","kind":"read","repos":"a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,b0,b1,b2,b3,b4,b5,b6,b7,b8,b9,c0"}`,
		"a repository belonging":   `{"name":"rb","kind":"read","repos":"suit","belongs_to":"repository"}`,
		"a role that is not there": `{"name":"rr","kind":"push","repos":"suit","role":"king"}`,
	} {
		if st, _ := g.post(tokenCtx("dmestas-bot"), body, true, ""); st != 400 && st != 422 {
			t.Errorf("FAIL: %s was not refused as a bad request: %d", name, st)
		}
	}
	// A name that is an agent somebody else operates: the plain refusal.
	if st, _ := g.post(tokenCtx("dmestas-bot"), `{"name":"jva-bot","kind":"read","repos":"suit"}`, true, ""); st != 403 {
		t.Errorf("FAIL: somebody else's agent's name was taken: %d", st)
	}
	// Setup makes new agents only: the owner's own agent's name, and a
	// person's name in another case, are the plain refusal.
	for _, name := range []string{"dmestas-bot", "Dmestas-Bot", "DMestas"} {
		if st, _ := g.post(tokenCtx("dmestas-bot"), `{"name":"`+name+`","kind":"read","repos":"suit"}`, true, ""); st != 403 {
			t.Errorf("FAIL: the existing account %s was taken: %d", name, st)
		}
	}
	if count() != 0 {
		t.Errorf("FAIL: a refused ask left requests")
	}
	g.nothingMade("e-hid")
	if !t.Failed() {
		t.Log("ok")
	}
}

// The transaction does not start when the name was taken or a
// repository changed between asking and approving; the request reads
// failed with the reason and nothing was made.
func TestASetupWhoseNameWasTakenAfterAskingFailsWholeAndSaysSo(t *testing.T) {
	g := newSetupRig(t)
	id := g.ask("zeitgeist", pushBody, "")
	if err := g.db.AddAccount("suit-push", "a-long-enough-password", false); err != nil {
		t.Fatal(err)
	}
	if err := g.decide("dmestas", id, true); err == nil {
		t.Fatalf("FAIL: approving a request whose name was taken succeeded")
	}
	got := g.get("dmestas", id)
	if got.Request.State != "failed" || got.Request.Err == "" || got.Setup == nil || got.Setup.Done {
		t.Errorf("FAIL: the request: %+v", got.Request)
	}
	if grants, _ := g.db.GrantsFor("suit-push"); len(grants) != 0 {
		t.Errorf("FAIL: grants were written")
	}
	if toks, _ := g.db.Tokens("suit-push"); len(toks) != 0 {
		t.Errorf("FAIL: a token was minted")
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// RFC 0030 step 3: a forced grant failure leaves no account row. The
// approver loses the second repository between asking and approving
// while the reading gate in front is blind, so the failure comes from
// the store's own grant, after the account row and the first grant were
// written.
func TestAForcedGrantFailureLeavesNoAccountRow(t *testing.T) {
	g := newSetupRig(t)
	id := g.ask("dmestas-bot", `{"name":"e-desktop","kind":"read","repos":"suit,secret"}`, "")
	if err := g.db.Revoke("dmestas", "secret"); err != nil {
		t.Fatal(err)
	}
	g.blind = true
	err := g.decide("dmestas", id, true)
	g.blind = false
	if err == nil {
		t.Fatalf("FAIL: the grant on a repository the approver lost succeeded")
	}
	g.nothingMade("e-desktop")
	if got := g.get("dmestas", id); got.Request.State != "failed" {
		t.Errorf("FAIL: the request is %q", got.Request.State)
	}
	// Nothing remains, so the same request can be asked and approved once
	// the repository is reachable again.
	if err := g.db.Grant("dmestas", "secret", oltp.RepoAdmin); err != nil {
		t.Fatal(err)
	}
	id = g.ask("dmestas-bot", `{"name":"e-desktop","kind":"read","repos":"suit,secret"}`, "")
	if err := g.decide("dmestas", id, true); err != nil {
		t.Errorf("FAIL: a retry after the failure: %v", err)
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// A forge with no secret key cannot hold the value: refused before
// anything is made.
func TestASetupOnAForgeWithNoSecretKeyMakesNothing(t *testing.T) {
	g := newSetupRig(t, cisecret.Box{})
	id := g.ask("zeitgeist", pushBody, "")
	if err := g.decide("dmestas", id, true); err == nil {
		t.Fatalf("FAIL: approved with nothing to seal with")
	}
	g.nothingMade("suit-push")
	if !t.Failed() {
		t.Log("ok")
	}
}

// Failing to hold the result after the commit takes the setup back
// whole: no account row, no grant, no token, the name not retired, and
// nothing outward ever went out (no log row, receipt or topic
// membership), at each of the two writes that hold it.
func TestAResultThatCannotBeHeldTakesTheSetupBackWithNothingOutward(t *testing.T) {
	for name, set := range map[string]func(*setupRig){
		"the lapse references": func(g *setupRig) { g.failRefs = true },
		"the sealed result":    func(g *setupRig) { g.failFinish = true },
	} {
		t.Run(name, func(t *testing.T) {
			g := newSetupRig(t)
			id := g.ask("zeitgeist", pushBody, "")
			set(g)
			if err := g.decide("dmestas", id, true); err == nil {
				t.Fatalf("FAIL: approved though its result could not be held")
			}
			g.failRefs, g.failFinish = false, false
			g.nothingMade("suit-push")
			if calls := g.prov.seen(); len(calls) != 0 {
				t.Errorf("FAIL: provisioning went out: %v", calls)
			}
			for _, row := range g.rec.rows {
				if row.field == "agent-setup" {
					t.Errorf("FAIL: a settings-log row was written for a setup that was undone")
				}
			}
			acts, _ := g.db.ForgeActs("", 100)
			for _, a := range acts {
				if a.Op == setupOp {
					t.Errorf("FAIL: a receipt was written for a setup that was undone")
				}
			}
			got := g.get("dmestas", id)
			if got.Request.State != "failed" || got.Setup == nil || got.Setup.Outcome != setupUndone {
				t.Errorf("FAIL: the page's truth: %q %+v", got.Request.State, got.Setup)
			}
			if !t.Failed() {
				t.Log("ok")
			}
		})
	}
}

// When even the retract fails, the request says what remains, and the
// outcome is not "nothing was made".
func TestASetupThatCannotBeTakenBackSaysWhatRemains(t *testing.T) {
	g := newSetupRig(t)
	id := g.ask("zeitgeist", pushBody, "")
	g.failFinish, g.failRetract = true, true
	if err := g.decide("dmestas", id, true); err == nil {
		t.Fatalf("FAIL: approved though its result could not be held")
	}
	g.failFinish, g.failRetract = false, false
	if _, ok := g.db.AccountByName("suit-push"); !ok {
		t.Fatalf("FAIL: the test did not leave the agent behind")
	}
	got := g.get("dmestas", id)
	if got.Request.State != "failed" || got.Setup == nil || got.Setup.Outcome != setupLeft ||
		len(got.Setup.Left) != 2 || !strings.Contains(got.Setup.Left[0], "suit-push") {
		t.Errorf("FAIL: what remains is not told: %q %+v", got.Request.State, got.Setup)
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// A setup refused inside the transaction (the name was taken since
// asking) says nothing was made, as an outcome on the request.
func TestARefusedSetupRecordsThatNothingWasMade(t *testing.T) {
	g := newSetupRig(t)
	id := g.ask("zeitgeist", pushBody, "")
	if err := g.db.AddAccount("Suit-Push", "a-long-enough-password", false); err != nil {
		t.Fatal(err)
	}
	if err := g.decide("dmestas", id, true); err == nil {
		t.Fatalf("FAIL: approved over a name taken in another case")
	}
	if v := g.get("dmestas", id).Setup; v == nil || v.Outcome != setupUndone {
		t.Errorf("FAIL: the outcome: %+v", v)
	}
	if _, ok := g.db.AccountByName("suit-push"); ok {
		t.Errorf("FAIL: the agent was made")
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// What the owner is shown is decided once, when the request is filed,
// and stored in it: who the agent belongs to and the ceiling. If the
// approver can no longer make that choice, approval refuses rather than
// falling back to the other.
func TestASetupRunsWhatWasStoredAtAskTime(t *testing.T) {
	g := newSetupRig(t)
	id := g.ask("zeitgeist", pushBody, "")
	row, _, _ := g.db.SignOffByID(id)
	var in agentSetupIn
	if err := json.Unmarshal([]byte(row.Input), &in); err != nil || in.BelongsTo != "repository" || in.Ceiling != oltp.Write.Caps() || in.Role != "developer" {
		t.Fatalf("FAIL: the stored request: %v %+v", err, in)
	}
	if v := g.get("dmestas", id).Setup; v == nil || v.Belongs != "repository" || v.Owner != "suit" || v.Ceiling != oltp.Write.Caps() {
		t.Errorf("FAIL: the page does not show the stored values: %+v", v)
	}
	// The approver loses the admin letter on suit before approving.
	if err := g.db.Grant("dmestas", "suit", oltp.Write); err != nil {
		t.Fatal(err)
	}
	if v := g.get("dmestas", id).Setup; v == nil || v.Belongs != "repository" || v.Problem == "" {
		t.Errorf("FAIL: the page changed what it says, or hides that the request cannot run: %+v", v)
	}
	if err := g.decide("dmestas", id, true); err == nil {
		t.Fatalf("FAIL: approved a repository agent without the admin letter")
	}
	g.nothingMade("suit-push")
	// An asker cannot choose the ceiling.
	if st, _ := g.post(tokenCtx("zeitgeist"), `{"name":"x-wide","kind":"push","repos":"ctx","ceiling":"a"}`, true, ""); st != 400 && st != 422 {
		t.Errorf("FAIL: a chosen ceiling was taken: %d", st)
	}
	// A request whose stored ceiling no longer matches the role is refused.
	if _, err := g.deps.planSetup("dmestas", agentSetupIn{Name: "x-stale", Kind: "push", Repos: "ctx", Role: "developer", BelongsTo: "repository", Ceiling: "a"}); err == nil {
		t.Errorf("FAIL: a stale ceiling was run")
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// Each allowed push role is shown with its letters and a sentence, and
// mints exactly those letters.
func TestEachAllowedPushRoleIsShownAndMintedWithItsLetters(t *testing.T) {
	g := newSetupRig(t)
	// Admin does not carry y, so publishing is its own grant (RFC 0021);
	// an approver without it is refused when asking for a publisher.
	if st, _ := g.post(tokenCtx("zeitgeist"), `{"name":"r-nope","kind":"push","repos":"suit","role":"publisher"}`, true, ""); st != 403 {
		t.Errorf("FAIL: a publisher was asked for by an approver without y: %d", st)
	}
	if err := g.db.SetCaps("dmestas", "suit", "ay"); err != nil {
		t.Fatal(err)
	}
	// Each role has a sentence of its own; what it says is not pinned.
	sentences := map[string]string{}
	for role, preset := range map[string]oltp.Role{"developer": oltp.Write, "publisher": oltp.Publisher, "maintainer": oltp.Maintain} {
		name := "r-" + role
		id := g.ask("zeitgeist", `{"name":"`+name+`","kind":"push","repos":"suit","role":"`+role+`"}`, "")
		v := g.get("dmestas", id).Setup
		if v == nil || v.Role != role || v.Ceiling != preset.Caps() || v.RoleSentence == "" || v.RoleSentence == setupReadSent {
			t.Errorf("FAIL: the view of %s: %+v", role, v)
			continue
		}
		if sentences[v.RoleSentence] != "" {
			t.Errorf("FAIL: %s is described by the sentence %s has", role, sentences[v.RoleSentence])
		}
		sentences[v.RoleSentence] = role
		if err := g.decide("dmestas", id, true); err != nil {
			t.Errorf("FAIL: approving %s: %v", role, err)
			continue
		}
		if toks, _ := g.db.Tokens(name); len(toks) != 1 || toks[0].Ceiling != preset.Caps() {
			t.Errorf("FAIL: the token of %s: %+v", role, toks)
		}
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// RFC-level hole: an op with a preflight is judged at ask time on a
// plan step as on its own call, as the asker. Each preflight has a
// refused input listed here; one without an entry fails by name. And
// agent.setup is not a plan step at all.
func TestEveryPreflightIsEnforcedForAPlanStepToo(t *testing.T) {
	g := newSetupRig(t)
	refused := map[string]string{
		// jva-bot cannot read secret, and jva-bot is nobody's grantee there.
		"agent.grant": `{"agent":"jva-bot","repo":"secret","role":"reader"}`,
	}
	filed := func() int { rows, _ := g.db.SignOffsFor("jva", 50); return len(rows) }
	plan := func(agent, op, input string) (int, string) {
		body := `{"title":"t","steps":[{"op":"` + op + `","input":` + input + `}]}`
		req := httptest.NewRequest("POST", "/api/v1/requests", strings.NewReader(body)).WithContext(tokenCtx(agent))
		req.Header.Set("Content-Type", "application/json")
		w := httptest.NewRecorder()
		g.h.ServeHTTP(w, req)
		return w.Code, w.Body.String()
	}
	for op := range preflights {
		if op == setupOp {
			continue
		}
		input, ok := refused[op]
		if !ok {
			t.Errorf("FAIL: %s has a preflight but this test has no refused input for it", op)
			continue
		}
		// The same call asked on its own is the plain refusal.
		direct := httptest.NewRequest("POST", "/api/v1/agents/grant", strings.NewReader(input)).WithContext(tokenCtx("jva-bot"))
		direct.Header.Set("Content-Type", "application/json")
		direct.Header.Set("Quarry-Ask", "x")
		w := httptest.NewRecorder()
		g.h.ServeHTTP(w, direct)
		if w.Code == 202 {
			t.Errorf("FAIL: %s asked on its own was filed", op)
		}
		if st, body := plan("jva-bot", op, input); st == 202 || filed() != 0 {
			t.Errorf("FAIL: %s was filed as a plan step: %d %s", op, st, body)
		}
	}
	// The reviewer's probe: a scoped agent plans a setup on a private
	// repository it cannot read, and a setup with role admin.
	for name, input := range map[string]string{
		"a private repository": `{"name":"spy","kind":"push","repos":"secret","role":"maintainer"}`,
		"role admin":           `{"name":"adm","kind":"push","repos":"suit","role":"admin"}`,
		"a reachable one":      `{"name":"fine","kind":"push","repos":"suit"}`,
	} {
		if st, _ := plan("dmestas-bot", setupOp, input); st != 400 && st != 403 {
			t.Errorf("FAIL: setup as a plan step (%s) answered %d", name, st)
		}
	}
	rows, _ := g.db.SignOffsFor("dmestas", 50)
	if len(rows) != 0 {
		t.Errorf("FAIL: a plan with a setup step was filed")
	}
	g.nothingMade("spy")
	if !t.Failed() {
		t.Log("ok")
	}
}

// Bound to a pickup code, the secret is collected once by the key and
// never returned by signoff.get, to the asker or the owner.
func TestASetupBoundToAPickupCodeIsCollectedOnceByTheKey(t *testing.T) {
	g := newSetupRig(t)
	key, code := newPickupKey(t)
	id := g.ask("zeitgeist", pushBody, code)
	if st, _ := g.collectNow(key); st != 202 {
		t.Errorf("FAIL: collecting before approval: %d", st)
	}
	if err := g.decide("dmestas", id, true); err != nil {
		t.Fatal(err)
	}
	for _, who := range []string{"zeitgeist", "dmestas"} {
		if got := g.get(who, id); len(got.Secret) != 0 {
			t.Errorf("FAIL: signoff.get gave %s the secret of a pickup-bound request", who)
		}
	}
	st, body := g.collectNow(key)
	var got struct {
		Op     string
		Result agentSetupOut
		Secret struct{ Value string }
	}
	if err := json.Unmarshal([]byte(body), &got); st != 200 || err != nil || got.Op != setupOp || !strings.HasPrefix(got.Secret.Value, "qat_") || got.Result.Agent != "suit-push" {
		t.Fatalf("FAIL: collecting: %d %v", st, err)
	}
	// The collected value is the token's own.
	if tok, ok := g.db.ResolveToken("suit-push", got.Secret.Value, "sync"); !ok || tok.Account != "suit-push" {
		t.Errorf("FAIL: the collected value is not the minted token")
	}
	if st, _ := g.collectNow(key); st != 410 {
		t.Errorf("FAIL: a second collect answered %d", st)
	}
	if view := g.get("dmestas", id).Setup; view == nil || view.Pickup != code {
		t.Errorf("FAIL: the page's collector is not the pickup code")
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

func (g *setupRig) collectNow(key string) (int, string) {
	req := httptest.NewRequest("POST", "/api/v1/pickups/collect", nil)
	req.Header.Set("Authorization", "Pickup "+key)
	req.RemoteAddr = "192.0.2.9:4000"
	w := httptest.NewRecorder()
	g.coll.ServeHTTP(w, req)
	return w.Code, w.Body.String()
}

// Uncollected, the token is revoked after 24 hours, the agent stays, and
// the receipt says so exactly once; a second sweep says nothing.
func TestAnUncollectedSetupRevokesTheTokenAndKeepsTheAgent(t *testing.T) {
	g := newSetupRig(t)
	id := g.ask("zeitgeist", pushBody, "")
	if err := g.decide("dmestas", id, true); err != nil {
		t.Fatal(err)
	}
	row, _, _ := g.db.SignOffByID(id)
	if row.Pickup != "" {
		t.Fatalf("FAIL: an unbound request holds a pickup code")
	}
	receipts := func() (n int, last string) {
		acts, _ := g.db.ForgeActs("", 100)
		for _, a := range acts {
			if a.Op == "agent.token.revoke" && a.Owner == "suit-push" {
				n++
				last = a.Actor
			}
		}
		return n, last
	}
	if n, _ := receipts(); n != 0 {
		t.Fatalf("FAIL: a revoke before the lapse")
	}
	if n, err := g.deps.LapseSignOffSecrets(time.Now().Add(25 * time.Hour)); err != nil || n != 1 {
		t.Fatalf("FAIL: the sweep: %d %v", n, err)
	}
	toks, _ := g.db.Tokens("suit-push")
	if len(toks) != 1 || toks[0].Revoked == "" {
		t.Errorf("FAIL: the token was not revoked: %+v", toks)
	}
	if _, ok := g.db.AccountByName("suit-push"); !ok {
		t.Errorf("FAIL: the agent was deleted")
	}
	if stored, _ := g.db.CapsOf("suit-push", "suit"); stored == "" {
		t.Errorf("FAIL: the grant was removed")
	}
	if n, last := receipts(); n != 1 || !strings.Contains(last, "suit-push stays") || !strings.Contains(last, id) {
		t.Errorf("FAIL: the lapse receipt: %d %q", n, last)
	}
	if _, err := g.deps.LapseSignOffSecrets(time.Now().Add(26 * time.Hour)); err != nil {
		t.Fatal(err)
	}
	if n, _ := receipts(); n != 1 {
		t.Errorf("FAIL: a second sweep wrote a second row: %d", n)
	}
	if got := g.get("zeitgeist", id); len(got.Secret) != 0 || got.Note == "" {
		t.Errorf("FAIL: a lapsed value was still released, or no note")
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// The settings log and the agent's receipt record the creation, the
// grant and the mint with the approver as actor and the asker and the
// request named; never the value.
func TestASetupIsLoggedWithTheApproverAsActorAndNoValue(t *testing.T) {
	g := newSetupRig(t)
	id := g.ask("zeitgeist", pushBody, "")
	if err := g.decide("dmestas", id, true); err != nil {
		t.Fatal(err)
	}
	var rows []logRow
	for _, row := range g.rec.rows {
		if row.field == "agent-setup" {
			rows = append(rows, row)
		}
	}
	if len(rows) != 1 || rows[0].repo != "suit" || rows[0].actor != "dmestas" ||
		!strings.Contains(rows[0].now, "zeitgeist") || !strings.Contains(rows[0].now, id) || !strings.Contains(rows[0].now, "suit-push") {
		t.Fatalf("FAIL: the settings log: %+v", rows)
	}
	if strings.Contains(rows[0].now, "qat_") {
		t.Errorf("FAIL: the log row holds a value")
	}
	var receipts []oltp.ForgeAct
	acts, _ := g.db.ForgeActs("", 100)
	for _, a := range acts {
		if a.Op == setupOp && a.Owner == "suit-push" {
			receipts = append(receipts, a)
		}
	}
	if len(receipts) != 1 || !strings.Contains(receipts[0].Actor, "dmestas") || !strings.Contains(receipts[0].Actor, "zeitgeist") ||
		!strings.Contains(receipts[0].Actor, id) || strings.Contains(receipts[0].Actor, "qat_") {
		t.Errorf("FAIL: the agent's receipt: %+v", receipts)
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// RFC 0030 step 4's data: the view carries each line of the request
// from the stored input, before and after approval.
func TestTheSetupViewCarriesEachLineOfTheStoredRequest(t *testing.T) {
	g := newSetupRig(t)
	_, code := newPickupKey(t)
	id := g.ask("zeitgeist", `{"name":"suit-push","kind":"push","repos":"suit","role":"maintainer","days":30}`, code)
	v := g.get("dmestas", id).Setup
	if v == nil || v.Kind != "push" || strings.Join(v.Scopes, ",") != "sync" || strings.Join(v.Repos, ",") != "suit" ||
		strings.Join(v.TokenRepos, ",") != "suit" || v.Ceiling != oltp.Maintain.Caps() || v.Days != 30 || v.Pickup != code ||
		v.Role != "maintainer" || v.Belongs != "repository" || v.Owner != "suit" || v.Done || v.Problem != "" {
		t.Fatalf("FAIL: the view before approval: %+v", v)
	}
	if err := g.decide("dmestas", id, true); err != nil {
		t.Fatal(err)
	}
	if v = g.get("dmestas", id).Setup; v == nil || !v.Done || v.Owner != "suit" || strings.Join(v.Scopes, ",") != "sync" || v.Token == "" {
		t.Errorf("FAIL: the view after approval: %+v", v)
	}
	// A read client for two repositories.
	id = g.ask("dmestas-bot", `{"name":"e-desktop","kind":"read","repos":"suit,ctx"}`, "")
	v = g.get("dmestas", id).Setup
	if v == nil || strings.Join(v.Scopes, ",") != "api" || strings.Join(v.TokenRepos, ",") != "suit,ctx" || v.Ceiling != oltp.SetupReadCeiling ||
		v.Belongs != "person" || v.Owner != "dmestas" || v.Pickup != "" {
		t.Errorf("FAIL: the read view: %+v", v)
	}
	// A request whose approver now lacks a repository says so, to the
	// approver alone.
	id = g.ask("dmestas-bot", `{"name":"e-two","kind":"read","repos":"secret"}`, "")
	if err := g.db.Revoke("dmestas", "secret"); err != nil {
		t.Fatal(err)
	}
	if v := g.get("dmestas", id).Setup; v == nil || v.Problem == "" {
		t.Errorf("FAIL: the approver is not told the request cannot run")
	}
	if v := g.get("dmestas-bot", id).Setup; v == nil || v.Problem != "" {
		t.Errorf("FAIL: the asker is told why the request cannot run")
	}
	if !t.Failed() {
		t.Log("ok")
	}
}

// agent.setup is parkable in principle (a lapse revokes its token) but
// can never be parked: it only runs when approved, so a person's
// session sending a pickup code with it is refused, binds no code and
// makes nothing; the code stays free for a real ask.
func TestASetupRunDirectlyWithAPickupCodeIsRefusedAndParksNothing(t *testing.T) {
	g := newSetupRig(t)
	d, _ := g.r.Lookup(setupOp)
	if pickupAllowedFor(d) != nil {
		t.Fatalf("FAIL: agent.setup is not parkable")
	}
	_, code := newPickupKey(t)
	req := httptest.NewRequest("POST", "/api/v1/agents/setup", strings.NewReader(pushBody)).WithContext(asUser("dmestas"))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Quarry-Pickup", code)
	w := httptest.NewRecorder()
	g.h.ServeHTTP(w, req)
	if w.Code != 403 && w.Code != 400 {
		t.Errorf("FAIL: a session's direct run with a pickup code answered %d", w.Code)
	}
	if _, found, _ := g.db.SignOffByPickup(code); found {
		t.Errorf("FAIL: the code was bound")
	}
	g.nothingMade("suit-push")
	if id := g.ask("zeitgeist", pushBody, code); id == "" {
		t.Errorf("FAIL: the code was not free for a real ask")
	}
	if !t.Failed() {
		t.Log("ok")
	}
}