Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}