package api
import (
"errors"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
// agentCoreRig is agentRig with an ownership registry, the repository
// "test" the ops resolve, and the agent cap set.
func agentCoreRig(t *testing.T, perPerson int) (*ops.Registry, *oltp.DB, *fakeOwners) {
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.AddRepo("test", t.TempDir(), "", oltp.Public); err != nil {
t.Fatal(err)
}
owners := newFakeOwners()
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(wikiIndex(t)),
Teams: db, Accounts: adminAdapter{acctAdapter{db}}, Owners: owners, Categories: db, Roster: dbRoster{db}, AgentsPerPerson: perPerson,
CapsFor: func(a, repo string) CapSet { return db.EffectiveCaps(a, repo) }}); err != nil {
t.Fatal(err)
}
return r, db, owners
}
func TestAgentNewListDeleteAndTheCap(t *testing.T) {
r, db, _ := agentCoreRig(t, 2)
for _, name := range []string{"jva-coder", "jva-release"} {
if _, err := r.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: name}); err != nil {
t.Fatalf("FAIL: agent.new %s: %v", name, err)
}
}
if _, err := r.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: "jva-third"}); !errors.Is(err, ops.ErrInvalid) {
t.Errorf("FAIL: the third agent over a cap of two: %v", err)
} else {
t.Log("ok: agents_per_person caps creation")
}
if db.Operator("jva-coder") != "jva" {
t.Error("FAIL: the caller did not become the operator")
}
if a, _ := db.AccountByName("jva-coder"); a.Admin || a.Pending {
t.Errorf("FAIL: a new agent reads admin=%v pending=%v", a.Admin, a.Pending)
}
if _, ok := db.Authenticate("jva-coder", ""); ok {
t.Error("FAIL: an agent signed in with an empty password")
}
// Only a site admin creates for somebody else; the admin is never capped.
if _, err := r.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: "x", Operator: "mallory"}); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: a person created an agent for somebody else: %v", err)
}
for _, name := range []string{"bot1", "bot2", "bot3"} {
if _, err := r.Invoke(asUser("dmestas"), "agent.new", agentNewIn{Name: name}); err != nil {
t.Errorf("FAIL: the site admin was capped: %v", err)
}
}
if _, err := r.Invoke(asUser("dmestas"), "agent.new", agentNewIn{Name: "mallory-bot", Operator: "mallory"}); err != nil {
t.Errorf("FAIL: the site admin could not create for somebody else: %v", err)
}
// Listing: your own; a site admin may ask for anybody's.
out, err := r.Invoke(asUser("jva"), "agent.list", agentListIn{})
if err != nil {
t.Fatal(err)
}
if got := out.(agentListOut); got.Count != 2 || got.Agents[0].Name != "jva-coder" {
t.Errorf("FAIL: jva's list: %+v", got)
}
if _, err := r.Invoke(asUser("jva"), "agent.list", agentListIn{Operator: "dmestas"}); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: a person listed somebody else's agents: %v", err)
}
out, err = r.Invoke(asUser("dmestas"), "agent.list", agentListIn{Operator: "jva"})
if err != nil || out.(agentListOut).Count != 2 {
t.Errorf("FAIL: the admin's list of jva's: %v %+v", err, out)
}
// Deleting: never a person, never somebody else's.
if _, err := r.Invoke(asUser("jva"), "agent.delete", agentNameIn{Name: "mallory"}); !errors.Is(err, ops.ErrNotFound) {
t.Errorf("FAIL: agent.delete reached a person: %v", err)
}
if _, err := r.Invoke(asUser("mallory"), "agent.delete", agentNameIn{Name: "jva-coder"}); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: a stranger deleted somebody's agent: %v", err)
}
if _, err := r.Invoke(asUser("jva"), "agent.delete", agentNameIn{Name: "jva-coder"}); err != nil {
t.Errorf("FAIL: the operator could not delete their agent: %v", err)
}
if _, ok := db.AccountByName("jva-coder"); ok {
t.Error("FAIL: the deleted agent still exists")
} else {
t.Log("ok: new, list and delete, each held to the operator")
}
// A disabled forge: zero leaves creation to site admins.
r0, _, _ := agentCoreRig(t, 0)
if _, err := r0.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: "jva-coder"}); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: agents_per_person=0 let a person create: %v", err)
}
if _, err := r0.Invoke(asUser("dmestas"), "agent.new", agentNewIn{Name: "bot"}); err != nil {
t.Errorf("FAIL: agents_per_person=0 stopped the site admin: %v", err)
}
}
func TestAgentTokensMintListRevokeAndRevokeAll(t *testing.T) {
r, db, _ := agentCoreRig(t, 5)
for _, name := range []string{"jva-coder", "jva-release"} {
if _, err := r.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: name}); err != nil {
t.Fatal(err)
}
}
out, err := r.Invoke(asUser("jva"), "agent.token.new", agentTokenNewIn{Agent: "jva-coder", Name: "session 1", Scopes: "api,sync", Repos: "test", Ceiling: "uv", Days: 30})
if err != nil {
t.Fatalf("FAIL: %v", err)
}
minted := out.(agentTokenNewOut)
if !strings.HasPrefix(minted.Value, "qat_") || minted.Token.Ceiling != "uv" || len(minted.Token.Repos) != 1 {
t.Errorf("FAIL: minted %+v", minted)
}
if tok, ok := db.ResolveToken("jva-coder", minted.Value, oltp.ScopeAPI); !ok || tok.ID != minted.Token.ID {
t.Error("FAIL: the minted value does not resolve")
}
if _, err := r.Invoke(asUser("mallory"), "agent.token.new", agentTokenNewIn{Agent: "jva-coder", Name: "x", Scopes: "api"}); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: a stranger minted for somebody's agent: %v", err)
}
if _, err := r.Invoke(asUser("jva"), "agent.token.new", agentTokenNewIn{Agent: "mallory", Name: "x", Scopes: "api"}); !errors.Is(err, ops.ErrNotFound) {
t.Errorf("FAIL: a token was minted for a person through the agent door: %v", err)
}
// On a token, none of this is reachable.
onToken := ops.WithToken(asUser("jva"), ops.TokenAuth{ID: "T", Caps: func(string) ops.Caps { return oltp.CapSet("a") }})
for op, in := range map[string]any{
"agent.new": agentNewIn{Name: "x"}, "agent.list": agentListIn{}, "agent.delete": agentNameIn{Name: "jva-coder"},
"agent.token.new": agentTokenNewIn{Agent: "jva-coder", Name: "x", Scopes: "api"},
"agent.token.list": agentTokenListIn{Agent: "jva-coder"}, "agent.token.revoke": agentTokenRevokeIn{Agent: "jva-coder", ID: minted.Token.ID},
"agent.token.revoke_all": struct{}{}, "agent.grant": agentGrantIn{Agent: "jva-coder", Repo: "test", Role: "reader"},
"account.agent": accountAgentIn{Name: "jva-coder"},
} {
if _, err := r.Invoke(onToken, op, in); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: %s on a token: %v", op, err)
}
}
list, err := r.Invoke(asUser("jva"), "agent.token.list", agentTokenListIn{Agent: "jva-coder"})
if err != nil || len(list.(agentTokenListOut).Tokens) != 1 {
t.Errorf("FAIL: list: %v %+v", err, list)
}
if _, err := r.Invoke(asUser("jva"), "agent.token.revoke", agentTokenRevokeIn{Agent: "jva-coder", ID: minted.Token.ID}); err != nil {
t.Errorf("FAIL: revoke: %v", err)
}
if _, ok := db.ResolveToken("jva-coder", minted.Value, oltp.ScopeAPI); ok {
t.Error("FAIL: a revoked token still resolves")
}
var values []string
for _, agent := range []string{"jva-coder", "jva-release"} {
out, err := r.Invoke(asUser("jva"), "agent.token.new", agentTokenNewIn{Agent: agent, Name: "s", Scopes: "api"})
if err != nil {
t.Fatal(err)
}
values = append(values, out.(agentTokenNewOut).Value)
}
out, err = r.Invoke(asUser("jva"), "agent.token.revoke_all", struct{}{})
if err != nil || out.(agentRevokeAllOut).Revoked != 2 {
t.Fatalf("FAIL: revoke_all: %v %+v", err, out)
}
for i, agent := range []string{"jva-coder", "jva-release"} {
if _, ok := db.ResolveToken(agent, values[i], oltp.ScopeAPI); ok {
t.Errorf("FAIL: %s's token survived revoke_all", agent)
}
}
t.Log("ok: mint, list, revoke and revoke_all, each the operator's and never a token's")
}
func TestAgentGrantClipsAndRefusesBeyondReach(t *testing.T) {
r, db, _ := agentCoreRig(t, 5)
if _, err := r.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: "jva-coder"}); err != nil {
t.Fatal(err)
}
if err := db.Grant("jva", "test", oltp.Read); err != nil {
t.Fatal(err)
}
out, err := r.Invoke(asUser("jva"), "agent.grant", agentGrantIn{Agent: "jva-coder", Repo: "test", Role: "developer"})
if err != nil {
t.Fatalf("FAIL: %v", err)
}
got := out.(agentGrantOut)
if !got.Clipped || got.Warning == "" || oltp.CapSet(got.Caps).HasAny("i") {
t.Errorf("FAIL: developer on a reader's agent stored as %+v", got)
} else {
t.Log("ok: the string is clipped to the operator's letters and the response says so")
}
if db.EffectiveCaps("jva-coder", "test").HasAny("i") {
t.Error("FAIL: the agent checks in where its operator cannot")
}
if _, err := r.Invoke(asUser("mallory"), "agent.grant", agentGrantIn{Agent: "jva-coder", Repo: "test", Role: "reader"}); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: a stranger granted somebody's agent: %v", err)
}
if _, err := r.Invoke(asUser("jva"), "agent.grant", agentGrantIn{Agent: "jva-coder", Repo: "nope", Role: "reader"}); !errors.Is(err, ops.ErrNotFound) {
t.Errorf("FAIL: a grant on a repository that is not served: %v", err)
}
// account.grant by a repository admin on the agent still resolves
// under the operator's letters.
if err := db.Grant("dmestas", "test", oltp.RepoAdmin); err != nil {
t.Fatal(err)
}
if _, err := r.Invoke(asUser("dmestas"), "account.grant", accountGrantIn{Name: "jva-coder", Repo: "test", Role: "admin"}); err != nil {
t.Fatalf("FAIL: %v", err)
}
if eff := db.EffectiveCaps("jva-coder", "test"); eff.HasAny("a") || eff.HasAny("i") {
t.Error("FAIL: a repository admin's grant lifted the agent above its operator")
} else {
t.Log("ok: account.grant on an agent resolves under the operator's letters")
}
}
// The deed: the three deed acts refuse an agent before the deed is
// consulted, held or unclaimed alike.
func TestDeedActsRefuseAnAgent(t *testing.T) {
r, db, owners := agentCoreRig(t, 5)
if _, err := r.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: "jva-coder"}); err != nil {
t.Fatal(err)
}
for _, who := range []string{"jva", "jva-coder"} {
if err := db.Grant(who, "test", oltp.RepoAdmin); err != nil {
t.Fatal(err)
}
}
d := Deps{Owners: owners, Accounts: acctAdapter{db}}
for _, held := range []string{"", "jva"} {
owners.owner["test"] = held
for _, verb := range []string{"destroy", "stop serving"} {
err := d.refuseUnlessTheirs(asUser("jva-coder"), "test", verb)
if !errors.Is(err, ops.ErrForbidden) || !strings.Contains(err.Error(), "jva-coder") {
t.Errorf("FAIL: %s by an agent, deed %q: %v", verb, held, err)
}
}
if _, err := r.Invoke(asUser("jva-coder"), "repo.claim", repoClaimIn{Repo: "test"}); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: claim by an agent, deed %q: %v", held, err)
}
if _, err := r.Invoke(asUser("jva-coder"), "repo.claim", repoClaimIn{Repo: "test", Account: "jva"}); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: an agent handed a deed on, deed %q: %v", held, err)
}
}
if owners.owner["test"] != "jva" {
t.Error("FAIL: the deed moved")
}
// A person with the same letters still may.
owners.owner["test"] = ""
if err := d.refuseUnlessTheirs(asUser("jva"), "test", "destroy"); err != nil {
t.Errorf("FAIL: a person refused on an unclaimed repository: %v", err)
}
if _, err := r.Invoke(asUser("jva"), "repo.claim", repoClaimIn{Repo: "test"}); err != nil {
t.Errorf("FAIL: a person could not claim: %v", err)
}
// And nothing hands an agent a deed through the store.
if err := db.SetOwner("test", "jva-coder"); !oltp.IsRefusal(err) {
t.Errorf("FAIL: SetOwner took an agent: %v", err)
} else {
t.Log("ok: delete, remove, claim and SetOwner all refuse an agent; a person passes")
}
}
// Marking clears the password and ends the sessions; clearing the mark
// on a passwordless account returns an invite link; invite and reset
// refuse an agent by name.
func TestMarkingClearsThePasswordAndClearingMintsAnInvite(t *testing.T) {
r, db, _ := agentCoreRig(t, 5)
sess, _, ok := db.NewSession("jva", "a-long-enough-password")
if !ok {
t.Fatal("no session")
}
if _, err := r.Invoke(asUser("dmestas"), "account.agent", accountAgentIn{Name: "jva", Operator: "dmestas"}); err != nil {
t.Fatalf("FAIL: %v", err)
}
if _, ok := db.Authenticate("jva", "a-long-enough-password"); ok {
t.Error("FAIL: the password survived the mark")
}
if _, ok := db.SessionAccount(sess); ok {
t.Error("FAIL: the session survived the mark")
}
if _, err := r.Invoke(asUser("dmestas"), "account.invite", accountInviteIn{Name: "jva"}); !errors.Is(err, ops.ErrInvalid) || !strings.Contains(err.Error(), "jva") {
t.Errorf("FAIL: account.invite on an agent: %v", err)
}
if _, err := r.Invoke(asUser("dmestas"), "account.reset", accountResetIn{Name: "jva"}); err == nil || !strings.Contains(err.Error(), "jva") {
t.Errorf("FAIL: account.reset on an agent: %v", err)
}
out, err := r.Invoke(asUser("dmestas"), "account.agent", accountAgentIn{Name: "jva"})
if err != nil {
t.Fatalf("FAIL: %v", err)
}
link := out.(accountAgentOut).Link
if !strings.Contains(link, "/invite/") {
t.Fatalf("FAIL: clearing the mark returned no invite link: %+v", out)
}
token := link[strings.LastIndex(link, "/")+1:]
if name, ok := db.InviteFor(token); !ok || name != "jva" {
t.Errorf("FAIL: the link is not a live invite for jva: %q %v", name, ok)
} else {
t.Log("ok: the mark clears the password and the sessions; clearing it mints a live invite")
}
}
// adminAdapter is acctAdapter with the AccountAdmin half, so the
// invite and reset doors answer.
type adminAdapter struct{ acctAdapter }
func (a adminAdapter) Sessions(name string) ([]SessionInfo, error) {
rows, err := a.DB.Sessions(name)
if err != nil {
return nil, err
}
out := make([]SessionInfo, 0, len(rows))
for _, s := range rows {
out = append(out, SessionInfo{ID: s.ID, Account: s.Account})
}
return out, nil
}
func (a adminAdapter) EndSession(id, account string) error { return a.DB.EndSessionID(id, account) }
func (a adminAdapter) EndSessions(account string) error { return a.DB.EndAccountSessions(account) }
// An agent does not manage teams: every team op that changes grants,
// membership or where a repository lives refuses an agent caller, and
// the same calls pass for its operator.
func TestTeamOpsRefuseAnAgent(t *testing.T) {
r, db, _ := agentCoreRig(t, 5)
yes := true
if _, err := r.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: "jva-coder"}); err != nil {
t.Fatal(err)
}
for _, who := range []string{"jva", "jva-coder"} {
if err := db.Grant(who, "test", oltp.RepoAdmin); err != nil {
t.Fatal(err)
}
}
if _, err := r.Invoke(asUser("jva"), "team.create", teamCreateIn{Name: "crew", Title: "Crew"}); err != nil {
t.Fatalf("FAIL: the person could not create a team: %v", err)
}
if _, err := r.Invoke(asUser("jva"), "team.member", teamMemberIn{Name: "crew", Account: "jva-coder", Owner: &yes}); err != nil {
t.Fatalf("FAIL: the person could not add the agent as an owner: %v", err)
}
for op, in := range map[string]any{
"team.create": teamCreateIn{Name: "botcrew", Title: "Bots"},
"team.delete": teamGetIn{Name: "crew"},
"team.member": teamMemberIn{Name: "crew", Account: "mallory", Caps: "xy"},
"team.unmember": teamMemberIn{Name: "crew", Account: "jva"},
"team.repo.add": teamRepoIn{Name: "crew", Repo: "test"},
"team.repo.offer.accept": teamRepoIn{Name: "crew", Repo: "test"},
"team.repo.offer.decline": teamRepoIn{Name: "crew", Repo: "test"},
"team.repo.remove": teamRepoIn{Name: "crew", Repo: "test"},
} {
if _, err := r.Invoke(asUser("jva-coder"), op, in); !errors.Is(err, ops.ErrForbidden) || !strings.Contains(err.Error(), "jva-coder") {
t.Errorf("FAIL: %s by an agent: %v", op, err)
}
}
if _, ok := db.TeamByName("botcrew"); ok {
t.Error("FAIL: the agent's team exists")
}
if _, err := r.Invoke(asUser("jva"), "team.repo.add", teamRepoIn{Name: "crew", Repo: "test"}); err != nil {
t.Errorf("FAIL: the person could not place the repository: %v", err)
} else {
t.Log("ok: an agent is refused by every team op that changes grants, membership or placement; its operator is not")
}
}
// A category string hands letters to everyone in the category, and
// nobody sits outside the operator intersection: an agent with a but
// not y may not write y into any category; its operator may.
func TestCategorySetRefusesAnAgentWritingLettersItDoesNotHold(t *testing.T) {
r, db, _ := agentCoreRig(t, 5)
if _, err := r.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: "jva-coder"}); err != nil {
t.Fatal(err)
}
for _, who := range []string{"jva", "jva-coder"} {
if err := db.Grant(who, "test", oltp.RepoAdmin); err != nil {
t.Fatal(err)
}
}
if db.EffectiveCaps("jva-coder", "test").HasAny("y") {
t.Fatal("rig: the agent holds y")
}
for _, cat := range []string{"nobody", "reader"} {
before, _ := db.CategoryCapsFrom("test", cat)
if _, err := r.Invoke(asUser("jva-coder"), "repo.category.set", categorySetIn{Repo: "test", Category: cat, Caps: before + "y"}); !errors.Is(err, ops.ErrForbidden) {
t.Errorf("FAIL: the agent wrote y into %s: %v", cat, err)
}
if after, _ := db.CategoryCapsFrom("test", cat); strings.ContainsRune(after, 'y') {
t.Errorf("FAIL: %s now holds y", cat)
}
// Letters it does hold are fine.
if _, err := r.Invoke(asUser("jva-coder"), "repo.category.set", categorySetIn{Repo: "test", Category: cat, Caps: before}); err != nil {
t.Errorf("FAIL: the agent could not rewrite %s with letters it holds: %v", cat, err)
}
}
if db.EffectiveCaps("jva-coder", "test").HasAny("y") {
t.Error("FAIL: the agent reached y through a category")
}
before, _ := db.CategoryCapsFrom("test", "nobody")
if _, err := r.Invoke(asUser("jva"), "repo.category.set", categorySetIn{Repo: "test", Category: "nobody", Caps: before + "y"}); err != nil {
t.Errorf("FAIL: the person was held to the agent's rule: %v", err)
} else {
t.Log("ok: an agent writes only letters it resolves to into a category; its operator is not held to that")
}
}
// agent.list says whether the caller may make another agent, so a page
// draws its create control only where agent.new would accept it.
func TestAgentListSaysWhetherTheCallerMayCreate(t *testing.T) {
r, _, _ := agentCoreRig(t, 1)
can := func(user string) bool {
out, err := r.Invoke(asUser(user), "agent.list", agentListIn{})
if err != nil {
t.Fatal(err)
}
return out.(agentListOut).CanCreate
}
if !can("jva") {
t.Error("FAIL: under the cap, creating is not offered")
}
if _, err := r.Invoke(asUser("jva"), "agent.new", agentNewIn{Name: "jva-one"}); err != nil {
t.Fatal(err)
}
if can("jva") {
t.Error("FAIL: at the cap, creating is offered")
}
if !can("dmestas") {
t.Error("FAIL: a site admin is never capped")
}
off, _, _ := agentCoreRig(t, 0)
if out, _ := off.Invoke(asUser("jva"), "agent.list", agentListIn{}); out.(agentListOut).CanCreate {
t.Error("FAIL: creating is offered on a forge that allows none")
}
}