Skip to content
quarry Artifact [1d52210be4]
Login

Artifact [1d52210be4]

Artifact 1d52210be427404c1b0a7d0814367ef74e0a6c76b5e2103219e14be083dff313:


package api

import (
	"context"
	"errors"
	"strings"
	"testing"
	"time"

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

// fakeOwners is the ownership registry as this op uses it.
type fakeOwners struct {
	owner map[string]string
	ns    map[string]string
	nsBy  map[string]string
}

func newFakeOwners() *fakeOwners {
	return &fakeOwners{owner: map[string]string{}, ns: map[string]string{}, nsBy: map[string]string{}}
}

func (f *fakeOwners) OwnerOf(repo string) (string, error)     { return f.owner[repo], nil }
func (f *fakeOwners) SetOwner(repo, account string) error     { f.owner[repo] = account; return nil }
func (f *fakeOwners) NamespaceOf(repo string) (string, error) { return f.ns[repo], nil }
func (f *fakeOwners) SetNamespace(repo, ns, by string, at time.Time) error {
	f.ns[repo] = ns
	f.nsBy[repo] = by
	return nil
}

// fakeGrants answers GrantsFor and nothing else this op needs.
type fakeGrants struct{ grants map[string]map[string]string }

func (f *fakeGrants) Accounts() ([]Account, error)                   { return nil, nil }
func (f *fakeGrants) AddAccount(name, password string, a bool) error { return nil }
func (f *fakeGrants) SetPassword(name, password string) error        { return nil }
func (f *fakeGrants) DeleteAccount(name string) error                { return nil }
func (f *fakeGrants) Grant(account, repo, role string) error         { return nil }
func (f *fakeGrants) Revoke(account, repo string) error              { return nil }
func (f *fakeGrants) GrantsFor(account string) (map[string]string, error) {
	return f.grants[account], nil
}

func claimRegistry(t *testing.T, owners *fakeOwners, grants *fakeGrants) *ops.Registry {
	t.Helper()
	r := ops.NewRegistry()
	if err := registerRepoClaim(r, Deps{Owners: owners, Accounts: grants}); err != nil {
		t.Fatal(err)
	}
	return r
}

func TestClaimTakesAForgeHeldRepository(t *testing.T) {
	owners := newFakeOwners()
	r := claimRegistry(t, owners, &fakeGrants{})
	ctx := ops.WithUser(context.Background(), "dan")
	got, err := r.Invoke(ctx, "repo.claim", repoClaimIn{Repo: "zit"})
	if err != nil {
		t.Fatal(err)
	}
	out := got.(repoClaimOut)
	if out.Owner != "dan" || out.Namespace != "dan" {
		t.Fatalf("out = %+v, want dan owning zit in dan's namespace", out)
	}
	if owners.nsBy["zit"] != "dan" {
		t.Fatalf("placement recorded %q, want dan", owners.nsBy["zit"])
	}
}

func TestClaimRefusesARepositorySomebodyAlreadyHolds(t *testing.T) {
	owners := newFakeOwners()
	owners.owner["quarry"] = "grace"
	r := claimRegistry(t, owners, &fakeGrants{})
	ctx := ops.WithUser(context.Background(), "dan")
	_, err := r.Invoke(ctx, "repo.claim", repoClaimIn{Repo: "quarry"})
	if !errors.Is(err, ops.ErrInvalid) {
		t.Fatalf("err = %v, want ErrInvalid — a held repository changes hands by transfer", err)
	}
	if !strings.Contains(err.Error(), "transfer") {
		t.Fatalf("the refusal should name transfer as the right verb, got %v", err)
	}
	if owners.owner["quarry"] != "grace" {
		t.Fatal("a refused claim must not have moved the deed")
	}
}

// Handing the deed to somebody else is allowed only when they already
// administer the repository, which is the nearest thing to consent
// available without a pending-offer flow.
func TestClaimHandsTheDeedOnlyToAnExistingAdmin(t *testing.T) {
	owners := newFakeOwners()
	grants := &fakeGrants{grants: map[string]map[string]string{
		"dan":      {"zit": "admin"},
		"outsider": {},
	}}
	r := claimRegistry(t, owners, grants)
	ctx := ops.WithUser(context.Background(), "zeitgeist")

	if _, err := r.Invoke(ctx, "repo.claim", repoClaimIn{Repo: "zit", Account: "outsider"}); !errors.Is(err, ops.ErrInvalid) {
		t.Fatalf("err = %v, want ErrInvalid — outsider does not administer zit", err)
	}
	if owners.owner["zit"] != "" {
		t.Fatal("a refused hand-over must not have moved the deed")
	}

	got, err := r.Invoke(ctx, "repo.claim", repoClaimIn{Repo: "zit", Account: "dan"})
	if err != nil {
		t.Fatal(err)
	}
	if out := got.(repoClaimOut); out.Owner != "dan" {
		t.Fatalf("owner = %q, want dan", out.Owner)
	}
	// Provenance records who performed the act, not who received it.
	if owners.nsBy["zit"] != "zeitgeist" {
		t.Fatalf("placement recorded %q, want zeitgeist", owners.nsBy["zit"])
	}
}

// Claiming must not move a repository out of the team it appears under:
// the deed and the appearance are separate facts.
func TestClaimLeavesAnExistingNamespaceAlone(t *testing.T) {
	owners := newFakeOwners()
	owners.ns["proj"] = "hoppers"
	r := claimRegistry(t, owners, &fakeGrants{})
	ctx := ops.WithUser(context.Background(), "dan")
	got, err := r.Invoke(ctx, "repo.claim", repoClaimIn{Repo: "proj"})
	if err != nil {
		t.Fatal(err)
	}
	out := got.(repoClaimOut)
	if out.Owner != "dan" || out.Namespace != "hoppers" {
		t.Fatalf("out = %+v, want dan holding proj while it still appears under hoppers", out)
	}
}

func TestClaimSurfaceIsRepositoryAdmin(t *testing.T) {
	r := claimRegistry(t, newFakeOwners(), &fakeGrants{})
	op, ok := r.Lookup("repo.claim")
	if !ok {
		t.Fatal("repo.claim is not registered")
	}
	if op.Caps != "a" || op.Auth != ops.Token {
		t.Fatalf("surface = caps %q auth %v, want a/Token — the repository's own admin, not the site's", op.Caps, op.Auth)
	}
}