Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}