Search Apps Documentation Source Content File Folder Download Copy Actions Download

users_test.gno

13.37 Kb · 430 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, ok := roNS.Get(alice)
135		uassert.True(t, ok)
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, ok := nameStore.Get(alice)
143		uassert.False(t, raw.(*UserData).deleted)
144
145		// Addr store
146		aliceDataRaw, ok = roAS.Get(aliceAddr.String())
147		uassert.True(t, ok)
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, ok = 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
166		rawRoData, ok := roNS.Get(alice)
167		// uassert.False(t, ok)
168		// XXX: not sure what to do here, as the tree technically has the data so returns ok
169		// However the data is intercepted and something else (nil in this case) is returned.
170		// should we handle this somehow?
171
172		uassert.Equal(t, rawRoData, nil)
173		_, ok = rawRoData.(*UserData) // shouldn't be castable
174		uassert.False(t, ok)
175	})
176}
177
178func TestResolveAny(cur realm, t *testing.T) {
179	testing.SetRealm(testing.NewCodeRealm(initControllerPath))
180
181	t.Run("name", func(t *testing.T) {
182		cleanStore(t)
183
184		urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
185
186		res, _ := ResolveAny(alice)
187
188		uassert.Equal(t, aliceAddr, res.Addr())
189		uassert.Equal(t, alice, res.Name())
190	})
191
192	t.Run("address", func(t *testing.T) {
193		cleanStore(t)
194
195		urequire.NoError(t, RegisterUser(cross(cur), alice, aliceAddr))
196
197		res, _ := ResolveAny(aliceAddr.String())
198
199		uassert.Equal(t, aliceAddr, res.Addr())
200		uassert.Equal(t, alice, res.Name())
201	})
202
203	t.Run("not_registered", func(t *testing.T) {
204		cleanStore(t)
205
206		res, _ := ResolveAny(aliceAddr.String())
207
208		uassert.Equal(t, nil, res)
209	})
210}
211
212func TestProposeErrors(cur realm, t *testing.T) {
213	t.Run("propose_register_user_errors", func(t *testing.T) {
214		urequire.PanicsWithMessage(t, cur, ErrInvalidUsername.Error(), func() {
215			ProposeRegisterUser(cur, "bad name", aliceAddr)
216		})
217		urequire.PanicsWithMessage(t, cur, ErrInvalidAddress.Error(), func() {
218			ProposeRegisterUser(cur, alice, "badaddress")
219		})
220	})
221
222	t.Run("propose_update_name_errors", func(t *testing.T) {
223		cleanStore(t)
224
225		urequire.PanicsWithMessage(t, cur, ErrInvalidAddress.Error(), func() {
226			ProposeUpdateName(cur, "badaddress", "alice1")
227		})
228		urequire.PanicsWithMessage(t, cur, ErrInvalidUsername.Error(), func() {
229			ProposeUpdateName(cur, aliceAddr, "bad name")
230		})
231		// Note: unregistered user is not checked at proposal creation time.
232		// The callback handles it at execution time.
233	})
234
235	t.Run("propose_delete_user_errors", func(t *testing.T) {
236		cleanStore(t)
237
238		urequire.PanicsWithMessage(t, cur, ErrInvalidAddress.Error(), func() {
239			ProposeDeleteUser(cur, "badaddress")
240		})
241		// Note: unregistered user is not checked at proposal creation time.
242		// The callback handles it at execution time.
243	})
244}
245
246// Audit finding #6: ProposeControllerAdditionAndRemoval used to return early
247// from its callback when toAdd was already whitelisted (addToWhitelist
248// returned ErrAlreadyWhitelisted), skipping the remove step entirely. A
249// passed swap proposal would silently leave the old controller active.
250//
251// applyControllerSwap is the extracted callback body. These tests exercise
252// it directly to confirm:
253//   - normal swap (toAdd new, toRemove present) succeeds and updates state
254//   - toAdd already present is benign — remove still happens
255//   - toRemove already absent is benign — add still happens, no error
256//   - errors that aren't the idempotency cases still propagate
257func TestApplyControllerSwap(t *testing.T) {
258	// Use unique addresses per subtest to avoid having to drain the
259	// controllers set (addrset.Set has no clear/iterate-all method, and
260	// state persists across subtests in this package's tests).
261	t.Run("normal swap A->B", func(t *testing.T) {
262		a := testutils.TestAddress("swapA1")
263		b := testutils.TestAddress("swapB1")
264		controllers.Add(a)
265		defer controllers.Remove(b)
266
267		uassert.NoError(t, applyControllerSwap(b, a))
268		uassert.True(t, controllers.Has(b), "b should be whitelisted")
269		uassert.False(t, controllers.Has(a), "a should be removed")
270	})
271
272	t.Run("toAdd already whitelisted, toRemove present", func(t *testing.T) {
273		// Regression for audit finding #6. Before the fix, this returned
274		// ErrAlreadyWhitelisted from the swap callback, skipping the remove
275		// step entirely — the swap silently no-op'd and the old controller
276		// stayed active.
277		a := testutils.TestAddress("swapA2")
278		b := testutils.TestAddress("swapB2")
279		controllers.Add(a)
280		controllers.Add(b)
281		defer controllers.Remove(b)
282
283		uassert.NoError(t, applyControllerSwap(b, a))
284		uassert.True(t, controllers.Has(b), "b should remain whitelisted")
285		uassert.False(t, controllers.Has(a), "a should be removed even though b was already in")
286	})
287
288	t.Run("toRemove already absent, toAdd new", func(t *testing.T) {
289		a := testutils.TestAddress("swapA3")
290		b := testutils.TestAddress("swapB3")
291		c := testutils.TestAddress("swapC3") // never added
292		controllers.Add(a)
293		defer controllers.Remove(a)
294		defer controllers.Remove(b)
295
296		// Remove c which isn't in the set; should be benign.
297		uassert.NoError(t, applyControllerSwap(b, c))
298		uassert.True(t, controllers.Has(b), "b should be whitelisted")
299		uassert.True(t, controllers.Has(a), "a should still be whitelisted (unchanged)")
300		uassert.False(t, controllers.Has(c), "c was never in the set")
301	})
302
303	t.Run("both idempotency cases at once", func(t *testing.T) {
304		// toAdd already in, toRemove already out — should succeed cleanly,
305		// no state change.
306		a := testutils.TestAddress("swapA4")
307		b := testutils.TestAddress("swapB4")
308		c := testutils.TestAddress("swapC4")
309		controllers.Add(a)
310		controllers.Add(b)
311		defer controllers.Remove(a)
312		defer controllers.Remove(b)
313
314		uassert.NoError(t, applyControllerSwap(b, c))
315		uassert.True(t, controllers.Has(a))
316		uassert.True(t, controllers.Has(b))
317		uassert.False(t, controllers.Has(c))
318	})
319}
320
321// Audit finding #20: the controller whitelist must be queryable from
322// outside the package so operators can monitor authority without
323// source-diving or replaying governance proposals.
324func TestControllerQueries(t *testing.T) {
325	t.Run("IsController reflects whitelist state", func(t *testing.T) {
326		a := testutils.TestAddress("queryA1")
327		b := testutils.TestAddress("queryB1")
328
329		// Before any add: both report false.
330		uassert.False(t, IsController(a))
331		uassert.False(t, IsController(b))
332
333		controllers.Add(a)
334		defer controllers.Remove(a)
335
336		uassert.True(t, IsController(a))
337		uassert.False(t, IsController(b))
338	})
339
340	t.Run("Controllers returns a snapshot of current whitelist", func(t *testing.T) {
341		// Use distinct addresses that aren't already in the set from
342		// other tests in this file.
343		a := testutils.TestAddress("queryA2")
344		b := testutils.TestAddress("queryB2")
345		c := testutils.TestAddress("queryC2")
346
347		controllers.Add(a)
348		controllers.Add(b)
349		controllers.Add(c)
350		defer controllers.Remove(a)
351		defer controllers.Remove(b)
352		defer controllers.Remove(c)
353
354		got := Controllers()
355
356		// All three must appear (regardless of order vs other tests'
357		// leftover entries — we only assert ours are present).
358		seen := map[string]bool{}
359		for _, e := range got {
360			seen[e.String()] = true
361		}
362		uassert.True(t, seen[a.String()], "snapshot must contain a")
363		uassert.True(t, seen[b.String()], "snapshot must contain b")
364		uassert.True(t, seen[c.String()], "snapshot must contain c")
365	})
366
367	t.Run("Controllers returns a copy — caller mutation does not affect realm", func(t *testing.T) {
368		a := testutils.TestAddress("queryA3")
369		controllers.Add(a)
370		defer controllers.Remove(a)
371
372		got := Controllers()
373		// Mutate the returned slice. The realm's controllers set must not
374		// be affected.
375		got = got[:0]
376
377		uassert.True(t, controllers.Has(a), "realm state must be unaffected by caller-side slice mutation")
378		uassert.True(t, IsController(a))
379	})
380}
381
382// ProposeRegisterUser auto-injects a CANONICAL COLLISION warning into
383// the proposal description when the proposed name canonical-collides
384// with an existing registration. The warning surfaces the colliding
385// existing name to voters; the proposal still goes through if voted in
386// (decision #3, DAO grants always bypass).
387func TestProposeRegisterUser_CollisionWarning(cur realm, t *testing.T) {
388	testing.SetRealm(testing.NewCodeRealm(initControllerPath))
389
390	t.Run("warning_injected_on_collision", func(t *testing.T) {
391		cleanStore(t)
392		urequire.NoError(t, RegisterUser(cross(cur), "vitalik", aliceAddr))
393
394		req := ProposeRegisterUser(cur, "vital1k", bobAddr)
395		uassert.True(t,
396			strings.Contains(req.Description(), "CANONICAL COLLISION"),
397			"description must include collision warning")
398		uassert.True(t,
399			strings.Contains(req.Description(), "`vitalik`"),
400			"description must name the existing colliding registration")
401	})
402
403	t.Run("no_warning_when_no_collision", func(t *testing.T) {
404		cleanStore(t)
405
406		req := ProposeRegisterUser(cur, "freshname", aliceAddr)
407		uassert.False(t,
408			strings.Contains(req.Description(), "CANONICAL COLLISION"),
409			"description must not include collision warning when name is fresh")
410	})
411}
412
413// Note: full execution of ProposeRegisterUser/ProposeUpdateName closures
414// (with ignoreCanonical=true → later-wins overwrite) is exercised end-to-
415// end by the integration test in gno.land/pkg/integration/testdata/.
416// The bypass-write semantics themselves are covered here at the unit
417// level by TestRegisterUserIgnoreCanonical and TestUpdateName_CanonicalCollision.
418
419// TODO Uncomment after gnoweb /u/ page.
420//func TestUserRenderLink(cur realm, t *testing.T) {
421//	testing.SetOriginCaller(whitelistedCallerAddr)
422//	cleanStore(t)
423//
424//	urequire.NoError(t, RegisterUser(alice, aliceAddr))
425//
426//	data, _ := ResolveName(alice)
427//	uassert.Equal(t, data.RenderLink(""), ufmt.Sprintf("[@%s](/u/%s)", alice, alice))
428//	text := "my link text!"
429//	uassert.Equal(t, data.RenderLink(text), ufmt.Sprintf("[%s](/u/%s)", text, alice))
430//}