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}