1package permbook23import (4 "testing"56 "gno.land/p/nt/testutils/v0"7 "gno.land/p/nt/uassert/v0"8)910var (11 admin = testutils.TestAddress("permadmin")12 nominee = testutils.TestAddress("nominee")13 alice = testutils.TestAddress("alice")14 bob = testutils.TestAddress("bob")15 mallory = testutils.TestAddress("mallory")16)1718// --- test harness ---19//20// permbook's mutators identify their principal as rlm.Previous().Address()21// after proving rlm.IsCurrent(). A test function's own `cur` is current but22// has no previous frame — Previous() panics with "cannot seek beyond origin23// caller override". So every mutating call is made from inside a real24// crossing frame, entered with cross(cur), exactly as p/nt/ownable/v0's own25// tests do.26//27// testing.SetRealm(testing.NewUserRealm(addr)) sets the current realm to a28// user realm for addr, so the next crossing frame has29// Previous().Address() == addr — the principal the mutator authorizes30// against. It is written out at every call site on purpose: the override is31// scoped to the frame that installs it, so wrapping it in a helper makes it32// pop the moment the helper returns and the principal comes back empty.3334func grant(cur realm, b *Book, perm string, addr address) error {35 var err error36 func(cur realm) { err = b.Grant(0, cur, perm, addr) }(cross(cur))37 return err38}3940func revoke(cur realm, b *Book, perm string, addr address) error {41 var err error42 func(cur realm) { err = b.Revoke(0, cur, perm, addr) }(cross(cur))43 return err44}4546func revokeAll(cur realm, b *Book, addr address) (int, error) {47 var (48 n int49 err error50 )51 func(cur realm) { n, err = b.RevokeAll(0, cur, addr) }(cross(cur))52 return n, err53}5455func dropPerm(cur realm, b *Book, perm string) error {56 var err error57 func(cur realm) { err = b.DropPermission(0, cur, perm) }(cross(cur))58 return err59}6061func nominate(cur realm, b *Book, addr address) error {62 var err error63 func(cur realm) { err = b.NominateAdmin(0, cur, addr) }(cross(cur))64 return err65}6667func cancelNom(cur realm, b *Book) error {68 var err error69 func(cur realm) { err = b.CancelNomination(0, cur) }(cross(cur))70 return err71}7273func acceptAdmin(cur realm, b *Book) error {74 var err error75 func(cur realm) { err = b.AcceptAdmin(0, cur) }(cross(cur))76 return err77}7879// newBook builds a book for tests.80func newBook(t *testing.T, owner address) *Book {81 t.Helper()82 b, err := NewDefault(owner)83 uassert.NoError(t, err)84 if b == nil {85 t.Fatal("NewDefault returned nil book")86 }87 return b88}8990// --- construction ---9192func TestNewDefault(cur realm, t *testing.T) {93 b := newBook(t, admin)94 uassert.Equal(t, admin.String(), b.Admin().String())95 uassert.Equal(t, "", b.PendingAdmin().String())96 uassert.Equal(t, 0, b.PermissionCount())97 uassert.True(t, b.IsAdmin(admin))98 uassert.False(t, b.IsAdmin(alice))99100 l := b.Limits()101 uassert.Equal(t, DefaultMaxPermissions, l.MaxPermissions)102 uassert.Equal(t, DefaultMaxHoldersPerPermission, l.MaxHoldersPerPermission)103 uassert.Equal(t, DefaultMaxNameLen, l.MaxNameLen)104}105106func TestNewRejectsInvalidAdmin(cur realm, t *testing.T) {107 b, err := NewDefault(address(""))108 uassert.ErrorIs(t, err, ErrInvalidAddress)109 uassert.True(t, b == nil)110111 b, err = NewDefault(address("not-a-bech32-address"))112 uassert.ErrorIs(t, err, ErrInvalidAddress)113 uassert.True(t, b == nil)114}115116func TestNewRejectsInvalidLimits(cur realm, t *testing.T) {117 cases := []Limits{118 {MaxPermissions: 0, MaxHoldersPerPermission: 10, MaxNameLen: 10},119 {MaxPermissions: 10, MaxHoldersPerPermission: 0, MaxNameLen: 10},120 {MaxPermissions: 10, MaxHoldersPerPermission: 10, MaxNameLen: 0},121 {MaxPermissions: -1, MaxHoldersPerPermission: 10, MaxNameLen: 10},122 {MaxPermissions: MaxPermissionsCeiling + 1, MaxHoldersPerPermission: 10, MaxNameLen: 10},123 {MaxPermissions: 10, MaxHoldersPerPermission: MaxHoldersCeiling + 1, MaxNameLen: 10},124 {MaxPermissions: 10, MaxHoldersPerPermission: 10, MaxNameLen: MaxNameLenCeiling + 1},125 }126 for i, lim := range cases {127 b, err := New(admin, lim)128 uassert.ErrorIs(t, err, ErrInvalidLimits)129 if b != nil {130 t.Errorf("case %d: expected nil book", i)131 }132 }133}134135func TestNewAcceptsCeilingLimits(cur realm, t *testing.T) {136 b, err := New(admin, Limits{137 MaxPermissions: MaxPermissionsCeiling,138 MaxHoldersPerPermission: MaxHoldersCeiling,139 MaxNameLen: MaxNameLenCeiling,140 })141 uassert.NoError(t, err)142 uassert.Equal(t, MaxPermissionsCeiling, b.Limits().MaxPermissions)143}144145// --- grant / query / revoke, the core objective ---146147func TestGrantThenHas(cur realm, t *testing.T) {148 testing.SetRealm(testing.NewUserRealm(admin))149 b := newBook(t, admin)150151 uassert.False(t, b.Has("withdraw", alice))152 uassert.NoError(t, grant(cur, b, "withdraw", alice))153 uassert.True(t, b.Has("withdraw", alice))154155 // granting to alice does not grant to bob156 uassert.False(t, b.Has("withdraw", bob))157 // nor does it create any other permission158 uassert.False(t, b.Has("deposit", alice))159 uassert.Equal(t, 1, b.PermissionCount())160 uassert.Equal(t, 1, b.HolderCount("withdraw"))161}162163func TestRevokeRemovesTheGrant(cur realm, t *testing.T) {164 testing.SetRealm(testing.NewUserRealm(admin))165 b := newBook(t, admin)166167 uassert.NoError(t, grant(cur, b, "withdraw", alice))168 uassert.NoError(t, grant(cur, b, "withdraw", bob))169 uassert.True(t, b.Has("withdraw", alice))170171 uassert.NoError(t, revoke(cur, b, "withdraw", alice))172 uassert.False(t, b.Has("withdraw", alice))173 // bob is untouched174 uassert.True(t, b.Has("withdraw", bob))175 uassert.Equal(t, 1, b.HolderCount("withdraw"))176 // the permission still exists because bob holds it177 uassert.Equal(t, 1, b.PermissionCount())178}179180func TestRevokingLastHolderPrunesThePermission(cur realm, t *testing.T) {181 testing.SetRealm(testing.NewUserRealm(admin))182 b := newBook(t, admin)183184 uassert.NoError(t, grant(cur, b, "withdraw", alice))185 uassert.Equal(t, 1, b.PermissionCount())186187 uassert.NoError(t, revoke(cur, b, "withdraw", alice))188 // the empty bucket is gone, so it does not consume a MaxPermissions slot189 uassert.Equal(t, 0, b.PermissionCount())190 uassert.Equal(t, 0, b.HolderCount("withdraw"))191 // and the query is still well-defined192 uassert.False(t, b.Has("withdraw", alice))193}194195func TestGrantAfterPruneWorks(cur realm, t *testing.T) {196 testing.SetRealm(testing.NewUserRealm(admin))197 b := newBook(t, admin)198199 uassert.NoError(t, grant(cur, b, "withdraw", alice))200 uassert.NoError(t, revoke(cur, b, "withdraw", alice))201 // re-creating a pruned permission must not collide with a stale bucket202 uassert.NoError(t, grant(cur, b, "withdraw", bob))203 uassert.True(t, b.Has("withdraw", bob))204 uassert.False(t, b.Has("withdraw", alice))205 uassert.Equal(t, 1, b.PermissionCount())206}207208func TestDuplicateGrantIsAnError(cur realm, t *testing.T) {209 testing.SetRealm(testing.NewUserRealm(admin))210 b := newBook(t, admin)211212 uassert.NoError(t, grant(cur, b, "withdraw", alice))213 uassert.ErrorIs(t, grant(cur, b, "withdraw", alice), ErrAlreadyGranted)214 // the failed duplicate did not grow state215 uassert.Equal(t, 1, b.HolderCount("withdraw"))216}217218func TestRevokeWhatIsNotHeld(cur realm, t *testing.T) {219 testing.SetRealm(testing.NewUserRealm(admin))220 b := newBook(t, admin)221222 // unknown permission223 uassert.ErrorIs(t, revoke(cur, b, "nope", alice), ErrNotGranted)224 // known permission, wrong holder225 uassert.NoError(t, grant(cur, b, "withdraw", alice))226 uassert.ErrorIs(t, revoke(cur, b, "withdraw", bob), ErrNotGranted)227 uassert.True(t, b.Has("withdraw", alice))228}229230func TestMultiplePermissionsAreIndependent(cur realm, t *testing.T) {231 testing.SetRealm(testing.NewUserRealm(admin))232 b := newBook(t, admin)233234 uassert.NoError(t, grant(cur, b, "read", alice))235 uassert.NoError(t, grant(cur, b, "write", alice))236 uassert.NoError(t, grant(cur, b, "write", bob))237238 uassert.True(t, b.Has("read", alice))239 uassert.True(t, b.Has("write", alice))240 uassert.False(t, b.Has("read", bob))241 uassert.True(t, b.Has("write", bob))242 uassert.Equal(t, 2, b.PermissionCount())243244 uassert.NoError(t, revoke(cur, b, "write", alice))245 // revoking one permission leaves the other intact246 uassert.True(t, b.Has("read", alice))247 uassert.False(t, b.Has("write", alice))248 uassert.True(t, b.Has("write", bob))249}250251// --- authorization ---252253func TestNonAdminCannotGrant(cur realm, t *testing.T) {254 testing.SetRealm(testing.NewUserRealm(mallory))255 b := newBook(t, admin)256257 uassert.ErrorIs(t, grant(cur, b, "withdraw", mallory), ErrUnauthorized)258 uassert.False(t, b.Has("withdraw", mallory))259 uassert.Equal(t, 0, b.PermissionCount())260}261262func TestNonAdminCannotRevoke(cur realm, t *testing.T) {263 testing.SetRealm(testing.NewUserRealm(admin))264 b := newBook(t, admin)265 uassert.NoError(t, grant(cur, b, "withdraw", alice))266267 testing.SetRealm(testing.NewUserRealm(mallory))268 uassert.ErrorIs(t, revoke(cur, b, "withdraw", alice), ErrUnauthorized)269 uassert.True(t, b.Has("withdraw", alice))270}271272// A holder of a permission has no authority over the book. This is the273// property that p/thox/accesscontrol loses by exporting Role.Holders and274// returning *Role: there, any holder can self-grant.275func TestHolderIsNotAdmin(cur realm, t *testing.T) {276 testing.SetRealm(testing.NewUserRealm(admin))277 b := newBook(t, admin)278 uassert.NoError(t, grant(cur, b, "withdraw", alice))279280 testing.SetRealm(testing.NewUserRealm(alice))281 uassert.ErrorIs(t, grant(cur, b, "withdraw", mallory), ErrUnauthorized)282 uassert.ErrorIs(t, revoke(cur, b, "withdraw", alice), ErrUnauthorized)283 uassert.ErrorIs(t, dropPerm(cur, b, "withdraw"), ErrUnauthorized)284 uassert.ErrorIs(t, nominate(cur, b, alice), ErrUnauthorized)285 _, err := revokeAll(cur, b, alice)286 uassert.ErrorIs(t, err, ErrUnauthorized)287288 uassert.False(t, b.Has("withdraw", mallory))289 uassert.True(t, b.Has("withdraw", alice))290 uassert.Equal(t, admin.String(), b.Admin().String())291}292293func TestNonAdminCannotDropOrRevokeAll(cur realm, t *testing.T) {294 testing.SetRealm(testing.NewUserRealm(admin))295 b := newBook(t, admin)296 uassert.NoError(t, grant(cur, b, "a", alice))297 uassert.NoError(t, grant(cur, b, "b", alice))298299 testing.SetRealm(testing.NewUserRealm(mallory))300 uassert.ErrorIs(t, dropPerm(cur, b, "a"), ErrUnauthorized)301 n, err := revokeAll(cur, b, alice)302 uassert.ErrorIs(t, err, ErrUnauthorized)303 uassert.Equal(t, 0, n)304 uassert.Equal(t, 2, b.PermissionCount())305}306307// --- name validation ---308309func TestInvalidPermissionNames(cur realm, t *testing.T) {310 testing.SetRealm(testing.NewUserRealm(admin))311 b := newBook(t, admin)312313 bad := []string{314 "", // empty315 "Withdraw", // uppercase316 "with draw", // space317 "with-draw", // hyphen318 "with.draw", // dot319 "with/draw", // path separator320 "with,draw", // comma — a delimiter in rendered lists321 "[link](x)", // markdown322 "with\ndraw", // newline323 "withdraw!", // punctuation324 "wíthdraw", // non-ascii325 }326 for _, name := range bad {327 uassert.ErrorIs(t, grant(cur, b, name, alice), ErrInvalidName)328 }329 uassert.Equal(t, 0, b.PermissionCount())330331 good := []string{"a", "withdraw", "with_draw", "perm2", "_leading", "trailing_"}332 for _, name := range good {333 uassert.NoError(t, grant(cur, b, name, alice))334 }335 uassert.Equal(t, len(good), b.PermissionCount())336}337338func TestNameLengthBound(cur realm, t *testing.T) {339 testing.SetRealm(testing.NewUserRealm(admin))340 b, err := New(admin, Limits{MaxPermissions: 8, MaxHoldersPerPermission: 8, MaxNameLen: 4})341 uassert.NoError(t, err)342343 uassert.NoError(t, grant(cur, b, "abcd", alice))344 uassert.ErrorIs(t, grant(cur, b, "abcde", alice), ErrInvalidName)345 uassert.Equal(t, 1, b.PermissionCount())346}347348func TestGrantRejectsInvalidAddress(cur realm, t *testing.T) {349 testing.SetRealm(testing.NewUserRealm(admin))350 b := newBook(t, admin)351352 uassert.ErrorIs(t, grant(cur, b, "withdraw", address("")), ErrInvalidAddress)353 uassert.ErrorIs(t, grant(cur, b, "withdraw", address("g1nonsense")), ErrInvalidAddress)354 // a rejected grant must not have created the permission bucket355 uassert.Equal(t, 0, b.PermissionCount())356}357358// --- bounds ---359360func TestPermissionLimit(cur realm, t *testing.T) {361 testing.SetRealm(testing.NewUserRealm(admin))362 b, err := New(admin, Limits{MaxPermissions: 3, MaxHoldersPerPermission: 8, MaxNameLen: 16})363 uassert.NoError(t, err)364365 uassert.NoError(t, grant(cur, b, "p1", alice))366 uassert.NoError(t, grant(cur, b, "p2", alice))367 uassert.NoError(t, grant(cur, b, "p3", alice))368 uassert.ErrorIs(t, grant(cur, b, "p4", alice), ErrPermissionLimit)369 uassert.Equal(t, 3, b.PermissionCount())370371 // granting an EXISTING permission to a new address is not blocked by the372 // permission limit373 uassert.NoError(t, grant(cur, b, "p1", bob))374375 // freeing a slot re-opens it376 uassert.NoError(t, dropPerm(cur, b, "p3"))377 uassert.NoError(t, grant(cur, b, "p4", alice))378 uassert.Equal(t, 3, b.PermissionCount())379}380381func TestHolderLimit(cur realm, t *testing.T) {382 testing.SetRealm(testing.NewUserRealm(admin))383 b, err := New(admin, Limits{MaxPermissions: 4, MaxHoldersPerPermission: 2, MaxNameLen: 16})384 uassert.NoError(t, err)385386 uassert.NoError(t, grant(cur, b, "p", alice))387 uassert.NoError(t, grant(cur, b, "p", bob))388 uassert.ErrorIs(t, grant(cur, b, "p", mallory), ErrHolderLimit)389 uassert.Equal(t, 2, b.HolderCount("p"))390 uassert.False(t, b.Has("p", mallory))391392 // a revoke frees a slot393 uassert.NoError(t, revoke(cur, b, "p", alice))394 uassert.NoError(t, grant(cur, b, "p", mallory))395 uassert.True(t, b.Has("p", mallory))396}397398// A rejected grant must leave no bucket behind that occupies a399// MaxPermissions slot without holding anything.400//401// Two rejection routes exist and they are NOT the same case. A holder-limit402// rejection can only happen on a permission that already had holders, so403// nothing was created to clean up — Grant documents that argument where it404// returns ErrHolderLimit. An invalid-name or invalid-address rejection405// happens before any bucket is created. This asserts the invariant on both406// routes rather than on a branch.407func TestFailedGrantLeavesNoEmptyBucket(cur realm, t *testing.T) {408 testing.SetRealm(testing.NewUserRealm(admin))409 b, err := New(admin, Limits{MaxPermissions: 2, MaxHoldersPerPermission: 1, MaxNameLen: 16})410 uassert.NoError(t, err)411412 // route 1: holder limit, on a permission that already has its holder413 uassert.NoError(t, grant(cur, b, "p1", alice))414 uassert.ErrorIs(t, grant(cur, b, "p1", bob), ErrHolderLimit)415 // p1 still has exactly its one holder and no second bucket appeared416 uassert.Equal(t, 1, b.PermissionCount())417 uassert.Equal(t, 1, b.HolderCount("p1"))418 uassert.True(t, b.Has("p1", alice))419 uassert.False(t, b.Has("p1", bob))420421 // route 2: a rejection that happens before creation. The one free slot422 // is still free afterwards, which is what proves nothing was consumed.423 uassert.ErrorIs(t, grant(cur, b, "BAD", alice), ErrInvalidName)424 uassert.ErrorIs(t, grant(cur, b, "p2", address("")), ErrInvalidAddress)425 uassert.Equal(t, 1, b.PermissionCount())426 uassert.NoError(t, grant(cur, b, "p2", alice))427 uassert.Equal(t, 2, b.PermissionCount())428}429430// --- RevokeAll and DropPermission ---431432func TestRevokeAll(cur realm, t *testing.T) {433 testing.SetRealm(testing.NewUserRealm(admin))434 b := newBook(t, admin)435436 uassert.NoError(t, grant(cur, b, "read", alice))437 uassert.NoError(t, grant(cur, b, "write", alice))438 uassert.NoError(t, grant(cur, b, "admin_panel", alice))439 uassert.NoError(t, grant(cur, b, "read", bob))440441 n, err := revokeAll(cur, b, alice)442 uassert.NoError(t, err)443 uassert.Equal(t, 3, n)444445 uassert.False(t, b.HasAny(alice))446 uassert.False(t, b.Has("read", alice))447 uassert.False(t, b.Has("write", alice))448 uassert.False(t, b.Has("admin_panel", alice))449450 // bob is untouched, and "read" survives because he still holds it451 uassert.True(t, b.Has("read", bob))452 // "write" and "admin_panel" were emptied and therefore pruned453 uassert.Equal(t, 1, b.PermissionCount())454}455456func TestRevokeAllOnAddressWithNothing(cur realm, t *testing.T) {457 testing.SetRealm(testing.NewUserRealm(admin))458 b := newBook(t, admin)459 uassert.NoError(t, grant(cur, b, "read", bob))460461 n, err := revokeAll(cur, b, alice)462 uassert.NoError(t, err)463 uassert.Equal(t, 0, n)464 uassert.True(t, b.Has("read", bob))465}466467func TestDropPermission(cur realm, t *testing.T) {468 testing.SetRealm(testing.NewUserRealm(admin))469 b := newBook(t, admin)470471 uassert.NoError(t, grant(cur, b, "read", alice))472 uassert.NoError(t, grant(cur, b, "read", bob))473 uassert.NoError(t, grant(cur, b, "write", alice))474475 uassert.NoError(t, dropPerm(cur, b, "read"))476 uassert.False(t, b.Has("read", alice))477 uassert.False(t, b.Has("read", bob))478 uassert.Equal(t, 0, b.HolderCount("read"))479 // the other permission survives480 uassert.True(t, b.Has("write", alice))481 uassert.Equal(t, 1, b.PermissionCount())482483 uassert.ErrorIs(t, dropPerm(cur, b, "read"), ErrUnknownPermission)484 uassert.ErrorIs(t, dropPerm(cur, b, "never_existed"), ErrUnknownPermission)485}486487// --- admin handoff ---488489func TestTwoStepAdminHandoff(cur realm, t *testing.T) {490 testing.SetRealm(testing.NewUserRealm(admin))491 b := newBook(t, admin)492493 uassert.NoError(t, nominate(cur, b, nominee))494 // nomination alone does NOT move the admin495 uassert.Equal(t, admin.String(), b.Admin().String())496 uassert.Equal(t, nominee.String(), b.PendingAdmin().String())497 // and the incumbent still has authority498 uassert.NoError(t, grant(cur, b, "read", alice))499500 // the nominee cannot yet act501 testing.SetRealm(testing.NewUserRealm(nominee))502 uassert.ErrorIs(t, grant(cur, b, "write", alice), ErrUnauthorized)503504 uassert.NoError(t, acceptAdmin(cur, b))505 uassert.Equal(t, nominee.String(), b.Admin().String())506 uassert.Equal(t, "", b.PendingAdmin().String())507 uassert.NoError(t, grant(cur, b, "write", alice))508509 // the former admin has lost authority510 testing.SetRealm(testing.NewUserRealm(admin))511 uassert.ErrorIs(t, grant(cur, b, "other", alice), ErrUnauthorized)512513 // grants made under the previous admin survive the handoff514 uassert.True(t, b.Has("read", alice))515}516517func TestOnlyNomineeCanAccept(cur realm, t *testing.T) {518 testing.SetRealm(testing.NewUserRealm(admin))519 b := newBook(t, admin)520 uassert.NoError(t, nominate(cur, b, nominee))521522 testing.SetRealm(testing.NewUserRealm(mallory))523 uassert.ErrorIs(t, acceptAdmin(cur, b), ErrNotPendingAdmin)524 uassert.Equal(t, admin.String(), b.Admin().String())525526 // not even the incumbent admin can accept on the nominee's behalf527 testing.SetRealm(testing.NewUserRealm(admin))528 uassert.ErrorIs(t, acceptAdmin(cur, b), ErrNotPendingAdmin)529 uassert.Equal(t, admin.String(), b.Admin().String())530}531532func TestAcceptWithoutNomination(cur realm, t *testing.T) {533 testing.SetRealm(testing.NewUserRealm(nominee))534 b := newBook(t, admin)535 uassert.ErrorIs(t, acceptAdmin(cur, b), ErrNoPendingAdmin)536 uassert.Equal(t, admin.String(), b.Admin().String())537}538539func TestCancelNomination(cur realm, t *testing.T) {540 testing.SetRealm(testing.NewUserRealm(admin))541 b := newBook(t, admin)542543 uassert.ErrorIs(t, cancelNom(cur, b), ErrNoPendingAdmin)544545 uassert.NoError(t, nominate(cur, b, nominee))546 uassert.NoError(t, cancelNom(cur, b))547 uassert.Equal(t, "", b.PendingAdmin().String())548549 // the withdrawn nominee can no longer accept550 testing.SetRealm(testing.NewUserRealm(nominee))551 uassert.ErrorIs(t, acceptAdmin(cur, b), ErrNoPendingAdmin)552 uassert.Equal(t, admin.String(), b.Admin().String())553}554555func TestRenominationReplacesPending(cur realm, t *testing.T) {556 testing.SetRealm(testing.NewUserRealm(admin))557 b := newBook(t, admin)558559 uassert.NoError(t, nominate(cur, b, nominee))560 uassert.NoError(t, nominate(cur, b, bob))561 uassert.Equal(t, bob.String(), b.PendingAdmin().String())562563 // the superseded nominee cannot accept564 testing.SetRealm(testing.NewUserRealm(nominee))565 uassert.ErrorIs(t, acceptAdmin(cur, b), ErrNotPendingAdmin)566567 testing.SetRealm(testing.NewUserRealm(bob))568 uassert.NoError(t, acceptAdmin(cur, b))569 uassert.Equal(t, bob.String(), b.Admin().String())570}571572func TestNominateRejectsBadAddress(cur realm, t *testing.T) {573 testing.SetRealm(testing.NewUserRealm(admin))574 b := newBook(t, admin)575576 uassert.ErrorIs(t, nominate(cur, b, address("")), ErrInvalidAddress)577 uassert.ErrorIs(t, nominate(cur, b, address("g1bogus")), ErrInvalidAddress)578 uassert.ErrorIs(t, nominate(cur, b, admin), ErrSameAdmin)579 uassert.Equal(t, "", b.PendingAdmin().String())580}581582// --- read-side queries ---583584func TestPermissionsOfAddress(cur realm, t *testing.T) {585 testing.SetRealm(testing.NewUserRealm(admin))586 b := newBook(t, admin)587588 uassert.Equal(t, 0, len(b.Permissions(alice)))589590 uassert.NoError(t, grant(cur, b, "write", alice))591 uassert.NoError(t, grant(cur, b, "audit", alice))592 uassert.NoError(t, grant(cur, b, "read", alice))593 uassert.NoError(t, grant(cur, b, "read", bob))594595 got := b.Permissions(alice)596 uassert.Equal(t, 3, len(got))597 // lexicographic order, which is what makes rendered output deterministic598 uassert.Equal(t, "audit", got[0])599 uassert.Equal(t, "read", got[1])600 uassert.Equal(t, "write", got[2])601602 uassert.Equal(t, 1, len(b.Permissions(bob)))603 uassert.Equal(t, 0, len(b.Permissions(mallory)))604}605606func TestHasAny(cur realm, t *testing.T) {607 testing.SetRealm(testing.NewUserRealm(admin))608 b := newBook(t, admin)609610 uassert.False(t, b.HasAny(alice))611 uassert.NoError(t, grant(cur, b, "read", alice))612 uassert.True(t, b.HasAny(alice))613 uassert.False(t, b.HasAny(bob))614615 uassert.NoError(t, revoke(cur, b, "read", alice))616 uassert.False(t, b.HasAny(alice))617}618619func TestPermissionNamesPagination(cur realm, t *testing.T) {620 testing.SetRealm(testing.NewUserRealm(admin))621 b := newBook(t, admin)622623 for _, p := range []string{"e", "a", "d", "b", "c"} {624 uassert.NoError(t, grant(cur, b, p, alice))625 }626627 all := b.PermissionNames(0, 10)628 uassert.Equal(t, 5, len(all))629 uassert.Equal(t, "a", all[0])630 uassert.Equal(t, "e", all[4])631632 page := b.PermissionNames(1, 2)633 uassert.Equal(t, 2, len(page))634 uassert.Equal(t, "b", page[0])635 uassert.Equal(t, "c", page[1])636637 uassert.Equal(t, 0, len(b.PermissionNames(0, 0)))638 uassert.Equal(t, 0, len(b.PermissionNames(0, -1)))639 uassert.Equal(t, 0, len(b.PermissionNames(99, 5)))640}641642func TestHoldersPagination(cur realm, t *testing.T) {643 testing.SetRealm(testing.NewUserRealm(admin))644 b := newBook(t, admin)645646 uassert.NoError(t, grant(cur, b, "read", alice))647 uassert.NoError(t, grant(cur, b, "read", bob))648 uassert.NoError(t, grant(cur, b, "read", mallory))649650 all := b.Holders("read", 0, 10)651 uassert.Equal(t, 3, len(all))652653 page := b.Holders("read", 0, 2)654 uassert.Equal(t, 2, len(page))655 // sorted order, so pages do not overlap or skip656 uassert.True(t, page[0].String() < page[1].String())657658 uassert.Equal(t, 0, len(b.Holders("read", 0, 0)))659 uassert.Equal(t, 0, len(b.Holders("nope", 0, 10)))660}661662// Queries perform no authentication at all — consumer-contract clause 3.663// Anyone may read; that is deliberate and is asserted so a future change664// that adds a gate is caught as a behavioral break.665func TestQueriesAreUnauthenticated(cur realm, t *testing.T) {666 testing.SetRealm(testing.NewUserRealm(admin))667 b := newBook(t, admin)668 uassert.NoError(t, grant(cur, b, "read", alice))669670 testing.SetRealm(testing.NewUserRealm(mallory))671 uassert.True(t, b.Has("read", alice))672 uassert.Equal(t, 1, b.HolderCount("read"))673 uassert.Equal(t, 1, len(b.Permissions(alice)))674 uassert.Equal(t, admin.String(), b.Admin().String())675}676677// --- the groups base set is never used ---678679// permbook never writes to the Group's base address set, so "in the base680// set" can never be confused with "holds a permission". HasAny consults681// base OR roles; if base were ever written, this would report true.682func TestBaseSetStaysEmpty(cur realm, t *testing.T) {683 testing.SetRealm(testing.NewUserRealm(admin))684 b := newBook(t, admin)685686 uassert.NoError(t, grant(cur, b, "read", alice))687 uassert.NoError(t, revoke(cur, b, "read", alice))688689 // with every role gone, HasAny can only be true via the base set690 uassert.Equal(t, 0, b.PermissionCount())691 uassert.False(t, b.HasAny(alice))692 uassert.False(t, b.HasAny(admin))693}694Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.