users_test.gno
11.21 Kb · 313 lines
1package namereg
2
3import (
4 "chain"
5 "testing"
6
7 "gno.land/p/nt/testutils/v0"
8 "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.
18var cur realm
19
20func 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/v1"))
26 testing.SetHeight(123)
27}
28
29func 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
55func 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
70func 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
97func 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.
123func 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
134func 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
165func 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.
207func 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
225func 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
242func 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.
259func 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.
270func 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.
295func 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}