Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}