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