Skip to content
quarry Artifact [f5db1b98c4]
Login

Artifact [f5db1b98c4]

Artifact f5db1b98c4321514d2121512058321267de4b3b1748816667603abdee637e09c:


package api

import (
	"errors"
	"path/filepath"
	"strings"
	"testing"
	"time"

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

// testSignOffs is the forge database's sign-off rows under the API's
// type, as cmd/quarry's adapter puts them.
type testSignOffs struct{ db *oltp.DB }

func (s testSignOffs) RequestSignOff(r SignOffRequest) (SignOffRequest, error) {
	out, err := s.db.RequestSignOff(oltp.SignOff(r))
	if errors.Is(err, oltp.ErrTooManySignOffs) {
		err = ErrTooManySignOffs
	}
	return SignOffRequest(out), err
}
func (s testSignOffs) SignOffsFor(owner string, limit int) ([]SignOffRequest, error) {
	rows, err := s.db.SignOffsFor(owner, limit)
	var out []SignOffRequest
	for _, r := range rows {
		out = append(out, SignOffRequest(r))
	}
	return out, err
}
func (s testSignOffs) SignOffByID(id string) (SignOffRequest, bool, error) {
	r, ok, err := s.db.SignOffByID(id)
	return SignOffRequest(r), ok, err
}
func (s testSignOffs) DecideSignOff(id, owner string, approve bool) (SignOffRequest, error) {
	r, err := s.db.DecideSignOff(id, owner, approve)
	return SignOffRequest(r), err
}
func (s testSignOffs) FailSignOff(id, why string) error { return s.db.FailSignOff(id, why) }
func (s testSignOffs) FinishSignOff(id, result, secret string) error {
	return s.db.FinishSignOff(id, result, secret)
}
func (s testSignOffs) SetSignOffRefs(id, refs string) error { return s.db.SetSignOffRefs(id, refs) }
func (s testSignOffs) LapseSignOffSecrets(before time.Time) ([]SignOffLapse, error) {
	rows, err := s.db.LapseSignOffSecrets(before)
	var out []SignOffLapse
	for _, r := range rows {
		out = append(out, SignOffLapse(r))
	}
	return out, err
}
func (s testSignOffs) TakeSignOffSecret(id, agent string) (string, error) {
	return s.db.TakeSignOffSecret(id, agent)
}

// signOffRig: jva owns "test", jva-bot is jva's agent; dmestas (a site
// admin) owns "adm", and fbot is a forge agent answerable to dmestas.
func signOffRig(t *testing.T) (*ops.Registry, *fakeRegistry, *recordedActs, *fakeOwners, *Repos) {
	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", "mallory"} {
		if err := db.AddAccount(who, "a-long-enough-password", who == "dmestas"); err != nil {
			t.Fatal(err)
		}
	}
	if err := db.AddAccount("jva-bot", "a-long-enough-password", false); err != nil {
		t.Fatal(err)
	}
	if err := db.SetOperator("jva-bot", "jva"); err != nil {
		t.Fatal(err)
	}
	if err := db.NewForgeAgent("fbot", "dmestas"); err != nil {
		t.Fatal(err)
	}
	owners := newFakeOwners()
	owners.owner["test"], owners.owner["adm"] = "jva", "dmestas"
	repos := NewRepos(map[string]*Repo{"test": {Code: "code-test"}, "adm": {Code: "code-adm"}})
	reg, acts := &fakeRegistry{}, &recordedActs{}
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Repos: repos, Registry: reg, Owners: owners, Acts: acts,
		Accounts: adminAdapter{acctAdapter{db}}, SignOffs: testSignOffs{db}, Prefs: db, SecretBox: testBox(t),
		CapsFor: func(a, repo string) CapSet { return db.EffectiveCaps(a, repo) }}); err != nil {
		t.Fatal(err)
	}
	return r, reg, acts, owners, repos
}

func pendingOf(t *testing.T, r *ops.Registry, who, op string, in any) SignOffRequest {
	t.Helper()
	out, err := r.Invoke(asUser(who), op, in)
	if err != nil {
		t.Fatalf("FAIL: the agent's request was refused: %v", err)
	}
	so := out.(repoRemoveOut).SignOff
	if so == nil || so.State != "pending" {
		t.Fatalf("FAIL: no request came back: %+v", out)
	}
	return *so
}

