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
}