Kind Realm (renderable)
Name users
Namespace sys
Exported functions 21
Module gno.land/r/sys/users
gno 0.9 users_test.gno gno
⧉
1 package users 2 3 import ( 4 "strconv" 5 "strings" 6 "testing" 7 8 AddControllerAtGenesis (interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string)
Canonicalize (name string) string
Controllers () [].uverse.address
GetReadonlyAddrStore () *gno.land/p/nt/bptree/rotree/v0.ReadOnlyTree
GetReadOnlyNameStore () *gno.land/p/nt/bptree/rotree/v0.ReadOnlyTree
IsCanonicalTaken (name string) (existing string, taken bool)
IsController (addr string) bool
IsNameTaken (name string) bool
NewErrNotWhitelisted (int, caller interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}) struct{Caller string}
ProposeControllerAdditionAndRemoval (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, toAdd string, toRemove string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
ProposeControllerRemoval (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
ProposeDeleteUser (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
ProposeNewController (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
ProposeRegisterUser (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string, addr string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
ProposeUpdateName (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string, newName string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
RegisterUser (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string, address_XXX string) interface {Error func() string}
RegisterUserIgnoreCanonical (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string, address_XXX string) interface {Error func() string}
Render (string) string
ResolveAddress (addr string) *gno.land/r/sys/users.UserData
ResolveAny (input string) (*gno.land/r/sys/users.UserData, bool)
ResolveName (name string) (data *gno.land/r/sys/users.UserData, isCurrent bool)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.
"gno.land/p/nt/testutils/v0"
9 "gno.land/p/nt/uassert/v0"
10 "gno.land/p/nt/urequire/v0"
11 )
12
13 // cur is a zero-value realm used as a placeholder when forwarding to
14 // uassert/urequire dispatch helpers that gained an `rlm realm` param.
15 // These tests pass `func()` callbacks (no crossing inside the callback),
16 // so rlm is ignored — a nil realm here is safe.
17 var cur realm
18
19 func TestResolveName(cur realm, t *testing.T) {
20 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
21
22 t.Run( "single_name" , func (t *testing.T) {
23 cleanStore(t)
24
25 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
26
27 res, isLatest := ResolveName(alice)
28 uassert.Equal(t, aliceAddr, res.Addr())
29 uassert.Equal(t, alice, res.Name())
30 uassert.True(t, isLatest)
31 })
32
33 t.Run( "name+Alias" , func (t *testing.T) {
34 cleanStore(t)
35
36 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
37 data, _ := ResolveName(alice)
38 urequire.NoError(t, data.UpdateName( 0 , cur, "alice1" ))
39
40 res, isLatest := ResolveName( "alice1" )
41 urequire.NotEqual(t, nil , res)
42
43 uassert.Equal(t, aliceAddr, res.Addr())
44 uassert.Equal(t, "alice1" , res.Name())
45 uassert.True(t, isLatest)
46 })
47
48 t.Run( "multiple_aliases" , func (t *testing.T) {
49 cleanStore(t)
50
51 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
52
53 // RegisterUser and check each Alias
54 var names [] string
55 names = append(names, alice)
56 for i := 0 ; i < 5 ; i++ {
57 alias := "alice" + strconv.Itoa(i)
58 names = append(names, alias)
59
60 data, _ := ResolveName(alice)
61 urequire.NoError(t, data.UpdateName( 0 , cur, alias))
62 }
63
64 for _, alias := range names {
65 res, _ := ResolveName(alias)
66 urequire.NotEqual(t, nil , res)
67
68 uassert.Equal(t, aliceAddr, res.Addr())
69 uassert.Equal(t, "alice4" , res.Name())
70 }
71 })
72 }
73
74 func TestResolveAddress(cur realm, t *testing.T) {
75 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
76
77 t.Run( "single_name" , func (t *testing.T) {
78 cleanStore(t)
79
80 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
81
82 res := ResolveAddress(aliceAddr)
83
84 uassert.Equal(t, aliceAddr, res.Addr())
85 uassert.Equal(t, alice, res.Name())
86 })
87
88 t.Run( "name+Alias" , func (t *testing.T) {
89 cleanStore(t)
90
91 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
92 data, _ := ResolveName(alice)
93 urequire.NoError(t, data.UpdateName( 0 , cur, "alice1" ))
94
95 res := ResolveAddress(aliceAddr)
96 urequire.NotEqual(t, nil , res)
97
98 uassert.Equal(t, aliceAddr, res.Addr())
99 uassert.Equal(t, "alice1" , res.Name())
100 })
101
102 t.Run( "multiple_aliases" , func (t *testing.T) {
103 cleanStore(t)
104
105 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
106
107 // RegisterUser and check each Alias
108 var names [] string
109 names = append(names, alice)
110
111 for i := 0 ; i < 5 ; i++ {
112 alias := "alice" + strconv.Itoa(i)
113 names = append(names, alias)
114 data, _ := ResolveName(alice)
115 urequire.NoError(t, data.UpdateName( 0 , cur, alias))
116 }
117
118 res := ResolveAddress(aliceAddr)
119 uassert.Equal(t, aliceAddr, res.Addr())
120 uassert.Equal(t, "alice4" , res.Name())
121 })
122 }
123
124 func TestROStores(cur realm, t *testing.T) {
125 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
126 cleanStore(t)
127
128 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
129 roNS := GetReadOnlyNameStore()
130 roAS := GetReadonlyAddrStore()
131
132 t.Run( "get user data" , func (t *testing.T) {
133 // Name store
134 aliceDataRaw := roNS.Get(alice)
135 urequire.NotNil(t, aliceDataRaw)
136
137 roData, ok := aliceDataRaw.(*UserData)
138 uassert.True(t, ok, "Could not cast data from RO tree to UserData" )
139
140 // Try to modify data
141 roData.Delete( 0 , cur)
142 raw := nameStore.Get(alice)
143 uassert.False(t, raw.(*UserData).deleted)
144
145 // Addr store
146 aliceDataRaw = roAS.Get(aliceAddr.String())
147 urequire.NotNil(t, aliceDataRaw)
148
149 roData, ok = aliceDataRaw.(*UserData)
150 uassert.True(t, ok, "Could not cast data from RO tree to UserData" )
151
152 // Try to modify data
153 roData.Delete( 0 , cur)
154 raw = nameStore.Get(alice)
155 uassert.False(t, raw.(*UserData).deleted)
156 })
157
158 t.Run( "get deleted data" , func (t *testing.T) {
159 raw := nameStore.Get(alice)
160 aliceData := raw.(*UserData)
161
162 urequire.NoError(t, aliceData.Delete( 0 , cur))
163 urequire.True(t, aliceData.IsDeleted())
164
165 // Should be nil because of makeSafeFn intercepting the value.
166 rawRoData := roNS.Get(alice)
167 uassert.Equal(t, rawRoData, nil )
168 _, ok := rawRoData.(*UserData) // shouldn't be castable
169 uassert.False(t, ok)
170 })
171 }
172
173 func TestResolveAny(cur realm, t *testing.T) {
174 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
175
176 t.Run( "name" , func (t *testing.T) {
177 cleanStore(t)
178
179 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
180
181 res, _ := ResolveAny(alice)
182
183 uassert.Equal(t, aliceAddr, res.Addr())
184 uassert.Equal(t, alice, res.Name())
185 })
186
187 t.Run( "address" , func (t *testing.T) {
188 cleanStore(t)
189
190 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
191
192 res, _ := ResolveAny(aliceAddr.String())
193
194 uassert.Equal(t, aliceAddr, res.Addr())
195 uassert.Equal(t, alice, res.Name())
196 })
197
198 t.Run( "not_registered" , func (t *testing.T) {
199 cleanStore(t)
200
201 res, _ := ResolveAny(aliceAddr.String())
202
203 uassert.Equal(t, nil , res)
204 })
205 }
206
207 func TestProposeErrors(cur realm, t *testing.T) {
208 t.Run( "propose_register_user_errors" , func (t *testing.T) {
209 urequire.PanicsWithMessage(t, cur, ErrInvalidUsername.Error(), func () {
210 ProposeRegisterUser(cur, "bad name" , aliceAddr)
211 })
212 urequire.PanicsWithMessage(t, cur, ErrInvalidAddress.Error(), func () {
213 ProposeRegisterUser(cur, alice, "badaddress" )
214 })
215 })
216
217 t.Run( "propose_update_name_errors" , func (t *testing.T) {
218 cleanStore(t)
219
220 urequire.PanicsWithMessage(t, cur, ErrInvalidAddress.Error(), func () {
221 ProposeUpdateName(cur, "badaddress" , "alice1" )
222 })
223 urequire.PanicsWithMessage(t, cur, ErrInvalidUsername.Error(), func () {
224 ProposeUpdateName(cur, aliceAddr, "bad name" )
225 })
226 // Note: unregistered user is not checked at proposal creation time.
227 // The callback handles it at execution time.
228 })
229
230 t.Run( "propose_delete_user_errors" , func (t *testing.T) {
231 cleanStore(t)
232
233 urequire.PanicsWithMessage(t, cur, ErrInvalidAddress.Error(), func () {
234 ProposeDeleteUser(cur, "badaddress" )
235 })
236 // Note: unregistered user is not checked at proposal creation time.
237 // The callback handles it at execution time.
238 })
239 }
240
241 // Audit finding #6: ProposeControllerAdditionAndRemoval used to return early
242 // from its callback when toAdd was already whitelisted (addToWhitelist
243 // returned ErrAlreadyWhitelisted), skipping the remove step entirely. A
244 // passed swap proposal would silently leave the old controller active.
245 //
246 // applyControllerSwap is the extracted callback body. These tests exercise
247 // it directly to confirm:
248 // - normal swap (toAdd new, toRemove present) succeeds and updates state
249 // - toAdd already present is benign — remove still happens
250 // - toRemove already absent is benign — add still happens, no error
251 // - errors that aren't the idempotency cases still propagate
252 func TestApplyControllerSwap(t *testing.T) {
253 // Use unique addresses per subtest to avoid having to drain the
254 // controllers set (addrset.Set has no clear/iterate-all method, and
255 // state persists across subtests in this package's tests).
256 t.Run( "normal swap A->B" , func (t *testing.T) {
257 a := testutils.TestAddress( "swapA1" )
258 b := testutils.TestAddress( "swapB1" )
259 controllers.Add(a)
260 defer controllers.Remove(b)
261
262 uassert.NoError(t, applyControllerSwap(b, a))
263 uassert.True(t, controllers.Has(b), "b should be whitelisted" )
264 uassert.False(t, controllers.Has(a), "a should be removed" )
265 })
266
267 t.Run( "toAdd already whitelisted, toRemove present" , func (t *testing.T) {
268 // Regression for audit finding #6. Before the fix, this returned
269 // ErrAlreadyWhitelisted from the swap callback, skipping the remove
270 // step entirely — the swap silently no-op'd and the old controller
271 // stayed active.
272 a := testutils.TestAddress( "swapA2" )
273 b := testutils.TestAddress( "swapB2" )
274 controllers.Add(a)
275 controllers.Add(b)
276 defer controllers.Remove(b)
277
278 uassert.NoError(t, applyControllerSwap(b, a))
279 uassert.True(t, controllers.Has(b), "b should remain whitelisted" )
280 uassert.False(t, controllers.Has(a), "a should be removed even though b was already in" )
281 })
282
283 t.Run( "toRemove already absent, toAdd new" , func (t *testing.T) {
284 a := testutils.TestAddress( "swapA3" )
285 b := testutils.TestAddress( "swapB3" )
286 c := testutils.TestAddress( "swapC3" ) // never added
287 controllers.Add(a)
288 defer controllers.Remove(a)
289 defer controllers.Remove(b)
290
291 // Remove c which isn't in the set; should be benign.
292 uassert.NoError(t, applyControllerSwap(b, c))
293 uassert.True(t, controllers.Has(b), "b should be whitelisted" )
294 uassert.True(t, controllers.Has(a), "a should still be whitelisted (unchanged)" )
295 uassert.False(t, controllers.Has(c), "c was never in the set" )
296 })
297
298 t.Run( "both idempotency cases at once" , func (t *testing.T) {
299 // toAdd already in, toRemove already out — should succeed cleanly,
300 // no state change.
301 a := testutils.TestAddress( "swapA4" )
302 b := testutils.TestAddress( "swapB4" )
303 c := testutils.TestAddress( "swapC4" )
304 controllers.Add(a)
305 controllers.Add(b)
306 defer controllers.Remove(a)
307 defer controllers.Remove(b)
308
309 uassert.NoError(t, applyControllerSwap(b, c))
310 uassert.True(t, controllers.Has(a))
311 uassert.True(t, controllers.Has(b))
312 uassert.False(t, controllers.Has(c))
313 })
314 }
315
316 // Audit finding #20: the controller whitelist must be queryable from
317 // outside the package so operators can monitor authority without
318 // source-diving or replaying governance proposals.
319 func TestControllerQueries(t *testing.T) {
320 t.Run( "IsController reflects whitelist state" , func (t *testing.T) {
321 a := testutils.TestAddress( "queryA1" )
322 b := testutils.TestAddress( "queryB1" )
323
324 // Before any add: both report false.
325 uassert.False(t, IsController(a))
326 uassert.False(t, IsController(b))
327
328 controllers.Add(a)
329 defer controllers.Remove(a)
330
331 uassert.True(t, IsController(a))
332 uassert.False(t, IsController(b))
333 })
334
335 t.Run( "Controllers returns a snapshot of current whitelist" , func (t *testing.T) {
336 // Use distinct addresses that aren't already in the set from
337 // other tests in this file.
338 a := testutils.TestAddress( "queryA2" )
339 b := testutils.TestAddress( "queryB2" )
340 c := testutils.TestAddress( "queryC2" )
341
342 controllers.Add(a)
343 controllers.Add(b)
344 controllers.Add(c)
345 defer controllers.Remove(a)
346 defer controllers.Remove(b)
347 defer controllers.Remove(c)
348
349 got := Controllers()
350
351 // All three must appear (regardless of order vs other tests'
352 // leftover entries — we only assert ours are present).
353 seen := map [ string ] bool {}
354 for _, e := range got {
355 seen[e.String()] = true
356 }
357 uassert.True(t, seen[a.String()], "snapshot must contain a" )
358 uassert.True(t, seen[b.String()], "snapshot must contain b" )
359 uassert.True(t, seen[c.String()], "snapshot must contain c" )
360 })
361
362 t.Run( "Controllers returns a copy — caller mutation does not affect realm" , func (t *testing.T) {
363 a := testutils.TestAddress( "queryA3" )
364 controllers.Add(a)
365 defer controllers.Remove(a)
366
367 got := Controllers()
368 // Mutate the returned slice. The realm's controllers set must not
369 // be affected.
370 got = got[: 0 ]
371
372 uassert.True(t, controllers.Has(a), "realm state must be unaffected by caller-side slice mutation" )
373 uassert.True(t, IsController(a))
374 })
375 }
376
377 // ProposeRegisterUser auto-injects a CANONICAL COLLISION warning into
378 // the proposal description when the proposed name canonical-collides
379 // with an existing registration. The warning surfaces the colliding
380 // existing name to voters; the proposal still goes through if voted in
381 // (decision #3, DAO grants always bypass).
382 func TestProposeRegisterUser_CollisionWarning(cur realm, t *testing.T) {
383 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
384
385 t.Run( "warning_injected_on_collision" , func (t *testing.T) {
386 cleanStore(t)
387 urequire.NoError(t, RegisterUser(cross(cur), "vitalik" , aliceAddr))
388
389 req := ProposeRegisterUser(cur, "vital1k" , bobAddr)
390 uassert.True(t,
391 strings.Contains(req.Description(), "CANONICAL COLLISION" ),
392 "description must include collision warning" )
393 uassert.True(t,
394 strings.Contains(req.Description(), "`vitalik`" ),
395 "description must name the existing colliding registration" )
396 })
397
398 t.Run( "no_warning_when_no_collision" , func (t *testing.T) {
399 cleanStore(t)
400
401 req := ProposeRegisterUser(cur, "freshname" , aliceAddr)
402 uassert.False(t,
403 strings.Contains(req.Description(), "CANONICAL COLLISION" ),
404 "description must not include collision warning when name is fresh" )
405 })
406 }
407
408 // Note: full execution of ProposeRegisterUser/ProposeUpdateName closures
409 // (with ignoreCanonical=true → later-wins overwrite) is exercised end-to-
410 // end by the integration test in gno.land/pkg/integration/testdata/.
411 // The bypass-write semantics themselves are covered here at the unit
412 // level by TestRegisterUserIgnoreCanonical and TestUpdateName_CanonicalCollision.
413
414 // TODO Uncomment after gnoweb /u/ page.
415 //func TestUserRenderLink(cur realm, t *testing.T) {
416 // testing.SetOriginCaller(whitelistedCallerAddr)
417 // cleanStore(t)
418 //
419 // urequire.NoError(t, RegisterUser(alice, aliceAddr))
420 //
421 // data, _ := ResolveName(alice)
422 // uassert.Equal(t, data.RenderLink(""), ufmt.Sprintf("[@%s](/u/%s)", alice, alice))
423 // text := "my link text!"
424 // uassert.Equal(t, data.RenderLink(text), ufmt.Sprintf("[%s](/u/%s)", text, alice))
425 //}
426