store_test.gno
19.92 Kb · 569 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 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
283func 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
316func 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
322func 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
360func 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
369func 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
415func 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
460func 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
516func 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
539func 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
563func cleanStore(t *testing.T) {
564 t.Helper()
565
566 nameStore = bptree.NewBPTree32()
567 addressStore = bptree.NewBPTree32()
568 canonicalStore = bptree.NewBPTree32()
569}