package api
import (
"context"
"testing"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
// capsFakeAccounts is fakeAccounts plus the optional CapsExplainer seam.
// Embedded rather than added to the shared fake so no existing test
// gains a method it did not ask for.
type capsFakeAccounts struct {
*fakeAccounts
stored map[string]map[string]string
}
func (c *capsFakeAccounts) CapsAt(account, repo string) (string, string, error) {
s := c.stored[account][repo]
return s, s, nil
}
// A ROLE NAME IS A READING, AND /me MUST NOT LOSE THE DIFFERENCE.
//
// jan held "Reader plus check in" on a repository. /me reported the bare
// preset name "reader", so a client reading only that row concluded the
// account could not push. It could — jva pushed with it. The nearest
// preset name is a summary, and a summary that only ever rounds DOWN
// tells somebody a door is locked when it is open, which costs them the
// work and sends them looking for a permission they already have.
//
// accounts.explain answered the same account correctly the whole time.
// Two ops describing one account's reach disagreeing is worse than
// either being wrong alone, because the reader does not know which one
// they read.
func TestMeSaysWhenAGrantHoldsMoreThanItsRoleName(t *testing.T) {
accts := &capsFakeAccounts{
fakeAccounts: newFakeAccounts(),
stored: map[string]map[string]string{},
}
accts.accounts["jan"] = Account{Name: "jan", Created: "2026-01-01T00:00:00Z"}
// Reader, plus the check-in letter — the exact string jva measured.
readerPlusCheckIn := oltp.Read.Caps() + "i"
accts.grants["jan"] = map[string]string{"pushmatrix": "reader"}
accts.stored["jan"] = map[string]string{"pushmatrix": readerPlusCheckIn}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Accounts: accts}); err != nil {
t.Fatal(err)
}
out, err := r.Invoke(ops.WithUser(context.Background(), "jan"), "me.account", meAccountIn{})
if err != nil {
t.Fatal(err)
}
got := out.(meAccountOut)
if len(got.Repos) != 1 {
t.Fatalf("me.account reported %d repositories, want 1: %+v", len(got.Repos), got.Repos)
}
row := got.Repos[0]
if row.Role != "Reader" {
t.Errorf("role = %q, want Reader", row.Role)
}
if row.Exact {
t.Error("the row claims the string IS the role, so nothing tells the reader it holds more")
}
if len(row.Plus) == 0 {
t.Fatal("a grant holding more than its role name reported no extras: " +
"this is the understatement that made a client conclude the account could not push")
}
}
// And the ordinary case still reads as exact, so the new fields are a
// qualifier and not a permanent hedge on every row.
func TestMeSaysWhenAGrantIsExactlyItsRole(t *testing.T) {
accts := &capsFakeAccounts{
fakeAccounts: newFakeAccounts(),
stored: map[string]map[string]string{},
}
accts.accounts["jan"] = Account{Name: "jan", Created: "2026-01-01T00:00:00Z"}
accts.grants["jan"] = map[string]string{"ordinary": "reader"}
accts.stored["jan"] = map[string]string{"ordinary": oltp.Read.Caps()}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Accounts: accts}); err != nil {
t.Fatal(err)
}
out, err := r.Invoke(ops.WithUser(context.Background(), "jan"), "me.account", meAccountIn{})
if err != nil {
t.Fatal(err)
}
row := out.(meAccountOut).Repos[0]
if row.Role != "Reader" || !row.Exact || len(row.Plus) != 0 || len(row.Minus) != 0 {
t.Errorf("an exact preset grant read as Role=%q Exact=%v Plus=%v Minus=%v, want Reader/true/empty/empty",
row.Role, row.Exact, row.Plus, row.Minus)
}
}
// A store with no CapsExplainer still answers the op — it just reports
// the collapsed role, which is what it did before. The seam is optional
// so a forge without it keeps working rather than losing the whole read.
func TestMeStillAnswersFromAStoreThatCannotExplain(t *testing.T) {
accts := newFakeAccounts()
accts.accounts["jan"] = Account{Name: "jan", Created: "2026-01-01T00:00:00Z"}
accts.grants["jan"] = map[string]string{"ordinary": "reader"}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Accounts: accts}); err != nil {
t.Fatal(err)
}
out, err := r.Invoke(ops.WithUser(context.Background(), "jan"), "me.account", meAccountIn{})
if err != nil {
t.Fatal(err)
}
got := out.(meAccountOut)
if len(got.Repos) != 1 || got.Repos[0].Role != "reader" {
t.Errorf("without an explainer me.account = %+v, want one row reading reader", got.Repos)
}
}