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