Skip to content
quarry Artifact [f6ca798a39]
Login

Artifact [f6ca798a39]

Artifact f6ca798a39725a2306d308e07806607e9ff778c7a817459470a17543cccfc6e0:


package api

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

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

// A person must be able to find out what they may do. account.explain
// is the forge's one surface whose whole job is to answer that, and it
// took the admin letter — so the only accounts that could read their
// own standing were the ones who least needed to ask.
//
// jva met this from all three vantages and it is the root of f44e0c66:
// a reader cannot act on a refusal when nothing will tell them what
// they hold.
func TestAnAccountMayReadItsOwnLetters(t *testing.T) {
	held := map[string]string{"jva": "r", "boss": "a", "jan": ""}
	accts := explainAccounts{mentionAccounts{names: []string{"jva", "jan", "boss"}}, held}
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(wikiIndex(t)),
		Accounts: accts,
		CapsFor:  func(a, _ string) CapSet { return letterCaps(held[a]) }}); err != nil {
		t.Fatal(err)
	}
	as := func(who string) context.Context { return ops.WithUser(context.Background(), who) }

	// Your own, without the admin letter.
	out, err := r.Invoke(as("jva"), "account.explain", accountExplainIn{Name: "jva", Repo: "test"})
	if err != nil {
		t.Fatalf("FAIL: an account could not read its own letters: %v", err)
	}
	if e, _ := out.(accountExplainOut); e.Name != "jva" || e.Can == "" {
		t.Errorf("FAIL: the answer says nothing useful: %+v", e)
	} else {
		t.Logf("ok: jva reads their own — %q", e.Can)
	}

	// Even with no role at all, which is the vantage that needed it most.
	if _, err := r.Invoke(as("jan"), "account.explain",
		accountExplainIn{Name: "jan", Repo: "test"}); err != nil {
		t.Errorf("FAIL: a role-less account could not read its own: %v", err)
	} else {
		t.Log("ok: an account with no role can still be told it has none")
	}

	// Somebody else's still needs the admin letter, and the refusal
	// says whose and where rather than a bare forbidden.
	_, err = r.Invoke(as("jva"), "account.explain", accountExplainIn{Name: "jan", Repo: "test"})
	if !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: jva read jan's letters: %v", err)
	} else if !strings.Contains(err.Error(), "jan") || !strings.Contains(err.Error(), "admin") {
		t.Errorf("FAIL: the refusal does not say whose or what is needed: %v", err)
	} else {
		t.Logf("ok: refused, and says why — %v", err)
	}

	// An admin still reads anybody's.
	if _, err := r.Invoke(as("boss"), "account.explain",
		accountExplainIn{Name: "jva", Repo: "test"}); err != nil {
		t.Errorf("FAIL: an admin could not read another account's letters: %v", err)
	} else {
		t.Log("ok: an admin still reads anybody's")
	}
}

// A refusal names what it wanted. "This account does not have the
// capability this request needs" is true and useless: no letter, no
// ability, no remedy.
func TestACapabilityRefusalNamesWhatItWanted(t *testing.T) {
	for _, c := range []struct{ user, repo, want, must string }{
		{"jva", "quarry", "3", "post in the forum"},
		{"jva", "quarry", "i", "check in"},
		{"", "quarry", "a", "this request"},
	} {
		got := capRefusal(c.user, c.repo, c.want).Error()
		if !strings.Contains(got, c.must) {
			t.Errorf("FAIL: refusal for %q does not say %q: %s", c.want, c.must, got)
		} else if c.repo != "" && !strings.Contains(got, c.repo) {
			t.Errorf("FAIL: refusal does not say where: %s", got)
		} else {
			t.Logf("ok: %s", got)
		}
	}
	// A letter no ability claims still refuses usefully rather than
	// falling back to the old sentence.
	if got := capRefusal("jva", "quarry", "\x01").Error(); !strings.Contains(got, "quarry") {
		t.Errorf("FAIL: an unknown letter loses the repository: %s", got)
	} else {
		t.Logf("ok: %s", got)
	}
}

// explainAccounts answers both the account seam and CapsExplainer, so
// this test does not wait on oltp growing a CapsAt of its own.
type explainAccounts struct {
	mentionAccounts
	held map[string]string
}

func (e explainAccounts) CapsAt(account, _ string) (string, string, error) {
	return e.held[account], e.held[account], nil
}