Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

users_test.gno

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