Skip to content
quarry Artifact [56bcf92c9d]
Login

Artifact [56bcf92c9d]

Artifact 56bcf92c9db0d016a15fbb54fd85cd72f4387111fa892bab87dfd66cdd9a05f1:


package api

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

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

// dbRoster is oltp as a CapsWriter: its CapsOf returns the account's
// OWN string on a repository, which is how handsWarning tells somebody
// who reaches a repository only through their team from somebody who
// would keep it anyway.
type dbRoster struct{ *oltp.DB }

func (r dbRoster) CapsOf(account, repo string) (string, error) {
	return r.DB.CapsOf(account, repo)
}

func (r dbRoster) SetCaps(actor, account, repo, caps string) (string, error) {
	return r.DB.SetCapsGuarded(actor, account, repo, caps)
}

// handsRig: two teams, a repository, and members who reach it only
// through their team — the population a change of hands moves.
func handsRig(t *testing.T) (*ops.Registry, *oltp.DB, *fakeEvents) {
	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{"owen", "mia", "nils", "vera"} {
		if err := db.AddAccount(who, "a-long-enough-password", false); err != nil {
			t.Fatal(err)
		}
	}
	ev := &fakeEvents{}
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v",
		Repos:    NewRepos(map[string]*Repo{"test": {}}),
		Teams:    db,
		Accounts: acctAdapter{db},
		Roster:   dbRoster{db},
		Events:   ev,
		CapsFor:  func(a, repo string) CapSet { return db.EffectiveCaps(a, repo) }}); err != nil {
		t.Fatal(err)
	}
	// PRIVATE, which is the case a change of hands actually moves. On a
	// public repository the categories already grant everything the
	// membership floor does, so putting it under a team changes nothing
	// for anybody and saying nothing is the correct answer — the first
	// version of this test used the default (public) categories and
	// proved only that.
	//
	// RFC 0018 says the same thing from the other side: a private
	// repository has both categories empty by definition, and teams are
	// mostly used on private repositories, so the floor is the entire
	// access story there.
	for _, cat := range []string{"nobody", "anonymous"} {
		if err := db.SetCategoryCaps("test", cat, ""); err != nil {
			t.Fatal(err)
		}
	}
	// owen administers the repository, so he may move it.
	if err := db.Grant("owen", "test", oltp.RepoAdmin); err != nil {
		t.Fatal(err)
	}
	for _, team := range []string{"alpha", "beta"} {
		if _, err := r.Invoke(asUser("owen"), "team.create", teamCreateIn{Name: team}); err != nil {
			t.Fatal(err)
		}
	}
	// mia reaches it only through alpha; vera only through beta.
	if _, err := r.Invoke(asUser("owen"), "team.member",
		teamMemberIn{Name: "alpha", Account: "mia"}); err != nil {
		t.Fatal(err)
	}
	if _, err := r.Invoke(asUser("owen"), "team.member",
		teamMemberIn{Name: "beta", Account: "vera"}); err != nil {
		t.Fatal(err)
	}
	// nils is in alpha AND holds a string of his own, so a move costs
	// him nothing — he must not be told he lost anything.
	if _, err := r.Invoke(asUser("owen"), "team.member",
		teamMemberIn{Name: "alpha", Account: "nils"}); err != nil {
		t.Fatal(err)
	}
	if err := db.SetCaps("nils", "test", "uv3bBC78"); err != nil {
		t.Fatal(err)
	}
	return r, db, ev
}

// Putting a repository under a team grants every member access to it,
// and taking it out takes that away. account.caps raises RFC 0005's
// vocabulary and puts a line in the person's inbox for a change to ONE
// account; the team path changes access for many at once — and on a
// private repository changes the whole of it — and said nothing.
func TestAChangeOfHandsTellsThePeopleItMoved(t *testing.T) {
	r, _, ev := handsRig(t)

	if _, err := r.Invoke(asUser("owen"), "team.repo.add",
		teamRepoIn{Name: "alpha", Repo: "test"}); err != nil {
		t.Fatal(err)
	}
	// mia gained access, so she is told. nils held his own string
	// already and gained nothing, so he is not.
	added := ev.subjects()
	if len(added) == 0 {
		t.Fatal("FAIL: putting a repository under a team raised nothing")
	}
	if n := strings.Count(strings.Join(added, " "), "account.added"); n < 1 {
		t.Errorf("FAIL: an arrival did not raise account.added: %v", added)
	}

	// Moving it to beta: alpha's members lose it, beta's gain it.
	before := len(ev.subjects())
	out, err := r.Invoke(asUser("owen"), "team.repo.add",
		teamRepoIn{Name: "beta", Repo: "test"})
	if err != nil {
		t.Fatal(err)
	}
	moved := ev.subjects()[before:]
	if len(moved) == 0 {
		t.Error("FAIL: moving a repository between teams raised nothing")
	}
	// The answer names who loses access rather than leaving it to be
	// discovered — RFC 0017's rule for unbinding, same act.
	warn := out.(teamOkOut).Warning
	if !strings.Contains(warn, "mia") {
		t.Errorf("FAIL: the move did not name who loses access: %q", warn)
	}
	if strings.Contains(warn, "nils") {
		t.Errorf("FAIL: the warning names somebody who loses nothing: %q", warn)
	}
}

// Taking a repository out names who loses access, and dissolving a team
// does the same for every repository it held — the two paths that used
// to drop access with nothing said.
func TestRemovingAndDissolvingBothNameWhoLosesAccess(t *testing.T) {
	r, db, _ := handsRig(t)
	if _, err := r.Invoke(asUser("owen"), "team.repo.add",
		teamRepoIn{Name: "alpha", Repo: "test"}); err != nil {
		t.Fatal(err)
	}
	out, err := r.Invoke(asUser("owen"), "team.repo.remove",
		teamRepoIn{Name: "alpha", Repo: "test"})
	if err != nil {
		t.Fatal(err)
	}
	if w := out.(teamOkOut).Warning; !strings.Contains(w, "mia") {
		t.Errorf("FAIL: removal did not name who loses access: %q", w)
	}

	// And the dissolve path, which does not go through remove.
	if _, err := r.Invoke(asUser("owen"), "team.repo.add",
		teamRepoIn{Name: "alpha", Repo: "test"}); err != nil {
		t.Fatal(err)
	}
	// Dissolving does not go through remove, so it is its own path and
	// gets its own assertion: mia reaches the repository through alpha
	// and must not still reach it once alpha is gone.
	if !db.EffectiveCaps("mia", "test").HasAny("C") {
		t.Fatal("FAIL: setup — mia does not reach the repository through alpha")
	}
	if _, err := r.Invoke(asUser("owen"), "team.delete", teamGetIn{Name: "alpha"}); err != nil {
		t.Fatal(err)
	}
	if db.EffectiveCaps("mia", "test").HasAny("C") {
		t.Error("FAIL: alpha is gone and mia still reaches its repository")
	}
}