package oltp
import (
"errors"
"strings"
"testing"
)
// setupRig is the agent rig with dmestas holding the admin letter on
// priv, so a pushing agent set up there is the repository's own.
func setupRig(t *testing.T) *DB {
t.Helper()
db := agentRig(t)
if err := db.Grant("dmestas", "priv", RepoAdmin); err != nil {
t.Fatal(err)
}
return db
}
func pushSetup(name string) AgentSetup {
return AgentSetup{Name: name, Approver: "dmestas", Operator: "repo:priv", Repos: []string{"priv"},
Caps: Write.Caps(), RefuseClip: true, PerPerson: -1,
Token: TokenSpec{Name: "setup", Repos: []string{"priv"}, Scopes: []string{ScopeSync}, Ceiling: Write.Caps()}}
}
func readSetup(name string, repos ...string) AgentSetup {
return AgentSetup{Name: name, Approver: "jva", Operator: "jva", Repos: repos, Caps: Read.Caps(), PerPerson: -1,
Token: TokenSpec{Name: "setup", Repos: repos, Scopes: []string{ScopeAPI}, Ceiling: SetupReadCeiling}}
}
// held is everything a setup writes under name: the account row, the
// grants and the tokens, and whether the name is retired.
func held(t *testing.T, db *DB, name string) (acct bool, grants, tokens int, retired bool) {
t.Helper()
_, acct = db.AccountByName(name)
if err := db.db.QueryRow(`SELECT count(*) FROM repo_caps WHERE account = ?`, name).Scan(&grants); err != nil {
t.Fatal(err)
}
if err := db.db.QueryRow(`SELECT count(*) FROM token WHERE account = ?`, name).Scan(&tokens); err != nil {
t.Fatal(err)
}
return acct, grants, tokens, db.Tombstoned(name)
}
// RFC 0030 step 3: an approved setup makes an agent, a grant and a token
// whose scopes are sync and whose repositories are the one named.
func TestASetupMakesTheAgentItsGrantAndItsTokenTogether(t *testing.T) {
db := setupRig(t)
res, err := db.SetupAgent(pushSetup("priv-push"))
if err != nil {
t.Fatal(err)
}
a, ok := db.AccountByName("priv-push")
if !ok || a.Operator != "repo:priv" || a.Answerable != "dmestas" {
t.Fatalf("FAIL: the agent: %+v ", a)
}
if stored, _ := db.CapsOf("priv-push", "priv"); stored != Write.Caps() {
t.Errorf("FAIL: the grant is %q", stored)
}
toks, _ := db.Tokens("priv-push")
if len(toks) != 1 || strings.Join(toks[0].Scopes, ",") != ScopeSync || strings.Join(toks[0].Repos, ",") != "priv" || toks[0].Ceiling != Write.Caps() {
t.Fatalf("FAIL: the token: %+v", toks)
}
if res.Value == "" || toks[0].Expires != "" {
t.Errorf("FAIL: the value or the expiry: %q %q", res.Value, toks[0].Expires)
}
// A person's read client: the approver's own, inherit off, scoped to
// the one repository it reads, api only.
if _, err := db.SetupAgent(readSetup("e-one", "pub")); err != nil {
t.Fatal(err)
}
e, _ := db.AccountByName("e-one")
if e.Operator != "jva" || e.Inherit {
t.Errorf("FAIL: the read client: operator=%q inherit=%v", e.Operator, e.Inherit)
}
if !t.Failed() {
t.Log("ok")
}
}
// A failure at any step leaves nothing: no account row, no grant, no
// token and no retired name. Each failure point is forced, and the same
// name then sets up cleanly.
func TestAFailedSetupLeavesNothingAtEveryStep(t *testing.T) {
for _, step := range []string{"account", "grant", "mint"} {
t.Run(step, func(t *testing.T) {
db := setupRig(t)
setupFault = func(s string) error {
if s == step {
return errors.New("disk I/O error")
}
return nil
}
defer func() { setupFault = nil }()
if _, err := db.SetupAgent(readSetup("e-two", "pub", "priv")); err == nil {
t.Fatalf("FAIL: a setup forced to fail at %s succeeded", step)
}
setupFault = nil
if acct, grants, tokens, retired := held(t, db, "e-two"); acct || grants != 0 || tokens != 0 || retired {
t.Errorf("FAIL: after a failure at %s: account=%v grants=%d tokens=%d retired=%v", step, acct, grants, tokens, retired)
}
if _, err := db.SetupAgent(readSetup("e-two", "pub")); err != nil {
t.Errorf("FAIL: the name was not free after a failure at %s: %v", step, err)
}
if !t.Failed() {
t.Log("ok")
}
})
}
}
// A grant the store itself refuses, after the account row and one grant
// were written, rolls all of it back: the approver cannot reach priv.
func TestASetupWhoseSecondGrantIsRefusedLeavesNoAccountRow(t *testing.T) {
db := setupRig(t)
_, err := db.SetupAgent(readSetup("e-three", "pub", "priv"))
var refusal Refusal
if !errors.As(err, &refusal) {
t.Fatalf("FAIL: the unreachable repository was not refused: %v", err)
}
if acct, grants, tokens, retired := held(t, db, "e-three"); acct || grants != 0 || tokens != 0 || retired {
t.Errorf("FAIL: left behind: account=%v grants=%d tokens=%d retired=%v", acct, grants, tokens, retired)
}
if !t.Failed() {
t.Log("ok")
}
}
// A person's pushing agent is clipped to the approver's letters, and a
// clip that takes anything from the role refuses the whole setup.
func TestAPushSetupThatTheApproversLettersWouldClipIsRefused(t *testing.T) {
db := setupRig(t)
// jva holds reader on pub only; developer would be cut.
s := pushSetup("pub-push")
s.Approver, s.Operator, s.Scope, s.Repos = "jva", "jva", "repo:pub", []string{"pub"}
s.Token.Repos = []string{"pub"}
_, err := db.SetupAgent(s)
var refusal Refusal
if !errors.As(err, &refusal) {
t.Fatalf("FAIL: a clipped push setup was not refused: %v", err)
}
if acct, grants, tokens, retired := held(t, db, "pub-push"); acct || grants != 0 || tokens != 0 || retired {
t.Errorf("FAIL: left behind: account=%v grants=%d tokens=%d retired=%v", acct, grants, tokens, retired)
}
if !t.Failed() {
t.Log("ok")
}
}
// Setup makes a new agent only: a name that is already an account, in
// any case, a person or an agent, or that was one, is refused, and what
// that account held is untouched.
func TestASetupRefusesAnyNameThatIsAlreadyAnAccount(t *testing.T) {
db := setupRig(t)
if _, err := db.SetupAgent(readSetup("e-four", "pub")); err != nil {
t.Fatal(err)
}
before, _ := db.CapsOf("e-four", "pub")
for _, name := range []string{"e-four", "E-Four", "dmestas", "DMestas", "dmestas-coder", "root"} {
s := readSetup(name, "pub")
s.Caps = Write.Caps()
if _, err := db.SetupAgent(s); err == nil {
t.Errorf("FAIL: a setup took the existing account %q", name)
}
}
if got, _ := db.CapsOf("e-four", "pub"); got != before {
t.Errorf("FAIL: the existing agent's grant changed to %q", got)
}
if toks, _ := db.Tokens("e-four"); len(toks) != 1 {
t.Errorf("FAIL: the existing agent holds %d tokens", len(toks))
}
if err := db.DeleteAccount("e-four"); err != nil {
t.Fatal(err)
}
if _, err := db.SetupAgent(readSetup("E-FOUR", "pub")); err == nil {
t.Errorf("FAIL: a retired name was taken, in another case")
}
if !t.Failed() {
t.Log("ok")
}
}
// RetractSetup takes a committed setup back: the agent is gone and its
// name is free, not retired.
func TestRetractingASetupRemovesTheAgentAndFreesTheName(t *testing.T) {
db := setupRig(t)
res, err := db.SetupAgent(pushSetup("priv-push"))
if err != nil {
t.Fatal(err)
}
if err := db.RetractSetup("priv-push", res.Token.ID); err != nil {
t.Fatal(err)
}
if acct, grants, tokens, retired := held(t, db, "priv-push"); acct || grants != 0 || tokens != 0 || retired {
t.Errorf("FAIL: left behind: account=%v grants=%d tokens=%d retired=%v", acct, grants, tokens, retired)
}
if _, err := db.SetupAgent(pushSetup("priv-push")); err != nil {
t.Errorf("FAIL: the name was not free: %v", err)
}
if !t.Failed() {
t.Log("ok")
}
}