store_test.gno
19.93 Kb · 570 lines
1package users
2
3import (
4 "chain"
5 "testing"
6
7 "gno.land/p/nt/bptree/v0"
8 "gno.land/p/nt/testutils/v0"
9 "gno.land/p/nt/uassert/v0"
10 "gno.land/p/nt/urequire/v0"
11)
12
13var (
14 alice = "alice"
15 aliceAddr = testutils.TestAddress(alice)
16 bob = "bob"
17 bobAddr = testutils.TestAddress(bob)
18
19 whitelistedCallerAddr = chain.PackageAddress(initControllerPath)
20)
21
22func 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/v1
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
148func 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 _, ok := nameStore.Get("squattedname")
247 uassert.False(t, ok)
248 })
249
250 t.Run("invalid_inputs", func(t *testing.T) {
251 cleanStore(t)
252
253 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
254 data := ResolveAddress(aliceAddr)
255 {
256 testing.SetOriginCaller(whitelistedCallerAddr)
257 uassert.ErrorContains(t, data.UpdateName(0, cur, ""), ErrEmptyUsername.Error())
258 uassert.ErrorContains(t, data.UpdateName(0, cur, "username with a space"), ErrInvalidUsername.Error())
259 uassert.ErrorContains(t,
260 data.UpdateName(0, cur, "verylongusernameverylongusernameverylongusernameverylongusername1"),
261 ErrInvalidUsername.Error())
262 uassert.ErrorContains(t, data.UpdateName(0, cur, "namewith^&()"), ErrInvalidUsername.Error())
263 testing.SetRealm(testing.NewCodeRealm("gno.land/r/sys/users"))
264 }
265 })
266
267 t.Run("address_lookalike", func(t *testing.T) {
268 cleanStore(t)
269
270 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
271 data := ResolveAddress(aliceAddr)
272
273 {
274 // Address as username
275 uassert.ErrorContains(t, data.UpdateName(0, cur, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), ErrNameLikeAddress.Error())
276 // Beginning of address as username
277 uassert.ErrorContains(t, data.UpdateName(0, cur, "g1jg8mtutu9khhfwc4nxmu"), ErrNameLikeAddress.Error())
278 uassert.NoError(t, data.UpdateName(0, cur, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5longerthananaddress"))
279 testing.SetRealm(testing.NewCodeRealm("gno.land/r/sys/users"))
280 }
281 })
282}
283
284func TestDelete(cur realm, t *testing.T) {
285 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
286
287 t.Run("non_existent_user", func(t *testing.T) {
288 cleanStore(t)
289
290 data := ResolveAddress(testutils.TestAddress("unregistered"))
291 uassert.ErrorContains(t, data.Delete(0, cur), ErrUserNotExistOrDeleted.Error())
292 })
293
294 t.Run("double_delete", func(t *testing.T) {
295 cleanStore(t)
296
297 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
298 data := ResolveAddress(aliceAddr)
299 urequire.NoError(t, data.Delete(0, cur))
300 data = ResolveAddress(aliceAddr)
301 uassert.ErrorContains(t, data.Delete(0, cur), ErrUserNotExistOrDeleted.Error())
302 })
303
304 t.Run("valid_delete", func(t *testing.T) {
305 cleanStore(t)
306
307 urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
308 data := ResolveAddress(aliceAddr)
309 uassert.NoError(t, data.Delete(0, cur))
310
311 resolved1, _ := ResolveName(alice)
312 uassert.Equal(t, nil, resolved1)
313 uassert.Equal(t, nil, ResolveAddress(aliceAddr))
314 })
315}
316
317func TestRegisterNotWhitelisted(cur realm, t *testing.T) {
318 t.Run("register_not_whitelisted", func(t *testing.T) {
319 uassert.ErrorContains(t, RegisterUser(cross(cur), alice, aliceAddr), "does not exist in whitelist")
320 })
321}
322
323func TestCanonicalize(cur realm, t *testing.T) {
324 cases := []struct {
325 in, want string
326 }{
327 // Single-rule cases.
328 {"l", "i"}, {"i", "i"}, {"1", "i"},
329 {"0", "o"}, {"o", "o"},
330 {"-", ""}, {".", ""}, {"_", ""},
331
332 // Identity / pass-through.
333 {"abc", "abc"}, {"xyz", "xyz"}, {"123", "i23"},
334
335 // Open Nym Tier examples from the design doc.
336 {"nym-vital1k123", "nymvitaiiki23"},
337 {"nym-vitalik123", "nymvitaiiki23"},
338 {"nym-vital1k999", "nymvitaiik999"},
339
340 // Same stem, different digit suffix → distinct canonicals.
341 {"nym-foolbar000", "nymfooibarooo"},
342 {"nym-foolbar001", "nymfooibarooi"},
343
344 // Three-way separator equivalence.
345 {"xyz-com", "xyzcom"},
346 {"xyz_com", "xyzcom"},
347 {"xyz.com", "xyzcom"},
348
349 // Idempotency: applying twice equals applying once.
350 {"nymvitaiiki23", "nymvitaiiki23"},
351
352 // Empty input.
353 {"", ""},
354 }
355 for _, tc := range cases {
356 got := Canonicalize(tc.in)
357 uassert.Equal(t, tc.want, got)
358 }
359}
360
361func TestCanonicalize_NonASCIIPassthrough(cur realm, t *testing.T) {
362 // ASCII-only contract: the function passes non-ASCII bytes through
363 // unchanged. Locks the documented contract — registration paths
364 // reject non-ASCII upstream via validateName, so this code path is
365 // only reachable through direct callers from other realms.
366 uassert.Equal(t, "café", Canonicalize("café"))
367 uassert.Equal(t, "naïve", Canonicalize("naïve"))
368}
369
370func TestRegisterUser_CanonicalCollision(cur realm, t *testing.T) {
371 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
372
373 t.Run("non_bypass_blocks", func(t *testing.T) {
374 cleanStore(t)
375
376 urequire.NoError(t, RegisterUser(cross(cur), "vitalik", aliceAddr))
377 // Different exact name, same canonical form → ErrCanonicalCollision.
378 uassert.ErrorContains(t,
379 RegisterUser(cross(cur), "vital1k", bobAddr),
380 ErrCanonicalCollision.Error(),
381 )
382 })
383
384 t.Run("non_bypass_blocks_with_separator_strip", func(t *testing.T) {
385 cleanStore(t)
386
387 urequire.NoError(t, RegisterUser(cross(cur), "xyz-com", aliceAddr))
388 uassert.ErrorContains(t,
389 RegisterUser(cross(cur), "xyz_com", bobAddr),
390 ErrCanonicalCollision.Error(),
391 )
392 })
393
394 t.Run("non_bypass_allows_different_digit_suffix", func(t *testing.T) {
395 cleanStore(t)
396
397 // Same alpha stem, different digits-after-canonicalization → no collision.
398 urequire.NoError(t, RegisterUser(cross(cur), "nym-foolbar000", aliceAddr))
399 urequire.NoError(t, RegisterUser(cross(cur), "nym-foolbar999",
400 testutils.TestAddress("foolbar999")))
401 })
402
403 t.Run("exact_name_taken_takes_precedence", func(t *testing.T) {
404 cleanStore(t)
405
406 urequire.NoError(t, RegisterUser(cross(cur), "vitalik", aliceAddr))
407 // Same exact name → ErrNameTaken (NOT ErrCanonicalCollision).
408 // The nameStore.Has check precedes the canonical check.
409 uassert.ErrorContains(t,
410 RegisterUser(cross(cur), "vitalik", bobAddr),
411 ErrNameTaken.Error(),
412 )
413 })
414}
415
416func TestRegisterUserIgnoreCanonical(cur realm, t *testing.T) {
417 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
418
419 t.Run("bypass_succeeds_on_canonical_collision", func(t *testing.T) {
420 cleanStore(t)
421
422 urequire.NoError(t, RegisterUser(cross(cur), "vitalik", aliceAddr))
423 // Bypass succeeds even though canonical collides.
424 urequire.NoError(t, RegisterUserIgnoreCanonical(cross(cur), "vital1k", bobAddr))
425
426 // Both names resolve.
427 res, _ := ResolveName("vitalik")
428 uassert.Equal(t, aliceAddr, res.Addr())
429 res, _ = ResolveName("vital1k")
430 uassert.Equal(t, bobAddr, res.Addr())
431 })
432
433 t.Run("bypass_later_wins_overwrite", func(t *testing.T) {
434 cleanStore(t)
435
436 // First registration writes canonical entry.
437 urequire.NoError(t, RegisterUser(cross(cur), "vitalik", aliceAddr))
438 existing, taken := IsCanonicalTaken("vital1k")
439 urequire.True(t, taken)
440 uassert.Equal(t, "vitalik", existing)
441
442 // Bypass write overwrites the canonical entry (decision #14).
443 urequire.NoError(t, RegisterUserIgnoreCanonical(cross(cur), "vital1k", bobAddr))
444 existing, taken = IsCanonicalTaken("vitalik")
445 urequire.True(t, taken)
446 uassert.Equal(t, "vital1k", existing)
447 })
448
449 t.Run("bypass_still_blocks_exact_taken", func(t *testing.T) {
450 cleanStore(t)
451
452 urequire.NoError(t, RegisterUser(cross(cur), "vitalik", aliceAddr))
453 // Same exact name still blocked, even via the bypass path.
454 uassert.ErrorContains(t,
455 RegisterUserIgnoreCanonical(cross(cur), "vitalik", bobAddr),
456 ErrNameTaken.Error(),
457 )
458 })
459}
460
461func TestUpdateName_CanonicalCollision(cur realm, t *testing.T) {
462 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
463
464 t.Run("self_collision_blocks_per_decision_15", func(t *testing.T) {
465 cleanStore(t)
466
467 // Alice registers vital1k. Canonical store maps "vitaiik" → "vital1k".
468 urequire.NoError(t, RegisterUser(cross(cur), "vital1k", aliceAddr))
469 data := ResolveAddress(aliceAddr)
470
471 // Alice attempts to rename to a confusable variant of HER OWN name.
472 // Decision #15: blocked. No self-collision exception in the
473 // non-bypass path.
474 testing.SetOriginCaller(whitelistedCallerAddr)
475 uassert.ErrorContains(t,
476 data.UpdateName(0, cur, "vitalik"),
477 ErrCanonicalCollision.Error(),
478 )
479 testing.SetRealm(testing.NewCodeRealm("gno.land/r/sys/users"))
480 })
481
482 t.Run("different_user_collision_blocks", func(t *testing.T) {
483 cleanStore(t)
484
485 urequire.NoError(t, RegisterUser(cross(cur), "vitalik", aliceAddr))
486 urequire.NoError(t, RegisterUser(cross(cur), "bob", bobAddr))
487
488 data := ResolveAddress(bobAddr)
489 testing.SetOriginCaller(whitelistedCallerAddr)
490 uassert.ErrorContains(t,
491 data.UpdateName(0, cur, "vital1k"),
492 ErrCanonicalCollision.Error(),
493 )
494 testing.SetRealm(testing.NewCodeRealm("gno.land/r/sys/users"))
495 })
496
497 t.Run("bypass_allows_self_collision_rename", func(t *testing.T) {
498 cleanStore(t)
499
500 urequire.NoError(t, RegisterUser(cross(cur), "vital1k", aliceAddr))
501 data := ResolveAddress(aliceAddr)
502
503 // Bypass path allows the rename (DAO grant scenario).
504 testing.SetOriginCaller(whitelistedCallerAddr)
505 urequire.NoError(t, data.UpdateNameIgnoreCanonical(0, cur, "vitalik"))
506 testing.SetRealm(testing.NewCodeRealm("gno.land/r/sys/users"))
507
508 // Both names point to alice; latest is "vitalik".
509 uassert.Equal(t, "vitalik", ResolveAddress(aliceAddr).Name())
510 // Old name still resolves to alice (decision #4: keep old entry).
511 res, isLatest := ResolveName("vital1k")
512 uassert.Equal(t, aliceAddr, res.Addr())
513 uassert.False(t, isLatest)
514 })
515}
516
517func TestCanonicalEntry_Persists_After_Delete(cur realm, t *testing.T) {
518 // Decision #5: Delete does NOT remove the canonical entry. Mirrors
519 // the existing tombstone behavior (anti-revival policy).
520 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
521 cleanStore(t)
522
523 urequire.NoError(t, RegisterUser(cross(cur), "vitalik", aliceAddr))
524 data := ResolveAddress(aliceAddr)
525 urequire.NoError(t, data.Delete(0, cur))
526
527 // Canonical entry retained.
528 existing, taken := IsCanonicalTaken("vitalik")
529 urequire.True(t, taken)
530 uassert.Equal(t, "vitalik", existing)
531
532 // A different user CANNOT register a confusable variant — the deleted
533 // user's canonical claim still stands.
534 uassert.ErrorContains(t,
535 RegisterUser(cross(cur), "vital1k", bobAddr),
536 ErrCanonicalCollision.Error(),
537 )
538}
539
540func TestCanonicalEntry_Persists_After_UpdateName(cur realm, t *testing.T) {
541 // Decision #4: UpdateName keeps the OLD canonical entry. Mirrors the
542 // existing nameStore alias retention (anti-rename-squat policy).
543 testing.SetRealm(testing.NewCodeRealm(initControllerPath))
544 cleanStore(t)
545
546 urequire.NoError(t, RegisterUser(cross(cur), "alice", aliceAddr))
547 data := ResolveAddress(aliceAddr)
548 testing.SetOriginCaller(whitelistedCallerAddr)
549 urequire.NoError(t, data.UpdateName(0, cur, "alice2"))
550 testing.SetRealm(testing.NewCodeRealm("gno.land/r/sys/users"))
551
552 // Both old and new canonical entries exist.
553 existing, taken := IsCanonicalTaken("alice")
554 urequire.True(t, taken)
555 uassert.Equal(t, "alice", existing)
556
557 existing, taken = IsCanonicalTaken("alice2")
558 urequire.True(t, taken)
559 uassert.Equal(t, "alice2", existing)
560}
561
562// cleanStore should not be needed, as vm store should be reset after each test.
563// Reference: https://github.com/gnolang/gno/issues/1982
564func cleanStore(t *testing.T) {
565 t.Helper()
566
567 nameStore = bptree.NewBPTree32()
568 addressStore = bptree.NewBPTree32()
569 canonicalStore = bptree.NewBPTree32()
570}