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//}