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

groups_test.gno

12.01 Kb · 445 lines
  1package groups
  2
  3import (
  4	"strconv"
  5	"strings"
  6	"testing"
  7
  8	"gno.land/p/nt/uassert/v0"
  9	"gno.land/p/nt/urequire/v0"
 10)
 11
 12var (
 13	addr0 = address("addr0")
 14	addr1 = address("addr1")
 15	addr2 = address("addr2")
 16	addr3 = address("addr3")
 17	addr4 = address("addr4")
 18	addr5 = address("addr5")
 19	addr6 = address("addr6")
 20)
 21
 22// newTestGroup builds the fixture used across tests:
 23//
 24//	base:   addr0, addr1, addr3, addr5
 25//	admins: addr2, addr3, addr5
 26//	mods:   addr1, addr4, addr5
 27//
 28// Overlap topology: addr0 base-only, addr2 admins-only, addr4 mods-only,
 29// addr1 base+mods, addr3 base+admins, addr5 base+both roles.
 30// Distinct addresses: addr0..addr5 (addr6 is never a member).
 31func newTestGroup(t *testing.T) *Group {
 32	t.Helper()
 33	g := NewGroup()
 34	g.Add(addr0)
 35	g.Add(addr1)
 36	g.Add(addr3)
 37	g.Add(addr5)
 38
 39	admins, err := g.AddRole("admins")
 40	urequire.NoError(t, err)
 41	admins.Members().Add(addr2)
 42	admins.Members().Add(addr3)
 43	admins.Members().Add(addr5)
 44
 45	mods, err := g.AddRole("mods")
 46	urequire.NoError(t, err)
 47	mods.Members().Add(addr1)
 48	mods.Members().Add(addr4)
 49	mods.Members().Add(addr5)
 50	return g
 51}
 52
 53func joinAddrs(addrs []address) string {
 54	strs := make([]string, 0, len(addrs))
 55	for _, a := range addrs {
 56		strs = append(strs, string(a))
 57	}
 58	return strings.Join(strs, ",")
 59}
 60
 61func TestBaseSet(t *testing.T) {
 62	g := NewGroup()
 63	uassert.Equal(t, 0, g.Size())
 64	uassert.False(t, g.Has(addr1))
 65
 66	uassert.True(t, g.Add(addr1))
 67	uassert.False(t, g.Add(addr1)) // duplicate
 68	uassert.True(t, g.Add(addr2))
 69	uassert.Equal(t, 2, g.Size())
 70	uassert.True(t, g.Has(addr1))
 71	uassert.False(t, g.Has(addr3))
 72
 73	uassert.True(t, g.Remove(addr1))
 74	uassert.False(t, g.Remove(addr1)) // already gone
 75	uassert.Equal(t, 1, g.Size())
 76	uassert.False(t, g.Has(addr1))
 77}
 78
 79func TestIterate(t *testing.T) {
 80	g := NewGroup()
 81	g.Add(addr2)
 82	g.Add(addr1)
 83	g.Add(addr3)
 84
 85	var got []address
 86	stopped := g.Iterate(0, 10, func(a address) bool {
 87		got = append(got, a)
 88		return false
 89	})
 90	uassert.False(t, stopped)
 91	uassert.Equal(t, "addr1,addr2,addr3", joinAddrs(got)) // sorted order
 92
 93	got = nil
 94	g.Iterate(1, 1, func(a address) bool {
 95		got = append(got, a)
 96		return false
 97	})
 98	uassert.Equal(t, "addr2", joinAddrs(got))
 99
100	// Early stop halts iteration, not just the return value.
101	visited := 0
102	stopped = g.Iterate(0, 10, func(a address) bool {
103		visited++
104		return true
105	})
106	uassert.True(t, stopped)
107	uassert.Equal(t, 1, visited)
108
109	// Roles are never consulted.
110	gg := newTestGroup(t)
111	got = nil
112	gg.Iterate(0, 10, func(a address) bool {
113		got = append(got, a)
114		return false
115	})
116	uassert.Equal(t, "addr0,addr1,addr3,addr5", joinAddrs(got))
117}
118
119func TestAddRole(t *testing.T) {
120	g := NewGroup()
121
122	r, err := g.AddRole("admins")
123	urequire.NoError(t, err)
124	uassert.Equal(t, "admins", r.Name())
125	uassert.Equal(t, 0, r.Members().Size())
126	uassert.Equal(t, 1, g.RoleCount())
127
128	_, err = g.AddRole("admins")
129	uassert.ErrorIs(t, err, ErrRoleExists)
130
131	_, err = g.AddRole("")
132	uassert.ErrorIs(t, err, ErrEmptyName)
133
134	uassert.Equal(t, 1, g.RoleCount())
135}
136
137func TestGetRoleHasRole(t *testing.T) {
138	g := newTestGroup(t)
139
140	r, found := g.GetRole("admins")
141	uassert.True(t, found)
142	uassert.Equal(t, "admins", r.Name())
143	uassert.True(t, r.Members().Has(addr2))
144
145	_, found = g.GetRole("nope")
146	uassert.False(t, found)
147
148	uassert.True(t, g.HasRole("mods"))
149	uassert.False(t, g.HasRole("nope"))
150}
151
152func TestRemoveRole(t *testing.T) {
153	g := newTestGroup(t)
154
155	uassert.True(t, g.RemoveRole("admins"))
156	uassert.False(t, g.RemoveRole("admins")) // already gone
157	uassert.Equal(t, 1, g.RoleCount())
158	uassert.False(t, g.HasRole("admins"))
159
160	// Members of the removed role stay in the base set and other roles.
161	uassert.True(t, g.Has(addr3))     // base
162	uassert.True(t, g.HasAny(addr5))  // still in base + mods
163	uassert.False(t, g.HasAny(addr2)) // was only in admins
164}
165
166func TestIterateRoles(t *testing.T) {
167	g := newTestGroup(t)
168
169	var names []string
170	stopped := g.IterateRoles(0, 10, func(rr *ReadonlyRole) bool {
171		names = append(names, rr.Name())
172		return false
173	})
174	uassert.False(t, stopped)
175	uassert.Equal(t, "admins,mods", strings.Join(names, ","))
176
177	// offset/count window.
178	names = nil
179	g.IterateRoles(1, 1, func(rr *ReadonlyRole) bool {
180		names = append(names, rr.Name())
181		return false
182	})
183	uassert.Equal(t, "mods", strings.Join(names, ","))
184
185	// Early stop halts iteration, not just the return value.
186	names = nil
187	stopped = g.IterateRoles(0, 10, func(rr *ReadonlyRole) bool {
188		names = append(names, rr.Name())
189		return true
190	})
191	uassert.True(t, stopped)
192	uassert.Equal(t, "admins", strings.Join(names, ","))
193
194	// Mutation path: capture names during iteration, revisit via GetRole.
195	names = nil
196	g.IterateRoles(0, 10, func(rr *ReadonlyRole) bool {
197		names = append(names, rr.Name())
198		return false
199	})
200	for _, name := range names {
201		r, found := g.GetRole(name)
202		urequire.True(t, found)
203		r.Members().Add(addr6)
204	}
205	uassert.Equal(t, "admins,mods", strings.Join(g.RolesContaining(addr6), ","))
206}
207
208func TestRoleMeta(t *testing.T) {
209	g := NewGroup()
210	r, err := g.AddRole("admins")
211	urequire.NoError(t, err)
212
213	uassert.Nil(t, r.Meta())
214	r.SetMeta("quorum=2/3")
215	uassert.Equal(t, "quorum=2/3", r.Meta())
216	uassert.Equal(t, "quorum=2/3", r.Readonly().Meta())
217	r.SetMeta(nil)
218	uassert.Nil(t, r.Meta())
219}
220
221func TestHasAny(t *testing.T) {
222	g := newTestGroup(t)
223
224	uassert.True(t, g.HasAny(addr0))  // base only
225	uassert.True(t, g.HasAny(addr1))  // base + mods
226	uassert.True(t, g.HasAny(addr2))  // admins only
227	uassert.True(t, g.HasAny(addr4))  // mods only
228	uassert.True(t, g.HasAny(addr5))  // base + both roles
229	uassert.False(t, g.HasAny(addr6)) // nowhere
230
231	// Base-only member, no roles registered at all.
232	g2 := NewGroup()
233	g2.Add(addr1)
234	uassert.True(t, g2.HasAny(addr1))
235	uassert.False(t, g2.HasAny(addr2))
236}
237
238func TestTotalSize(t *testing.T) {
239	uassert.Equal(t, 0, NewGroup().TotalSize())
240
241	g := newTestGroup(t)
242	uassert.Equal(t, 4, g.Size())      // base only
243	uassert.Equal(t, 6, g.TotalSize()) // addr0..addr5, deduplicated
244}
245
246func TestIterateAll(t *testing.T) {
247	g := newTestGroup(t)
248
249	collect := func(offset, count int) (addrs []address, stopped bool) {
250		stopped = g.IterateAll(offset, count, func(a address) bool {
251			addrs = append(addrs, a)
252			return false
253		})
254		return addrs, stopped
255	}
256
257	// Order: base first (sorted), then roles in name order, deduplicated.
258	got, stopped := collect(0, 10)
259	uassert.False(t, stopped)
260	uassert.Equal(t, "addr0,addr1,addr3,addr5,addr2,addr4", joinAddrs(got))
261
262	// offset/count window over the deduplicated output.
263	got, _ = collect(1, 2)
264	uassert.Equal(t, "addr1,addr3", joinAddrs(got))
265	got, _ = collect(3, 10)
266	uassert.Equal(t, "addr5,addr2,addr4", joinAddrs(got))
267	// Window straddling skipped duplicates: count applies to deduplicated
268	// output, not to scanned items.
269	got, _ = collect(4, 2)
270	uassert.Equal(t, "addr2,addr4", joinAddrs(got))
271
272	// Degenerate parameters.
273	got, stopped = collect(6, 10) // offset == total
274	uassert.False(t, stopped)
275	uassert.Equal(t, 0, len(got))
276	got, _ = collect(0, 0)
277	uassert.Equal(t, 0, len(got))
278	got, _ = collect(-1, 2) // negative offset counts as zero
279	uassert.Equal(t, "addr0,addr1", joinAddrs(got))
280
281	// Early stop halts iteration, not just the return value.
282	visited := 0
283	stopped = g.IterateAll(0, 10, func(a address) bool {
284		visited++
285		return true
286	})
287	uassert.True(t, stopped)
288	uassert.Equal(t, 1, visited)
289}
290
291// TestIterateAllWindows pins the pagination contract exhaustively: every
292// (offset, count) window over the deduplicated walk must equal the
293// corresponding slice of the full walk.
294func TestIterateAllWindows(t *testing.T) {
295	g := newTestGroup(t)
296
297	var full []address
298	g.IterateAll(0, 100, func(a address) bool {
299		full = append(full, a)
300		return false
301	})
302	urequire.Equal(t, 6, len(full))
303
304	for offset := 0; offset <= len(full); offset++ {
305		for count := 1; count <= len(full)+1; count++ {
306			var page []address
307			g.IterateAll(offset, count, func(a address) bool {
308				page = append(page, a)
309				return false
310			})
311			end := offset + count
312			if end > len(full) {
313				end = len(full)
314			}
315			label := "offset=" + strconv.Itoa(offset) + " count=" + strconv.Itoa(count)
316			uassert.Equal(t, joinAddrs(full[offset:end]), joinAddrs(page), label)
317		}
318	}
319}
320
321func TestRolesContaining(t *testing.T) {
322	g := newTestGroup(t)
323
324	uassert.Equal(t, "admins,mods", strings.Join(g.RolesContaining(addr5), ","))
325	uassert.Equal(t, "admins", strings.Join(g.RolesContaining(addr2), ","))
326	uassert.Equal(t, "mods", strings.Join(g.RolesContaining(addr1), ",")) // base not consulted
327	uassert.Equal(t, 0, len(g.RolesContaining(addr0)))                    // base-only member
328	uassert.Equal(t, 0, len(g.RolesContaining(addr6)))
329}
330
331func TestRemoveFromAll(t *testing.T) {
332	g := newTestGroup(t)
333
334	uassert.True(t, g.RemoveFromAll(addr5)) // base + both roles
335	uassert.False(t, g.HasAny(addr5))
336
337	uassert.True(t, g.RemoveFromAll(addr0)) // base-only: removed reflects the base hit alone
338	uassert.False(t, g.HasAny(addr0))
339
340	uassert.True(t, g.RemoveFromAll(addr2)) // role-only
341	uassert.False(t, g.HasAny(addr2))
342
343	uassert.False(t, g.RemoveFromAll(addr6)) // nowhere
344
345	uassert.Equal(t, 3, g.TotalSize()) // addr1, addr3, addr4 remain
346	uassert.Equal(t, 2, g.RoleCount()) // roles themselves are kept
347}
348
349// TestRemoveRoleStaleHandle pins the aliasing semantics: a *Role handle
350// held across RemoveRole keeps working as a detached object, but its
351// mutations no longer affect the group, and re-adding the name creates a
352// fresh, independent role.
353func TestRemoveRoleStaleHandle(t *testing.T) {
354	g := newTestGroup(t)
355	stale, found := g.GetRole("admins")
356	urequire.True(t, found)
357
358	uassert.True(t, g.RemoveRole("admins"))
359
360	// The detached role still works as an orphan object...
361	uassert.Equal(t, "admins", stale.Name())
362	stale.Members().Add(addr6)
363	uassert.True(t, stale.Members().Has(addr6))
364
365	// ...but no longer affects the group.
366	uassert.False(t, g.HasAny(addr6))
367	uassert.Equal(t, 0, len(g.RolesContaining(addr6)))
368	uassert.False(t, g.HasAny(addr2))  // addr2 was admins-only
369	uassert.Equal(t, 5, g.TotalSize()) // fixture minus admins-only addr2
370
371	// Re-adding the name yields a fresh empty role, not the stale one.
372	fresh, err := g.AddRole("admins")
373	urequire.NoError(t, err)
374	uassert.Equal(t, 0, fresh.Members().Size())
375	uassert.False(t, g.HasAny(addr6))
376}
377
378func TestReadonlyGroup(t *testing.T) {
379	g := newTestGroup(t)
380	rg := g.Readonly()
381
382	// Base reads.
383	uassert.True(t, rg.Has(addr0))
384	uassert.False(t, rg.Has(addr2))
385	uassert.Equal(t, 4, rg.Size())
386	var got []address
387	stopped := rg.Iterate(0, 10, func(a address) bool {
388		got = append(got, a)
389		return false
390	})
391	uassert.False(t, stopped)
392	uassert.Equal(t, "addr0,addr1,addr3,addr5", joinAddrs(got))
393
394	// Role registry reads.
395	uassert.True(t, rg.HasRole("admins"))
396	uassert.False(t, rg.HasRole("nope"))
397	uassert.Equal(t, 2, rg.RoleCount())
398	var names []string
399	stopped = rg.IterateRoles(0, 10, func(rr *ReadonlyRole) bool {
400		names = append(names, rr.Name())
401		return false
402	})
403	uassert.False(t, stopped)
404	uassert.Equal(t, "admins,mods", strings.Join(names, ","))
405
406	rr, found := rg.GetRole("admins")
407	uassert.True(t, found)
408	uassert.Equal(t, "admins", rr.Name())
409	uassert.True(t, rr.Members().Has(addr2))
410	uassert.Equal(t, 3, rr.Members().Size())
411	_, found = rg.GetRole("nope")
412	uassert.False(t, found)
413
414	// Aggregated reads.
415	uassert.True(t, rg.HasAny(addr4))
416	uassert.False(t, rg.HasAny(addr6))
417	uassert.Equal(t, 6, rg.TotalSize())
418	got = nil
419	stopped = rg.IterateAll(0, 10, func(a address) bool {
420		got = append(got, a)
421		return false
422	})
423	uassert.False(t, stopped)
424	uassert.Equal(t, "addr0,addr1,addr3,addr5,addr2,addr4", joinAddrs(got))
425	uassert.True(t, rg.IterateAll(0, 10, func(a address) bool { return true }))
426	uassert.Equal(t, "admins,mods", strings.Join(rg.RolesContaining(addr5), ","))
427}
428
429func TestReadonlyViewsAreLive(t *testing.T) {
430	g := newTestGroup(t)
431	rg := g.Readonly()
432	admins, found := g.GetRole("admins")
433	urequire.True(t, found)
434	rr := admins.Readonly()
435
436	// Views are thin handles, not snapshots: mutations through the owning
437	// side are immediately visible through previously created views.
438	g.Add(addr6)
439	uassert.Equal(t, 5, rg.Size())
440	uassert.True(t, rg.Has(addr6))
441
442	admins.Members().Add(addr6)
443	uassert.Equal(t, 4, rr.Members().Size())
444	uassert.True(t, rr.Members().Has(addr6))
445}