Kind Realm (renderable)
Name v0
Namespace sys / namereg
Exported functions 11
Module gno.land/r/sys/namereg/v0
gno 0.9 users_test.gno gno
⧉
1 package namereg 2 3 import ( 4 "chain" 5 "testing" 6 7 "gno.land/p/nt/testutils/v0" 8 Canonicalize (s string) string
IsPaused () bool
IsReserved (stem string) bool
NewSetPausedExecutor (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, newPausedValue bool) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
ProposeDeleteUser (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string, reason string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
ProposeNewName (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string, newName string) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
ProposeNewRegisterPrice (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, newPrice int64) struct{title string; description string; executor gno.land/r/gov/dao.Executor; filter gno.land/r/gov/dao.Filter}
Register (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, username string)
Render (path string) string
RenderLatestUsersWidget (num int) string
ValidateNymFormat (username string) interface {Error func() string}
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
"gno.land/p/nt/uassert/v0"
9 "gno.land/p/nt/urequire/v0"
10
11 susers "gno.land/r/sys/users"
12 )
13
14 // cur is a zero-value realm used as a placeholder when forwarding to
15 // uassert/urequire dispatch helpers that gained an `rlm realm` param.
16 // These tests pass `func()` callbacks (no crossing inside the callback),
17 // so rlm is ignored — a nil realm here is safe.
18 var cur realm
19
20 func init(cur realm) {
21 // Unit tests run with DefaultHeight=123, so the production init() in init.gno
22 // (guarded on height==0) is a no-op in tests. Temporarily reset height so we
23 // can whitelist this realm as a controller for testing.
24 testing.SetHeight( 0 )
25 susers.AddControllerAtGenesis(cross(cur), chain.PackageAddress( "gno.land/r/sys/namereg/v0" ))
26 testing.SetHeight( 123 )
27 }
28
29 func TestRegister_Valid(cur realm, t *testing.T) {
30 // Stems chosen with no `l` characters to keep canonical forms identical
31 // to the originals — avoids accidental cross-subtest collisions with
32 // other tests that may register l-bearing names.
33 validUsernames := [] string {
34 "nym-bravo123" , // 5-char stem (minimum)
35 "nym-tango456" , // 5-char stem
36 "nym-victor789" , // 6-char stem
37 "nym-romeoecho012" , // 10-char stem
38 "nym-mikefoxhote345" , // 11-char stem
39 "nym-mikefoxhotenp678" , // 13-char stem (maximum)
40 }
41
42 for _, username := range validUsernames {
43 addr := testutils.TestAddress(username)
44
45 testing.SetRealm(testing.NewUserRealm(addr))
46 testing.SetOriginCaller(addr)
47 testing.SetOriginSend(chain.NewCoins(chain.NewCoin( "ugnot" , registerPrice)))
48
49 urequire.NotPanics(t, cur, func () {
50 Register(cross(cur), username)
51 })
52 }
53 }
54
55 func TestRegister_Free(cur realm, t *testing.T) {
56 oldPrice := registerPrice
57 defer func () { registerPrice = oldPrice }()
58 registerPrice = 0
59
60 addr := testutils.TestAddress( "free-payer" )
61
62 testing.SetRealm(testing.NewUserRealm(addr))
63 testing.SetOriginCaller(addr)
64
65 urequire.NotPanics(t, cur, func () {
66 Register(cross(cur), "nym-foxtrot456" )
67 })
68 }
69
70 func TestRegister_InvalidFormat(cur realm, t *testing.T) {
71 testing.SetOriginSend(chain.NewCoins(chain.NewCoin( "ugnot" , registerPrice)))
72 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress( "fmt-payer" )))
73
74 cases := [] string {
75 "" , // empty
76 " " , // whitespace
77 "alice123" , // missing nym- prefix
78 "usr-alice123" , // wrong prefix
79 "nym-" , // prefix only
80 "nym-abc123" , // stem too short (3 chars, need ≥5)
81 "nym-abcd123" , // stem too short (4 chars)
82 "nym-abcdefghijklmn123" , // stem too long (14 chars, need ≤13)
83 "nym-Alice123" , // uppercase in stem
84 "nym-al1ce123" , // digit in stem
85 "nym-alice12" , // only 2 trailing digits
86 "nym-alice1234" , // 4 trailing digits (extra digit in stem too long)
87 "nym-alice&#($)123" , // special chars in stem
88 }
89
90 for _, username := range cases {
91 uassert.AbortsWithMessage(t, cur, ErrInvalidFormat.Error(), func () {
92 Register(cross(cur), username)
93 })
94 }
95 }
96
97 func TestRegister_ReservedPrefix(cur realm, t *testing.T) {
98 testing.SetOriginSend(chain.NewCoins(chain.NewCoin( "ugnot" , registerPrice)))
99 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress( "prefix-payer" )))
100
101 cases := [] string {
102 "nym-gnoblah123" , // gno prefix
103 "nym-gnomeland456" , // gno prefix
104 "nym-glasgow012" , // gl prefix (most-confusable with bech32 g1...)
105 "nym-atomic123" , // atom prefix
106 "nym-atonex123" , // atone prefix
107 "nym-photons456" , // photon prefix
108 "nym-cosmoswide789" , // cosmos prefix
109 }
110
111 for _, username := range cases {
112 uassert.AbortsWithMessage(t, cur, ErrReservedPrefix.Error(), func () {
113 Register(cross(cur), username)
114 })
115 }
116 }
117
118 // gi is intentionally NOT in reservedPrefixes (i is visually distinct
119 // enough from 1/l that legitimate gi* names should be registerable).
120 // Phishing protection for the gi/gl visual class is enforced by
121 // canonical-collision detection in r/sys/users — see
122 // TestRegister_CanonicalCollision.
123 func TestRegister_GiPrefix_Allowed(cur realm, t *testing.T) {
124 addr := testutils.TestAddress( "gi-prefix-allowed" )
125 testing.SetRealm(testing.NewUserRealm(addr))
126 testing.SetOriginCaller(addr)
127 testing.SetOriginSend(chain.NewCoins(chain.NewCoin( "ugnot" , registerPrice)))
128
129 urequire.NotPanics(t, cur, func () {
130 Register(cross(cur), "nym-gillette789" )
131 })
132 }
133
134 func TestRegister_Blacklisted(cur realm, t *testing.T) {
135 testing.SetOriginSend(chain.NewCoins(chain.NewCoin( "ugnot" , registerPrice)))
136 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress( "blk-payer" )))
137
138 cases := [] string {
139 // `admin` is in reservedNames; with stem 5 chars it now matches the regex.
140 "nym-admin123" ,
141 // `support` is reserved (7 chars).
142 "nym-support456" ,
143 // `bitcoin` is reserved (7 chars).
144 "nym-bitcoin789" ,
145 // Implicit s-suffix expansion: `blog` is reserved, so `blogs` (stem
146 // = blog+s) must also be rejected.
147 "nym-blogs012" ,
148 // Canonical match: `setting` is reserved → canonical "setting" (no l).
149 // Try `setting` directly via 7-char stem.
150 "nym-setting345" ,
151 // Canonical match: `news` is reserved via `new+s` rule; canonical
152 // of `news` is `news`. With a 5-char stem this won't reach here
153 // (stem must be ≥5). Use a longer reserved name with l→i interaction:
154 // `tendermint` (10 chars, no l) → canonical = tendermint → blacklisted.
155 "nym-tendermint678" ,
156 }
157
158 for _, username := range cases {
159 uassert.AbortsWithMessage(t, cur, ErrBlacklisted.Error(), func () {
160 Register(cross(cur), username)
161 })
162 }
163 }
164
165 func TestRegister_CanonicalCollision(cur realm, t *testing.T) {
166 // First register a name. Same-digits confusable variant must collide
167 // in the unified susers canonical store.
168 addr1 := testutils.TestAddress( "collision-1" )
169 testing.SetRealm(testing.NewUserRealm(addr1))
170 testing.SetOriginCaller(addr1)
171 testing.SetOriginSend(chain.NewCoins(chain.NewCoin( "ugnot" , registerPrice)))
172 urequire.NotPanics(t, cur, func () {
173 Register(cross(cur), "nym-balloon123" )
174 })
175
176 // Then attempt to register a canonical-equivalent variant — same
177 // canonical full name (after l→i, etc.). Must reject from susers.
178 addr2 := testutils.TestAddress( "collision-2" )
179 testing.SetRealm(testing.NewUserRealm(addr2))
180 testing.SetOriginCaller(addr2)
181 testing.SetOriginSend(chain.NewCoins(chain.NewCoin( "ugnot" , registerPrice)))
182 uassert.AbortsWithMessage(t, cur, susers.ErrCanonicalCollision.Error(), func () {
183 Register(cross(cur), "nym-baiioon123" )
184 })
185
186 // Reverse direction also blocked.
187 addr3 := testutils.TestAddress( "collision-3" )
188 testing.SetRealm(testing.NewUserRealm(addr3))
189 testing.SetOriginCaller(addr3)
190 testing.SetOriginSend(chain.NewCoins(chain.NewCoin( "ugnot" , registerPrice)))
191 urequire.NotPanics(t, cur, func () {
192 Register(cross(cur), "nym-papaiio789" )
193 })
194
195 addr4 := testutils.TestAddress( "collision-4" )
196 testing.SetRealm(testing.NewUserRealm(addr4))
197 testing.SetOriginCaller(addr4)
198 testing.SetOriginSend(chain.NewCoins(chain.NewCoin( "ugnot" , registerPrice)))
199 uassert.AbortsWithMessage(t, cur, susers.ErrCanonicalCollision.Error(), func () {
200 Register(cross(cur), "nym-papallo789" )
201 })
202 }
203
204 // Decision #2 in Option B: store key is full canonical username (not
205 // stem). Same alpha stem with different digit suffixes coexists — does
206 // not collide.
207 func TestRegister_SameStemDifferentDigits_Coexist(cur realm, t *testing.T) {
208 addrA := testutils.TestAddress( "stem-coexist-A" )
209 testing.SetRealm(testing.NewUserRealm(addrA))
210 testing.SetOriginCaller(addrA)
211 testing.SetOriginSend(chain.NewCoins(chain.NewCoin( "ugnot" , registerPrice)))
212 urequire.NotPanics(t, cur, func () {
213 Register(cross(cur), "nym-foolbar000" )
214 })
215
216 addrB := testutils.TestAddress( "stem-coexist-B" )
217 testing.SetRealm(testing.NewUserRealm(addrB))
218 testing.SetOriginCaller(addrB)
219 testing.SetOriginSend(chain.NewCoins(chain.NewCoin( "ugnot" , registerPrice)))
220 urequire.NotPanics(t, cur, func () {
221 Register(cross(cur), "nym-foolbar999" )
222 })
223 }
224
225 func TestRegister_TakenUsername(cur realm, t *testing.T) {
226 testing.SetOriginSend(chain.NewCoins(chain.NewCoin( "ugnot" , registerPrice)))
227 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress( "taken-payer" )))
228
229 username := "nym-zulutwo567"
230
231 urequire.NotPanics(t, cur, func () {
232 Register(cross(cur), username)
233 })
234
235 // Re-registration of the same exact name is rejected by susers via
236 // ErrNameTaken (the nameStore.Has check precedes the canonical check).
237 uassert.AbortsWithMessage(t, cur, susers.ErrNameTaken.Error(), func () {
238 Register(cross(cur), username)
239 })
240 }
241
242 func TestRegister_InvalidPayment(cur realm, t *testing.T) {
243 addr := testutils.TestAddress( "payment-payer" )
244
245 testing.SetRealm(testing.NewUserRealm(addr))
246 testing.SetOriginCaller(addr)
247 testing.SetOriginSend(chain.NewCoins(chain.NewCoin( "ugnot" , 12 ))) // invalid
248
249 uassert.AbortsContains(t, cur, ErrInvalidPayment.Error(), func () {
250 Register(cross(cur), "nym-yankee345" )
251 })
252 }
253
254 // Audit finding #11: the OriginSend() payment check is only trustworthy
255 // when the direct caller is a pure EOA (IsUserCall). Any intermediate
256 // code realm can attach -send to the tx, keep the coins, and call
257 // Register; OriginSend() would still describe the envelope but namereg
258 // would receive nothing. The EOA-only guard must reject all such callers.
259 func TestRegister_IntermediateCodeRealmRejected(cur realm, t *testing.T) {
260 testing.SetRealm(testing.NewCodeRealm( "gno.land/r/evil/wrapper" ))
261 testing.SetOriginSend(chain.NewCoins(chain.NewCoin( "ugnot" , registerPrice)))
262
263 uassert.AbortsWithMessage(t, cur, ErrNonUserCall.Error(), func () {
264 Register(cross(cur), "nym-whisky345" )
265 })
266 }
267
268 // Audit finding #13: the OriginSend mismatch error must report the
269 // CURRENT registerPrice, not the value frozen at package init time.
270 func TestRegister_PaymentErrorReflectsCurrentPrice(cur realm, t *testing.T) {
271 oldPrice := registerPrice
272 defer func () { registerPrice = oldPrice }()
273
274 registerPrice = 5_000_000
275
276 addr := testutils.TestAddress( "price-payer" )
277 testing.SetRealm(testing.NewUserRealm(addr))
278 testing.SetOriginCaller(addr)
279 testing.SetOriginSend(chain.NewCoins(chain.NewCoin( "ugnot" , 1_000_000 ))) // wrong
280
281 uassert.AbortsContains(t, cur, "5000000" , func () {
282 Register(cross(cur), "nym-xrayfox678" )
283 })
284
285 uassert.AbortsContains(t, cur, ErrInvalidPayment.Error(), func () {
286 Register(cross(cur), "nym-xrayfox678" )
287 })
288 }
289
290 // Audit finding #14: ProposeNewRegisterPrice originally rejected only
291 // negative prices. A passed proposal to set a negative price would
292 // have been arithmetically nonsense; reject below MinRegisterPrice
293 // (currently 0) at proposal-creation time so governance can never
294 // underflow the floor.
295 func TestProposeNewRegisterPrice_floor(cur realm, t *testing.T) {
296 t.Run( "zero is accepted (free registration is the default)" , func (t *testing.T) {
297 urequire.NotPanics(t, cur, func () { ProposeNewRegisterPrice(cur, 0 ) })
298 })
299
300 t.Run( "negative is rejected" , func (t *testing.T) {
301 urequire.PanicsWithMessage(t, cur,
302 "price below floor: -1 ugnot < 0 ugnot (MinRegisterPrice)" ,
303 func () { ProposeNewRegisterPrice(cur, - 1 ) })
304 })
305
306 t.Run( "at floor is accepted" , func (t *testing.T) {
307 urequire.NotPanics(t, cur, func () { ProposeNewRegisterPrice(cur, MinRegisterPrice) })
308 })
309
310 t.Run( "above floor is accepted" , func (t *testing.T) {
311 urequire.NotPanics(t, cur, func () { ProposeNewRegisterPrice(cur, 1_000_000_000 ) })
312 })
313 }
314
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.