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