// An agent's deed act on its owner's repository waits for them, once:
// asking again returns the same request, and nothing is done until the
// owner approves; approving runs it exactly once, as the owner, with a
// receipt that says who asked.
func TestAnAgentsDeleteWaitsForItsOwner(t *testing.T) {
	r, reg, acts, _, _ := signOffRig(t)
	in := repoDeleteIn{Name: "test", Confirm: "test"}
	req := pendingOf(t, r, "jva-bot", "repo.delete", in)
	if again := pendingOf(t, r, "jva-bot", "repo.delete", in); again.ID != req.ID {
		t.Errorf("FAIL: asking twice made two requests")
	}
	if len(reg.deleted) != 0 {
		t.Fatalf("FAIL: deleted before the owner signed off: %v", reg.deleted)
	}
	list, err := r.Invoke(asUser("jva"), "signoff.list", signOffListIn{})
	if err != nil || len(list.(signOffListOut).Requests) != 1 || !strings.Contains(list.(signOffListOut).Requests[0].Summary, "test") {
		t.Fatalf("FAIL: the owner's list: %+v %v", list, err)
	}
	if _, err := r.Invoke(asUser("jva-bot"), "signoff.decide", signOffDecideIn{ID: req.ID, Approve: true}); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: the agent approved its own request: %v", err)
	}
	if _, err := r.Invoke(asUser("mallory"), "signoff.decide", signOffDecideIn{ID: req.ID, Approve: true}); !errors.Is(err, ops.ErrNotFound) {
		t.Errorf("FAIL: somebody else approved it: %v", err)
	}
	if _, err := r.Invoke(asUser("jva"), "signoff.decide", signOffDecideIn{ID: req.ID, Approve: true}); err != nil {
		t.Fatalf("FAIL: the owner's approval: %v", err)
	}
	if len(reg.deleted) != 1 || reg.deleted[0] != "test" {
		t.Errorf("FAIL: approval deleted %v", reg.deleted)
	}
	if len(acts.acts) != 1 || acts.acts[0].Actor != "jva-bot (signed off by jva)" || acts.acts[0].Op != "repo.delete" {
		t.Errorf("FAIL: the receipt: %+v", acts.acts)
	}
	if _, err := r.Invoke(asUser("jva"), "signoff.decide", signOffDecideIn{ID: req.ID, Approve: true}); err == nil || len(reg.deleted) != 1 {
		t.Errorf("FAIL: a second approval ran it again: %v, %v", err, reg.deleted)
	}
}

// Denied, nothing runs; and a request whose repository changed hands
// since it was made cannot be approved.
func TestADeniedOrStaleRequestRunsNothing(t *testing.T) {
	r, reg, _, owners, _ := signOffRig(t)
	req := pendingOf(t, r, "jva-bot", "repo.remove", repoRemoveIn{Name: "test"})
	if _, err := r.Invoke(asUser("jva"), "signoff.decide", signOffDecideIn{ID: req.ID}); err != nil {
		t.Fatal(err)
	}
	if len(reg.removed) != 0 {
		t.Errorf("FAIL: a denied request ran: %v", reg.removed)
	}
	req = pendingOf(t, r, "jva-bot", "repo.remove", repoRemoveIn{Name: "test"})
	owners.owner["adm"] = "jva"
	if _, err := r.Invoke(asUser("jva"), "signoff.decide", signOffDecideIn{ID: req.ID, Approve: true}); err != nil {
		t.Fatalf("FAIL: an unchanged request: %v", err)
	}
	req = pendingOf(t, r, "jva-bot", "repo.delete", repoDeleteIn{Name: "adm", Confirm: "adm"})
	owners.owner["adm"] = "mallory"
	if _, err := r.Invoke(asUser("jva"), "signoff.decide", signOffDecideIn{ID: req.ID, Approve: true}); !errors.Is(err, ops.ErrInvalid) {
		t.Errorf("FAIL: approved a request for a repository that changed hands: %v", err)
	}
	if len(reg.deleted) != 0 {
		t.Errorf("FAIL: the stale request ran: %v", reg.deleted)
	}
}

