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