permbook_test.gno
23.25 Kb · 693 lines
1package permbook
2
3import (
4 "testing"
5
6 "gno.land/p/nt/testutils/v0"
7 "gno.land/p/nt/uassert/v0"
8)
9
10var (
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)
17
18// --- 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 but
22// has no previous frame — Previous() panics with "cannot seek beyond origin
23// caller override". So every mutating call is made from inside a real
24// crossing frame, entered with cross(cur), exactly as p/nt/ownable/v0's own
25// tests do.
26//
27// testing.SetRealm(testing.NewUserRealm(addr)) sets the current realm to a
28// user realm for addr, so the next crossing frame has
29// Previous().Address() == addr — the principal the mutator authorizes
30// against. It is written out at every call site on purpose: the override is
31// scoped to the frame that installs it, so wrapping it in a helper makes it
32// pop the moment the helper returns and the principal comes back empty.
33
34func grant(cur realm, b *Book, perm string, addr address) error {
35 var err error
36 func(cur realm) { err = b.Grant(0, cur, perm, addr) }(cross(cur))
37 return err
38}
39
40func revoke(cur realm, b *Book, perm string, addr address) error {
41 var err error
42 func(cur realm) { err = b.Revoke(0, cur, perm, addr) }(cross(cur))
43 return err
44}
45
46func revokeAll(cur realm, b *Book, addr address) (int, error) {
47 var (
48 n int
49 err error
50 )
51 func(cur realm) { n, err = b.RevokeAll(0, cur, addr) }(cross(cur))
52 return n, err
53}
54
55func dropPerm(cur realm, b *Book, perm string) error {
56 var err error
57 func(cur realm) { err = b.DropPermission(0, cur, perm) }(cross(cur))
58 return err
59}
60
61func nominate(cur realm, b *Book, addr address) error {
62 var err error
63 func(cur realm) { err = b.NominateAdmin(0, cur, addr) }(cross(cur))
64 return err
65}
66
67func cancelNom(cur realm, b *Book) error {
68 var err error
69 func(cur realm) { err = b.CancelNomination(0, cur) }(cross(cur))
70 return err
71}
72
73func acceptAdmin(cur realm, b *Book) error {
74 var err error
75 func(cur realm) { err = b.AcceptAdmin(0, cur) }(cross(cur))
76 return err
77}
78
79// 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 b
88}
89
90// --- construction ---
91
92func 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))
99
100 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}
105
106func TestNewRejectsInvalidAdmin(cur realm, t *testing.T) {
107 b, err := NewDefault(address(""))
108 uassert.ErrorIs(t, err, ErrInvalidAddress)
109 uassert.True(t, b == nil)
110
111 b, err = NewDefault(address("not-a-bech32-address"))
112 uassert.ErrorIs(t, err, ErrInvalidAddress)
113 uassert.True(t, b == nil)
114}
115
116func 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}
134
135func 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}
144
145// --- grant / query / revoke, the core objective ---
146
147func TestGrantThenHas(cur realm, t *testing.T) {
148 testing.SetRealm(testing.NewUserRealm(admin))
149 b := newBook(t, admin)
150
151 uassert.False(t, b.Has("withdraw", alice))
152 uassert.NoError(t, grant(cur, b, "withdraw", alice))
153 uassert.True(t, b.Has("withdraw", alice))
154
155 // granting to alice does not grant to bob
156 uassert.False(t, b.Has("withdraw", bob))
157 // nor does it create any other permission
158 uassert.False(t, b.Has("deposit", alice))
159 uassert.Equal(t, 1, b.PermissionCount())
160 uassert.Equal(t, 1, b.HolderCount("withdraw"))
161}
162
163func TestRevokeRemovesTheGrant(cur realm, t *testing.T) {
164 testing.SetRealm(testing.NewUserRealm(admin))
165 b := newBook(t, admin)
166
167 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))
170
171 uassert.NoError(t, revoke(cur, b, "withdraw", alice))
172 uassert.False(t, b.Has("withdraw", alice))
173 // bob is untouched
174 uassert.True(t, b.Has("withdraw", bob))
175 uassert.Equal(t, 1, b.HolderCount("withdraw"))
176 // the permission still exists because bob holds it
177 uassert.Equal(t, 1, b.PermissionCount())
178}
179
180func TestRevokingLastHolderPrunesThePermission(cur realm, t *testing.T) {
181 testing.SetRealm(testing.NewUserRealm(admin))
182 b := newBook(t, admin)
183
184 uassert.NoError(t, grant(cur, b, "withdraw", alice))
185 uassert.Equal(t, 1, b.PermissionCount())
186
187 uassert.NoError(t, revoke(cur, b, "withdraw", alice))
188 // the empty bucket is gone, so it does not consume a MaxPermissions slot
189 uassert.Equal(t, 0, b.PermissionCount())
190 uassert.Equal(t, 0, b.HolderCount("withdraw"))
191 // and the query is still well-defined
192 uassert.False(t, b.Has("withdraw", alice))
193}
194
195func TestGrantAfterPruneWorks(cur realm, t *testing.T) {
196 testing.SetRealm(testing.NewUserRealm(admin))
197 b := newBook(t, admin)
198
199 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 bucket
202 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}
207
208func TestDuplicateGrantIsAnError(cur realm, t *testing.T) {
209 testing.SetRealm(testing.NewUserRealm(admin))
210 b := newBook(t, admin)
211
212 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 state
215 uassert.Equal(t, 1, b.HolderCount("withdraw"))
216}
217
218func TestRevokeWhatIsNotHeld(cur realm, t *testing.T) {
219 testing.SetRealm(testing.NewUserRealm(admin))
220 b := newBook(t, admin)
221
222 // unknown permission
223 uassert.ErrorIs(t, revoke(cur, b, "nope", alice), ErrNotGranted)
224 // known permission, wrong holder
225 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}
229
230func TestMultiplePermissionsAreIndependent(cur realm, t *testing.T) {
231 testing.SetRealm(testing.NewUserRealm(admin))
232 b := newBook(t, admin)
233
234 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))
237
238 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())
243
244 uassert.NoError(t, revoke(cur, b, "write", alice))
245 // revoking one permission leaves the other intact
246 uassert.True(t, b.Has("read", alice))
247 uassert.False(t, b.Has("write", alice))
248 uassert.True(t, b.Has("write", bob))
249}
250
251// --- authorization ---
252
253func TestNonAdminCannotGrant(cur realm, t *testing.T) {
254 testing.SetRealm(testing.NewUserRealm(mallory))
255 b := newBook(t, admin)
256
257 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}
261
262func 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))
266
267 testing.SetRealm(testing.NewUserRealm(mallory))
268 uassert.ErrorIs(t, revoke(cur, b, "withdraw", alice), ErrUnauthorized)
269 uassert.True(t, b.Has("withdraw", alice))
270}
271
272// A holder of a permission has no authority over the book. This is the
273// property that p/thox/accesscontrol loses by exporting Role.Holders and
274// 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))
279
280 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)
287
288 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}
292
293func 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))
298
299 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}
306
307// --- name validation ---
308
309func TestInvalidPermissionNames(cur realm, t *testing.T) {
310 testing.SetRealm(testing.NewUserRealm(admin))
311 b := newBook(t, admin)
312
313 bad := []string{
314 "", // empty
315 "Withdraw", // uppercase
316 "with draw", // space
317 "with-draw", // hyphen
318 "with.draw", // dot
319 "with/draw", // path separator
320 "with,draw", // comma — a delimiter in rendered lists
321 "[link](x)", // markdown
322 "with\ndraw", // newline
323 "withdraw!", // punctuation
324 "wíthdraw", // non-ascii
325 }
326 for _, name := range bad {
327 uassert.ErrorIs(t, grant(cur, b, name, alice), ErrInvalidName)
328 }
329 uassert.Equal(t, 0, b.PermissionCount())
330
331 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}
337
338func 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)
342
343 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}
347
348func TestGrantRejectsInvalidAddress(cur realm, t *testing.T) {
349 testing.SetRealm(testing.NewUserRealm(admin))
350 b := newBook(t, admin)
351
352 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 bucket
355 uassert.Equal(t, 0, b.PermissionCount())
356}
357
358// --- bounds ---
359
360func 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)
364
365 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())
370
371 // granting an EXISTING permission to a new address is not blocked by the
372 // permission limit
373 uassert.NoError(t, grant(cur, b, "p1", bob))
374
375 // freeing a slot re-opens it
376 uassert.NoError(t, dropPerm(cur, b, "p3"))
377 uassert.NoError(t, grant(cur, b, "p4", alice))
378 uassert.Equal(t, 3, b.PermissionCount())
379}
380
381func 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)
385
386 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))
391
392 // a revoke frees a slot
393 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}
397
398// A rejected grant must leave no bucket behind that occupies a
399// MaxPermissions slot without holding anything.
400//
401// Two rejection routes exist and they are NOT the same case. A holder-limit
402// rejection can only happen on a permission that already had holders, so
403// nothing was created to clean up — Grant documents that argument where it
404// returns ErrHolderLimit. An invalid-name or invalid-address rejection
405// happens before any bucket is created. This asserts the invariant on both
406// 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)
411
412 // route 1: holder limit, on a permission that already has its holder
413 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 appeared
416 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))
420
421 // route 2: a rejection that happens before creation. The one free slot
422 // 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}
429
430// --- RevokeAll and DropPermission ---
431
432func TestRevokeAll(cur realm, t *testing.T) {
433 testing.SetRealm(testing.NewUserRealm(admin))
434 b := newBook(t, admin)
435
436 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))
440
441 n, err := revokeAll(cur, b, alice)
442 uassert.NoError(t, err)
443 uassert.Equal(t, 3, n)
444
445 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))
449
450 // bob is untouched, and "read" survives because he still holds it
451 uassert.True(t, b.Has("read", bob))
452 // "write" and "admin_panel" were emptied and therefore pruned
453 uassert.Equal(t, 1, b.PermissionCount())
454}
455
456func 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))
460
461 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}
466
467func TestDropPermission(cur realm, t *testing.T) {
468 testing.SetRealm(testing.NewUserRealm(admin))
469 b := newBook(t, admin)
470
471 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))
474
475 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 survives
480 uassert.True(t, b.Has("write", alice))
481 uassert.Equal(t, 1, b.PermissionCount())
482
483 uassert.ErrorIs(t, dropPerm(cur, b, "read"), ErrUnknownPermission)
484 uassert.ErrorIs(t, dropPerm(cur, b, "never_existed"), ErrUnknownPermission)
485}
486
487// --- admin handoff ---
488
489func TestTwoStepAdminHandoff(cur realm, t *testing.T) {
490 testing.SetRealm(testing.NewUserRealm(admin))
491 b := newBook(t, admin)
492
493 uassert.NoError(t, nominate(cur, b, nominee))
494 // nomination alone does NOT move the admin
495 uassert.Equal(t, admin.String(), b.Admin().String())
496 uassert.Equal(t, nominee.String(), b.PendingAdmin().String())
497 // and the incumbent still has authority
498 uassert.NoError(t, grant(cur, b, "read", alice))
499
500 // the nominee cannot yet act
501 testing.SetRealm(testing.NewUserRealm(nominee))
502 uassert.ErrorIs(t, grant(cur, b, "write", alice), ErrUnauthorized)
503
504 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))
508
509 // the former admin has lost authority
510 testing.SetRealm(testing.NewUserRealm(admin))
511 uassert.ErrorIs(t, grant(cur, b, "other", alice), ErrUnauthorized)
512
513 // grants made under the previous admin survive the handoff
514 uassert.True(t, b.Has("read", alice))
515}
516
517func 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))
521
522 testing.SetRealm(testing.NewUserRealm(mallory))
523 uassert.ErrorIs(t, acceptAdmin(cur, b), ErrNotPendingAdmin)
524 uassert.Equal(t, admin.String(), b.Admin().String())
525
526 // not even the incumbent admin can accept on the nominee's behalf
527 testing.SetRealm(testing.NewUserRealm(admin))
528 uassert.ErrorIs(t, acceptAdmin(cur, b), ErrNotPendingAdmin)
529 uassert.Equal(t, admin.String(), b.Admin().String())
530}
531
532func 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}
538
539func TestCancelNomination(cur realm, t *testing.T) {
540 testing.SetRealm(testing.NewUserRealm(admin))
541 b := newBook(t, admin)
542
543 uassert.ErrorIs(t, cancelNom(cur, b), ErrNoPendingAdmin)
544
545 uassert.NoError(t, nominate(cur, b, nominee))
546 uassert.NoError(t, cancelNom(cur, b))
547 uassert.Equal(t, "", b.PendingAdmin().String())
548
549 // the withdrawn nominee can no longer accept
550 testing.SetRealm(testing.NewUserRealm(nominee))
551 uassert.ErrorIs(t, acceptAdmin(cur, b), ErrNoPendingAdmin)
552 uassert.Equal(t, admin.String(), b.Admin().String())
553}
554
555func TestRenominationReplacesPending(cur realm, t *testing.T) {
556 testing.SetRealm(testing.NewUserRealm(admin))
557 b := newBook(t, admin)
558
559 uassert.NoError(t, nominate(cur, b, nominee))
560 uassert.NoError(t, nominate(cur, b, bob))
561 uassert.Equal(t, bob.String(), b.PendingAdmin().String())
562
563 // the superseded nominee cannot accept
564 testing.SetRealm(testing.NewUserRealm(nominee))
565 uassert.ErrorIs(t, acceptAdmin(cur, b), ErrNotPendingAdmin)
566
567 testing.SetRealm(testing.NewUserRealm(bob))
568 uassert.NoError(t, acceptAdmin(cur, b))
569 uassert.Equal(t, bob.String(), b.Admin().String())
570}
571
572func TestNominateRejectsBadAddress(cur realm, t *testing.T) {
573 testing.SetRealm(testing.NewUserRealm(admin))
574 b := newBook(t, admin)
575
576 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}
581
582// --- read-side queries ---
583
584func TestPermissionsOfAddress(cur realm, t *testing.T) {
585 testing.SetRealm(testing.NewUserRealm(admin))
586 b := newBook(t, admin)
587
588 uassert.Equal(t, 0, len(b.Permissions(alice)))
589
590 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))
594
595 got := b.Permissions(alice)
596 uassert.Equal(t, 3, len(got))
597 // lexicographic order, which is what makes rendered output deterministic
598 uassert.Equal(t, "audit", got[0])
599 uassert.Equal(t, "read", got[1])
600 uassert.Equal(t, "write", got[2])
601
602 uassert.Equal(t, 1, len(b.Permissions(bob)))
603 uassert.Equal(t, 0, len(b.Permissions(mallory)))
604}
605
606func TestHasAny(cur realm, t *testing.T) {
607 testing.SetRealm(testing.NewUserRealm(admin))
608 b := newBook(t, admin)
609
610 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))
614
615 uassert.NoError(t, revoke(cur, b, "read", alice))
616 uassert.False(t, b.HasAny(alice))
617}
618
619func TestPermissionNamesPagination(cur realm, t *testing.T) {
620 testing.SetRealm(testing.NewUserRealm(admin))
621 b := newBook(t, admin)
622
623 for _, p := range []string{"e", "a", "d", "b", "c"} {
624 uassert.NoError(t, grant(cur, b, p, alice))
625 }
626
627 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])
631
632 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])
636
637 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}
641
642func TestHoldersPagination(cur realm, t *testing.T) {
643 testing.SetRealm(testing.NewUserRealm(admin))
644 b := newBook(t, admin)
645
646 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))
649
650 all := b.Holders("read", 0, 10)
651 uassert.Equal(t, 3, len(all))
652
653 page := b.Holders("read", 0, 2)
654 uassert.Equal(t, 2, len(page))
655 // sorted order, so pages do not overlap or skip
656 uassert.True(t, page[0].String() < page[1].String())
657
658 uassert.Equal(t, 0, len(b.Holders("read", 0, 0)))
659 uassert.Equal(t, 0, len(b.Holders("nope", 0, 10)))
660}
661
662// Queries perform no authentication at all — consumer-contract clause 3.
663// Anyone may read; that is deliberate and is asserted so a future change
664// 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))
669
670 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}
676
677// --- the groups base set is never used ---
678
679// permbook never writes to the Group's base address set, so "in the base
680// set" can never be confused with "holds a permission". HasAny consults
681// 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)
685
686 uassert.NoError(t, grant(cur, b, "read", alice))
687 uassert.NoError(t, revoke(cur, b, "read", alice))
688
689 // with every role gone, HasAny can only be true via the base set
690 uassert.Equal(t, 0, b.PermissionCount())
691 uassert.False(t, b.HasAny(alice))
692 uassert.False(t, b.HasAny(admin))
693}