Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}