Kind Realm (renderable)
Name users
Namespace sys
Exported functions 21
Module gno.land/r/sys/users
gno 0.9 store_test.gno gno
⧉
1 package users 2 3 import ( 4 "chain" 5 "testing" 6 7 "gno.land/p/nt/bptree/v0" 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 var (
14 alice = "alice"
15 aliceAddr = testutils.TestAddress(alice)
16 bob = "bob"
17 bobAddr = testutils.TestAddress(bob)
18
19 whitelistedCallerAddr = chain.PackageAddress(initControllerPath)
20 )
21
22 func TestRegister(cur realm, t *testing.T) {
23 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
24
25 t.Run( "valid_registration" , func (t *testing.T) {
26 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
27
28 res, isLatest := ResolveName(alice)
29 uassert.Equal(t, aliceAddr, res.Addr())
30 uassert.True(t, isLatest)
31
32 res = ResolveAddress(aliceAddr)
33 uassert.Equal(t, alice, res.Name())
34 })
35
36 t.Run( "invalid_inputs" , func (t *testing.T) {
37 cleanStore(t)
38
39 uassert.ErrorContains(t, RegisterUser(cross(cur), "" , aliceAddr), ErrEmptyUsername.Error())
40 uassert.ErrorContains(t, RegisterUser(cross(cur), alice, "" ), ErrInvalidAddress.Error())
41 uassert.ErrorContains(t, RegisterUser(cross(cur), alice, "invalidaddress" ), ErrInvalidAddress.Error())
42
43 uassert.ErrorContains(t, RegisterUser(cross(cur), "username with a space" , aliceAddr), ErrInvalidUsername.Error())
44 uassert.ErrorContains(t,
45 RegisterUser(cross(cur), "verylongusernameverylongusernameverylongusernameverylongusername1" , aliceAddr),
46 ErrInvalidUsername.Error())
47 uassert.ErrorContains(t, RegisterUser(cross(cur), "namewith^&()" , aliceAddr), ErrInvalidUsername.Error())
48
49 // Lowercase-only enforcement (closes case-confusable squatting via
50 // Alice/alice/ALICE registering as distinct names — see TO_REVIEW
51 // adversarial review). reName mirrors gno's mempackage Re_name shape.
52 uassert.ErrorContains(t, RegisterUser(cross(cur), "Alice" , aliceAddr), ErrInvalidUsername.Error())
53 uassert.ErrorContains(t, RegisterUser(cross(cur), "ALICE" , aliceAddr), ErrInvalidUsername.Error())
54 uassert.ErrorContains(t, RegisterUser(cross(cur), "aLice" , aliceAddr), ErrInvalidUsername.Error())
55
56 // Must start with a lowercase letter. Names starting with a digit,
57 // underscore, or hyphen are rejected.
58 uassert.ErrorContains(t, RegisterUser(cross(cur), "1alice" , aliceAddr), ErrInvalidUsername.Error())
59 uassert.ErrorContains(t, RegisterUser(cross(cur), "9" , aliceAddr), ErrInvalidUsername.Error())
60 uassert.ErrorContains(t, RegisterUser(cross(cur), "_alice" , aliceAddr), ErrInvalidUsername.Error())
61 uassert.ErrorContains(t, RegisterUser(cross(cur), "-alice" , aliceAddr), ErrInvalidUsername.Error())
62
63 // Cannot end with a separator either (the new mempackage-aligned
64 // regex requires the final char to be in [a-z0-9]).
65 uassert.ErrorContains(t, RegisterUser(cross(cur), "alice_" , aliceAddr), ErrInvalidUsername.Error())
66 uassert.ErrorContains(t, RegisterUser(cross(cur), "alice-" , aliceAddr), ErrInvalidUsername.Error())
67
68 // Cannot have consecutive separators in the middle either.
69 // Each separator MUST be followed by at least one alphanumeric.
70 uassert.ErrorContains(t, RegisterUser(cross(cur), "alice--bob" , aliceAddr), ErrInvalidUsername.Error())
71 uassert.ErrorContains(t, RegisterUser(cross(cur), "alice__bob" , aliceAddr), ErrInvalidUsername.Error())
72 uassert.ErrorContains(t, RegisterUser(cross(cur), "alice-_bob" , aliceAddr), ErrInvalidUsername.Error())
73 uassert.ErrorContains(t, RegisterUser(cross(cur), "alice_-bob" , aliceAddr), ErrInvalidUsername.Error())
74
75 // Length cap of exactly 64 — boundary cases.
76 exactly65 := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" // 65 chars
77 uassert.ErrorContains(t, RegisterUser(cross(cur), exactly65, aliceAddr), ErrInvalidUsername.Error())
78 })
79
80 t.Run( "valid_edge_cases" , func (t *testing.T) {
81 // Names valid under the mempackage-aligned reName: starts with a
82 // letter, may contain hyphens or underscores in the middle, ends
83 // in [a-z0-9]. The hyphen support is what enables the namereg/v0
84 // `nym-...` prefix shape.
85 cleanStore(t)
86 urequire.NoError(t, RegisterUser(cross(cur),
87 "nym-alice123" ,
88 testutils.TestAddress( "u_nym_alice" )))
89
90 cleanStore(t)
91 urequire.NoError(t, RegisterUser(cross(cur),
92 "a_b_c" ,
93 testutils.TestAddress( "u_underscore" )))
94
95 cleanStore(t)
96 urequire.NoError(t, RegisterUser(cross(cur),
97 "a" ,
98 testutils.TestAddress( "u_singlechar" )))
99
100 cleanStore(t)
101 // 64 chars exactly (length cap is inclusive).
102 exactly64 := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
103 urequire.NoError(t, RegisterUser(cross(cur),
104 exactly64,
105 testutils.TestAddress( "u_64" )))
106 })
107
108 t.Run( "addr_already_registered" , func (t *testing.T) {
109 cleanStore(t)
110
111 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
112
113 // Try registering again
114 uassert.ErrorContains(t, RegisterUser(cross(cur), "othername" , aliceAddr), ErrAlreadyHasName.Error())
115 })
116
117 t.Run( "name_taken" , func (t *testing.T) {
118 cleanStore(t)
119
120 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
121
122 // Try registering alice's name with bob's address
123 uassert.ErrorContains(t, RegisterUser(cross(cur), alice, bobAddr), ErrNameTaken.Error())
124 })
125
126 t.Run( "user_deleted" , func (t *testing.T) {
127 cleanStore(t)
128
129 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
130 data := ResolveAddress(aliceAddr)
131 urequire.NoError(t, data.Delete( 0 , cur))
132
133 // Try re-registering after deletion
134 uassert.ErrorContains(t, RegisterUser(cross(cur), "newname" , aliceAddr), ErrDeletedUser.Error())
135 })
136
137 t.Run( "address_lookalike" , func (t *testing.T) {
138 cleanStore(t)
139
140 // Address as username
141 uassert.ErrorContains(t, RegisterUser(cross(cur), "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" , aliceAddr), ErrNameLikeAddress.Error())
142 // Beginning of address as username
143 uassert.ErrorContains(t, RegisterUser(cross(cur), "g1jg8mtutu9khhfwc4nxmu" , aliceAddr), ErrNameLikeAddress.Error())
144 uassert.NoError(t, RegisterUser(cross(cur), "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5longerthananaddress" , aliceAddr))
145 })
146 }
147
148 func TestUpdateName(cur realm, t *testing.T) {
149 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
150
151 t.Run( "valid_direct_alias" , func (t *testing.T) {
152 cleanStore(t)
153
154 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
155 data := ResolveAddress(aliceAddr)
156 {
157 testing.SetOriginCaller(whitelistedCallerAddr)
158 uassert.NoError(t, data.UpdateName( 0 , cur, "alice1" ))
159 testing.SetRealm(testing.NewCodeRealm( "gno.land/r/sys/users" ))
160 }
161 })
162
163 t.Run( "valid_double_alias" , func (t *testing.T) {
164 cleanStore(t)
165
166 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
167 data := ResolveAddress(aliceAddr)
168 {
169 testing.SetOriginCaller(whitelistedCallerAddr)
170 uassert.NoError(t, data.UpdateName( 0 , cur, "alice2" ))
171 uassert.NoError(t, data.UpdateName( 0 , cur, "alice3" ))
172 testing.SetRealm(testing.NewCodeRealm( "gno.land/r/sys/users" ))
173 }
174 uassert.Equal(t, ResolveAddress(aliceAddr).username, "alice3" )
175 })
176
177 t.Run( "name_taken" , func (t *testing.T) {
178 cleanStore(t)
179
180 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
181
182 data := ResolveAddress(aliceAddr)
183 uassert.Error(t, data.UpdateName( 0 , cur, alice), ErrNameTaken.Error())
184 })
185
186 t.Run( "alias_before_name" , func (t *testing.T) {
187 cleanStore(t)
188 data := ResolveAddress(aliceAddr) // not registered
189
190 uassert.ErrorContains(t, data.UpdateName( 0 , cur, alice), ErrUserNotExistOrDeleted.Error())
191 })
192
193 t.Run( "alias_after_delete" , func (t *testing.T) {
194 cleanStore(t)
195
196 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
197 data := ResolveAddress(aliceAddr)
198 {
199 urequire.NoError(t, data.Delete( 0 , cur))
200 testing.SetRealm(testing.NewCodeRealm( "gno.land/r/sys/users" ))
201 }
202
203 data = ResolveAddress(aliceAddr)
204 {
205 uassert.ErrorContains(t, data.UpdateName( 0 , cur, "newalice" ), ErrUserNotExistOrDeleted.Error())
206 testing.SetRealm(testing.NewCodeRealm( "gno.land/r/sys/users" ))
207 }
208 })
209
210 // Audit finding #3: a controller holding a cached *UserData pointer to a
211 // user that gets deleted between resolve and update must not be able to
212 // insert a new alias. The alias_after_delete test above re-resolves and
213 // gets nil, so it only exercises the u==nil branch. This test holds the
214 // pointer across the delete and verifies the deleted-flag branch.
215 t.Run( "alias_with_cached_pointer_after_delete" , func (t *testing.T) {
216 cleanStore(t)
217
218 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
219 // Cache the pointer BEFORE deletion (simulating a controller that
220 // resolved earlier and held the reference).
221 cached := ResolveAddress(aliceAddr)
222 urequire.NotEqual(t, nil , cached)
223
224 // Delete via a fresh resolve.
225 data := ResolveAddress(aliceAddr)
226 {
227 urequire.NoError(t, data.Delete( 0 , cur))
228 testing.SetRealm(testing.NewCodeRealm( "gno.land/r/sys/users" ))
229 }
230
231 // The cached pointer is non-nil but its .deleted is now true.
232 urequire.NotEqual(t, nil , cached)
233 uassert.True(t, cached.IsDeleted())
234
235 // Attempting UpdateName on the cached pointer must reject — without
236 // this check, "squattedname" would be inserted into nameStore
237 // pointing at the deleted user, becoming permanently unresolvable
238 // AND unregisterable.
239 {
240 testing.SetOriginCaller(whitelistedCallerAddr)
241 uassert.ErrorContains(t, cached.UpdateName( 0 , cur, "squattedname" ), ErrUserNotExistOrDeleted.Error())
242 testing.SetRealm(testing.NewCodeRealm( "gno.land/r/sys/users" ))
243 }
244
245 // Confirm the squat didn't happen: nameStore should NOT have it.
246 uassert.False(t, nameStore.Has( "squattedname" ))
247 })
248
249 t.Run( "invalid_inputs" , func (t *testing.T) {
250 cleanStore(t)
251
252 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
253 data := ResolveAddress(aliceAddr)
254 {
255 testing.SetOriginCaller(whitelistedCallerAddr)
256 uassert.ErrorContains(t, data.UpdateName( 0 , cur, "" ), ErrEmptyUsername.Error())
257 uassert.ErrorContains(t, data.UpdateName( 0 , cur, "username with a space" ), ErrInvalidUsername.Error())
258 uassert.ErrorContains(t,
259 data.UpdateName( 0 , cur, "verylongusernameverylongusernameverylongusernameverylongusername1" ),
260 ErrInvalidUsername.Error())
261 uassert.ErrorContains(t, data.UpdateName( 0 , cur, "namewith^&()" ), ErrInvalidUsername.Error())
262 testing.SetRealm(testing.NewCodeRealm( "gno.land/r/sys/users" ))
263 }
264 })
265
266 t.Run( "address_lookalike" , func (t *testing.T) {
267 cleanStore(t)
268
269 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
270 data := ResolveAddress(aliceAddr)
271
272 {
273 // Address as username
274 uassert.ErrorContains(t, data.UpdateName( 0 , cur, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" ), ErrNameLikeAddress.Error())
275 // Beginning of address as username
276 uassert.ErrorContains(t, data.UpdateName( 0 , cur, "g1jg8mtutu9khhfwc4nxmu" ), ErrNameLikeAddress.Error())
277 uassert.NoError(t, data.UpdateName( 0 , cur, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5longerthananaddress" ))
278 testing.SetRealm(testing.NewCodeRealm( "gno.land/r/sys/users" ))
279 }
280 })
281 }
282
283 func TestDelete(cur realm, t *testing.T) {
284 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
285
286 t.Run( "non_existent_user" , func (t *testing.T) {
287 cleanStore(t)
288
289 data := ResolveAddress(testutils.TestAddress( "unregistered" ))
290 uassert.ErrorContains(t, data.Delete( 0 , cur), ErrUserNotExistOrDeleted.Error())
291 })
292
293 t.Run( "double_delete" , func (t *testing.T) {
294 cleanStore(t)
295
296 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
297 data := ResolveAddress(aliceAddr)
298 urequire.NoError(t, data.Delete( 0 , cur))
299 data = ResolveAddress(aliceAddr)
300 uassert.ErrorContains(t, data.Delete( 0 , cur), ErrUserNotExistOrDeleted.Error())
301 })
302
303 t.Run( "valid_delete" , func (t *testing.T) {
304 cleanStore(t)
305
306 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
307 data := ResolveAddress(aliceAddr)
308 uassert.NoError(t, data.Delete( 0 , cur))
309
310 resolved1, _ := ResolveName(alice)
311 uassert.Equal(t, nil , resolved1)
312 uassert.Equal(t, nil , ResolveAddress(aliceAddr))
313 })
314 }
315
316 func TestRegisterNotWhitelisted(cur realm, t *testing.T) {
317 t.Run( "register_not_whitelisted" , func (t *testing.T) {
318 uassert.ErrorContains(t, RegisterUser(cross(cur), alice, aliceAddr), "does not exist in whitelist" )
319 })
320 }
321
322 func TestCanonicalize(cur realm, t *testing.T) {
323 cases := [] struct {
324 in, want string
325 }{
326 // Single-rule cases.
327 { "l" , "i" }, { "i" , "i" }, { "1" , "i" },
328 { "0" , "o" }, { "o" , "o" },
329 { "-" , "" }, { "." , "" }, { "_" , "" },
330
331 // Identity / pass-through.
332 { "abc" , "abc" }, { "xyz" , "xyz" }, { "123" , "i23" },
333
334 // Open Nym Tier examples from the design doc.
335 { "nym-vital1k123" , "nymvitaiiki23" },
336 { "nym-vitalik123" , "nymvitaiiki23" },
337 { "nym-vital1k999" , "nymvitaiik999" },
338
339 // Same stem, different digit suffix → distinct canonicals.
340 { "nym-foolbar000" , "nymfooibarooo" },
341 { "nym-foolbar001" , "nymfooibarooi" },
342
343 // Three-way separator equivalence.
344 { "xyz-com" , "xyzcom" },
345 { "xyz_com" , "xyzcom" },
346 { "xyz.com" , "xyzcom" },
347
348 // Idempotency: applying twice equals applying once.
349 { "nymvitaiiki23" , "nymvitaiiki23" },
350
351 // Empty input.
352 { "" , "" },
353 }
354 for _, tc := range cases {
355 got := Canonicalize(tc.in)
356 uassert.Equal(t, tc.want, got)
357 }
358 }
359
360 func TestCanonicalize_NonASCIIPassthrough(cur realm, t *testing.T) {
361 // ASCII-only contract: the function passes non-ASCII bytes through
362 // unchanged. Locks the documented contract — registration paths
363 // reject non-ASCII upstream via validateName, so this code path is
364 // only reachable through direct callers from other realms.
365 uassert.Equal(t, "café" , Canonicalize( "café" ))
366 uassert.Equal(t, "naïve" , Canonicalize( "naïve" ))
367 }
368
369 func TestRegisterUser_CanonicalCollision(cur realm, t *testing.T) {
370 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
371
372 t.Run( "non_bypass_blocks" , func (t *testing.T) {
373 cleanStore(t)
374
375 urequire.NoError(t, RegisterUser(cross(cur), "vitalik" , aliceAddr))
376 // Different exact name, same canonical form → ErrCanonicalCollision.
377 uassert.ErrorContains(t,
378 RegisterUser(cross(cur), "vital1k" , bobAddr),
379 ErrCanonicalCollision.Error(),
380 )
381 })
382
383 t.Run( "non_bypass_blocks_with_separator_strip" , func (t *testing.T) {
384 cleanStore(t)
385
386 urequire.NoError(t, RegisterUser(cross(cur), "xyz-com" , aliceAddr))
387 uassert.ErrorContains(t,
388 RegisterUser(cross(cur), "xyz_com" , bobAddr),
389 ErrCanonicalCollision.Error(),
390 )
391 })
392
393 t.Run( "non_bypass_allows_different_digit_suffix" , func (t *testing.T) {
394 cleanStore(t)
395
396 // Same alpha stem, different digits-after-canonicalization → no collision.
397 urequire.NoError(t, RegisterUser(cross(cur), "nym-foolbar000" , aliceAddr))
398 urequire.NoError(t, RegisterUser(cross(cur), "nym-foolbar999" ,
399 testutils.TestAddress( "foolbar999" )))
400 })
401
402 t.Run( "exact_name_taken_takes_precedence" , func (t *testing.T) {
403 cleanStore(t)
404
405 urequire.NoError(t, RegisterUser(cross(cur), "vitalik" , aliceAddr))
406 // Same exact name → ErrNameTaken (NOT ErrCanonicalCollision).
407 // The nameStore.Has check precedes the canonical check.
408 uassert.ErrorContains(t,
409 RegisterUser(cross(cur), "vitalik" , bobAddr),
410 ErrNameTaken.Error(),
411 )
412 })
413 }
414
415 func TestRegisterUserIgnoreCanonical(cur realm, t *testing.T) {
416 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
417
418 t.Run( "bypass_succeeds_on_canonical_collision" , func (t *testing.T) {
419 cleanStore(t)
420
421 urequire.NoError(t, RegisterUser(cross(cur), "vitalik" , aliceAddr))
422 // Bypass succeeds even though canonical collides.
423 urequire.NoError(t, RegisterUserIgnoreCanonical(cross(cur), "vital1k" , bobAddr))
424
425 // Both names resolve.
426 res, _ := ResolveName( "vitalik" )
427 uassert.Equal(t, aliceAddr, res.Addr())
428 res, _ = ResolveName( "vital1k" )
429 uassert.Equal(t, bobAddr, res.Addr())
430 })
431
432 t.Run( "bypass_later_wins_overwrite" , func (t *testing.T) {
433 cleanStore(t)
434
435 // First registration writes canonical entry.
436 urequire.NoError(t, RegisterUser(cross(cur), "vitalik" , aliceAddr))
437 existing, taken := IsCanonicalTaken( "vital1k" )
438 urequire.True(t, taken)
439 uassert.Equal(t, "vitalik" , existing)
440
441 // Bypass write overwrites the canonical entry (decision #14).
442 urequire.NoError(t, RegisterUserIgnoreCanonical(cross(cur), "vital1k" , bobAddr))
443 existing, taken = IsCanonicalTaken( "vitalik" )
444 urequire.True(t, taken)
445 uassert.Equal(t, "vital1k" , existing)
446 })
447
448 t.Run( "bypass_still_blocks_exact_taken" , func (t *testing.T) {
449 cleanStore(t)
450
451 urequire.NoError(t, RegisterUser(cross(cur), "vitalik" , aliceAddr))
452 // Same exact name still blocked, even via the bypass path.
453 uassert.ErrorContains(t,
454 RegisterUserIgnoreCanonical(cross(cur), "vitalik" , bobAddr),
455 ErrNameTaken.Error(),
456 )
457 })
458 }
459
460 func TestUpdateName_CanonicalCollision(cur realm, t *testing.T) {
461 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
462
463 t.Run( "self_collision_blocks_per_decision_15" , func (t *testing.T) {
464 cleanStore(t)
465
466 // Alice registers vital1k. Canonical store maps "vitaiik" → "vital1k".
467 urequire.NoError(t, RegisterUser(cross(cur), "vital1k" , aliceAddr))
468 data := ResolveAddress(aliceAddr)
469
470 // Alice attempts to rename to a confusable variant of HER OWN name.
471 // Decision #15: blocked. No self-collision exception in the
472 // non-bypass path.
473 testing.SetOriginCaller(whitelistedCallerAddr)
474 uassert.ErrorContains(t,
475 data.UpdateName( 0 , cur, "vitalik" ),
476 ErrCanonicalCollision.Error(),
477 )
478 testing.SetRealm(testing.NewCodeRealm( "gno.land/r/sys/users" ))
479 })
480
481 t.Run( "different_user_collision_blocks" , func (t *testing.T) {
482 cleanStore(t)
483
484 urequire.NoError(t, RegisterUser(cross(cur), "vitalik" , aliceAddr))
485 urequire.NoError(t, RegisterUser(cross(cur), "bob" , bobAddr))
486
487 data := ResolveAddress(bobAddr)
488 testing.SetOriginCaller(whitelistedCallerAddr)
489 uassert.ErrorContains(t,
490 data.UpdateName( 0 , cur, "vital1k" ),
491 ErrCanonicalCollision.Error(),
492 )
493 testing.SetRealm(testing.NewCodeRealm( "gno.land/r/sys/users" ))
494 })
495
496 t.Run( "bypass_allows_self_collision_rename" , func (t *testing.T) {
497 cleanStore(t)
498
499 urequire.NoError(t, RegisterUser(cross(cur), "vital1k" , aliceAddr))
500 data := ResolveAddress(aliceAddr)
501
502 // Bypass path allows the rename (DAO grant scenario).
503 testing.SetOriginCaller(whitelistedCallerAddr)
504 urequire.NoError(t, data.UpdateNameIgnoreCanonical( 0 , cur, "vitalik" ))
505 testing.SetRealm(testing.NewCodeRealm( "gno.land/r/sys/users" ))
506
507 // Both names point to alice; latest is "vitalik".
508 uassert.Equal(t, "vitalik" , ResolveAddress(aliceAddr).Name())
509 // Old name still resolves to alice (decision #4: keep old entry).
510 res, isLatest := ResolveName( "vital1k" )
511 uassert.Equal(t, aliceAddr, res.Addr())
512 uassert.False(t, isLatest)
513 })
514 }
515
516 func TestCanonicalEntry_Persists_After_Delete(cur realm, t *testing.T) {
517 // Decision #5: Delete does NOT remove the canonical entry. Mirrors
518 // the existing tombstone behavior (anti-revival policy).
519 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
520 cleanStore(t)
521
522 urequire.NoError(t, RegisterUser(cross(cur), "vitalik" , aliceAddr))
523 data := ResolveAddress(aliceAddr)
524 urequire.NoError(t, data.Delete( 0 , cur))
525
526 // Canonical entry retained.
527 existing, taken := IsCanonicalTaken( "vitalik" )
528 urequire.True(t, taken)
529 uassert.Equal(t, "vitalik" , existing)
530
531 // A different user CANNOT register a confusable variant — the deleted
532 // user's canonical claim still stands.
533 uassert.ErrorContains(t,
534 RegisterUser(cross(cur), "vital1k" , bobAddr),
535 ErrCanonicalCollision.Error(),
536 )
537 }
538
539 func TestCanonicalEntry_Persists_After_UpdateName(cur realm, t *testing.T) {
540 // Decision #4: UpdateName keeps the OLD canonical entry. Mirrors the
541 // existing nameStore alias retention (anti-rename-squat policy).
542 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
543 cleanStore(t)
544
545 urequire.NoError(t, RegisterUser(cross(cur), "alice" , aliceAddr))
546 data := ResolveAddress(aliceAddr)
547 testing.SetOriginCaller(whitelistedCallerAddr)
548 urequire.NoError(t, data.UpdateName( 0 , cur, "alice2" ))
549 testing.SetRealm(testing.NewCodeRealm( "gno.land/r/sys/users" ))
550
551 // Both old and new canonical entries exist.
552 existing, taken := IsCanonicalTaken( "alice" )
553 urequire.True(t, taken)
554 uassert.Equal(t, "alice" , existing)
555
556 existing, taken = IsCanonicalTaken( "alice2" )
557 urequire.True(t, taken)
558 uassert.Equal(t, "alice2" , existing)
559 }
560
561 // cleanStore should not be needed, as vm store should be reset after each test.
562 // Reference: https://github.com/gnolang/gno/issues/1982
563 func cleanStore(t *testing.T) {
564 t.Helper()
565
566 nameStore = bptree.NewBPTree32()
567 addressStore = bptree.NewBPTree32()
568 canonicalStore = bptree.NewBPTree32()
569 }
570