// A forge agent asks the site admin answerable for it; an agent on a
// repository its owner does not hold is refused as before, with no
// request made.
func TestAForgeAgentAsksItsAnswerableAdmin(t *testing.T) {
	r, reg, _, _, _ := signOffRig(t)
	req := pendingOf(t, r, "fbot", "repo.delete", repoDeleteIn{Name: "adm", Confirm: "adm"})
	if req.Owner != "dmestas" {
		t.Errorf("FAIL: the forge agent asked %q", req.Owner)
	}
	if _, err := r.Invoke(asUser("fbot"), "repo.delete", repoDeleteIn{Name: "test", Confirm: "test"}); err == nil {
		t.Errorf("FAIL: a forge agent deleted a repository its admin does not hold")
	}
	if list, _ := r.Invoke(asUser("jva"), "signoff.list", signOffListIn{}); len(list.(signOffListOut).Requests) != 0 {
		t.Errorf("FAIL: jva was asked about an agent that is not theirs")
	}
	if len(reg.deleted) != 0 {
		t.Errorf("FAIL: %v", reg.deleted)
	}
}

// RFC 0027, Results: what an approved call answered is kept for the
// agent and its person; a field marked secret goes to the asking agent
// alone, once, and never into the answer anyone else reads.
func TestAnApprovedRequestsAnswerReachesTheAgentAndItsSecretOnce(t *testing.T) {
	type minted struct {
		Name  string `json:"name"`
		Token string `json:"token" secret:"once"`
	}
	pub, sec, err := splitSecret(minted{Name: "rajeev", Token: "t0k"})
	if err != nil || strings.Contains(pub, "t0k") || !strings.Contains(pub, "rajeev") || sec != `{"token":"t0k"}` {
		t.Fatalf("FAIL: split %q / %q %v", pub, sec, err)
	}
	if pub, sec, _ := splitSecret(&minted{Name: "x"}); sec != `{"token":""}` || strings.Contains(pub, "token") {
		t.Errorf("FAIL: a pointer answer split %q / %q", pub, sec)
	}

	r, _, _, _, _ := signOffRig(t)
	req := pendingOf(t, r, "jva-bot", "repo.remove", repoRemoveIn{Name: "test"})
	if _, err := r.Invoke(asUser("jva"), "signoff.decide", signOffDecideIn{ID: req.ID, Approve: true}); err != nil {
		t.Fatal(err)
	}
	for _, who := range []string{"jva-bot", "jva"} {
		out, err := r.Invoke(asUser(who), "signoff.get", signOffGetIn{ID: req.ID})
		if err != nil {
			t.Fatalf("FAIL: %s could not read the request: %v", who, err)
		}
		if g := out.(signOffGetOut); g.Request.State != "approved" || len(g.Result) == 0 || len(g.Secret) != 0 {
			t.Errorf("FAIL: %s read state=%s result=%s secret=%s", who, g.Request.State, g.Result, g.Secret)
		}
	}
	if _, err := r.Invoke(asUser("mallory"), "signoff.get", signOffGetIn{ID: req.ID}); !errors.Is(err, ops.ErrNotFound) {
		t.Errorf("FAIL: somebody else read the request: %v", err)
	}
}

// RFC 0027, What may be asked: never the person's own credentials,
// sessions or account, nor deciding requests.
func TestAPersonsOwnAccountIsNotAskable(t *testing.T) {
	for _, c := range []struct {
		op   string
		in   any
		want bool
	}{
		{"me.passwd", struct{}{}, false},
		{"me.sync.token", struct{}{}, false},
		{"signoff.decide", signOffDecideIn{}, false},
		{"account.agent", accountAgentIn{Name: "x"}, false},
		{"account.delete", struct{ Name string }{"dmestas"}, false},
		{"account.delete", struct{ Name string }{"someone"}, true},
		{"agent.new", struct{ Name string }{"rajeev"}, true},
		{"repo.claim", repoClaimIn{Repo: "r"}, true},
	} {
		if got := askableFor(c.op, "dmestas", c.in); got != c.want {
			t.Errorf("FAIL: askable(%s, %+v) = %v, want %v", c.op, c.in, got, c.want)
		}
	}
}