Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

faucet_test.gno

12.94 Kb · 380 lines
  1package faucet
  2
  3import (
  4	"strconv"
  5
  6	"chain"
  7	"chain/banker"
  8	"testing"
  9
 10	"gno.land/p/nt/uassert/v0"
 11	"gno.land/p/nt/urequire/v0"
 12)
 13
 14const (
 15	zoe  = address("g12cs4cehujpffpjpywmkqj43m6u5ya53nj69sjz") // a friend with nothing
 16	kim  = address("g1jvh5ukk07dvd57fxefcp5aa29xaydxmxs7myyp") // a second approver
 17	carl = address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") // nobody in particular
 18)
 19
 20func balanceOf(a address) int64 {
 21	return banker.NewReadonlyBanker().GetCoins(a).AmountOf(Denom)
 22}
 23
 24// fund puts ugnot in the float the way a bank send to the realm address does,
 25// with no call involved. Approve spends this, so a test that funds more than
 26// it pays leaves a balance behind and ExampleRender sees it.
 27func fund(n int64) {
 28	testing.IssueCoins(Address(), chain.NewCoins(chain.NewCoin(Denom, n)))
 29}
 30
 31func TestFreshFaucetIsEmptyAndOwned(t *testing.T) {
 32	reset()
 33	uassert.True(t, Owner.IsValid(), "the owner is a real address")
 34	uassert.True(t, Address().IsValid(), "the float address")
 35	uassert.True(t, zoe.IsValid())
 36	uassert.True(t, kim.IsValid())
 37	uassert.True(t, carl.IsValid())
 38
 39	uassert.Equal(t, 0, Requests(), "nothing is seeded")
 40	uassert.Equal(t, int64(1), NextID())
 41	uassert.Equal(t, int64(0), TotalSent())
 42	uassert.Equal(t, int64(DefaultMaxPerRequest), MaxPerRequest())
 43	uassert.True(t, IsApprover(Owner.String()), "the owner approves by default")
 44	uassert.False(t, IsApprover(carl.String()))
 45	uassert.Equal(t, "", Status(1), "no such request")
 46}
 47
 48// The whole point, end to end: carl asks on zoe's behalf, the owner releases
 49// it, and zoe (who could never have paid the gas to ask) has coins.
 50func TestRequestForAFriendThenApprove(cur realm, t *testing.T) {
 51	reset()
 52	urequire.Equal(t, int64(0), Balance(), "start from an empty float")
 53	fund(300_000_000)
 54
 55	testing.SetRealm(testing.NewUserRealm(carl))
 56	id := Request(cross(cur), zoe.String(), 100_000_000, "no gas, needs to try the chain")
 57	uassert.Equal(t, int64(1), id)
 58	uassert.Equal(t, StatusPending, Status(id))
 59	uassert.Equal(t, int64(1), Pending())
 60	uassert.Equal(t, int64(0), balanceOf(zoe), "a request moves nothing")
 61	uassert.Equal(t, int64(2), NextID(), "and the next id is now readable")
 62
 63	// The filer is not an approver just because they filed.
 64	testing.SetRealm(testing.NewUserRealm(carl))
 65	uassert.AbortsContains(t, cur, "is not an approver", func() {
 66		Approve(cross(cur), id, zoe.String(), 100_000_000)
 67	})
 68
 69	testing.SetRealm(testing.NewUserRealm(Owner))
 70	Approve(cross(cur), id, zoe.String(), 100_000_000)
 71
 72	uassert.Equal(t, int64(100_000_000), balanceOf(zoe), "the coins landed")
 73	uassert.Equal(t, int64(200_000_000), Balance(), "out of the float, not the approver")
 74	uassert.Equal(t, StatusSent, Status(id))
 75	uassert.Equal(t, int64(100_000_000), TotalSent())
 76	uassert.Equal(t, int64(1), SentCount())
 77	uassert.Equal(t, int64(0), Pending())
 78	uassert.Equal(t, int64(100_000_000), ReceivedBy(zoe.String()))
 79
 80	// A decision happens once.
 81	testing.SetRealm(testing.NewUserRealm(Owner))
 82	uassert.AbortsContains(t, cur, "already sent", func() {
 83		Approve(cross(cur), id, zoe.String(), 100_000_000)
 84	})
 85
 86	// Put the float back so ExampleRender starts from zero.
 87	testing.SetRealm(testing.NewUserRealm(Owner))
 88	Withdraw(cross(cur), Balance())
 89	uassert.Equal(t, int64(0), Balance())
 90}
 91
 92// The guard that makes it safe to sign Request and Approve in the same
 93// transaction before either has run. The approval names an id it had to guess
 94// from NextID; if a different request took that id, the amounts and the
 95// recipient no longer agree and nothing is paid.
 96func TestApproveRefusesWhenTheIDMovedUnderYou(cur realm, t *testing.T) {
 97	reset()
 98	fund(500_000_000)
 99
100	// What the caller read: the next id will be 1.
101	urequire.Equal(t, int64(1), NextID())
102
103	// What actually landed at id 1: somebody else's ask, first.
104	testing.SetRealm(testing.NewUserRealm(kim))
105	Request(cross(cur), kim.String(), 50_000_000, "me first")
106	testing.SetRealm(testing.NewUserRealm(carl))
107	Request(cross(cur), zoe.String(), 100_000_000, "for zoe")
108
109	testing.SetRealm(testing.NewUserRealm(Owner))
110	uassert.AbortsContains(t, cur, "the id moved under you, nothing was paid", func() {
111		Approve(cross(cur), 1, zoe.String(), 100_000_000)
112	})
113	uassert.Equal(t, int64(0), balanceOf(zoe), "nobody was paid")
114	uassert.Equal(t, StatusPending, Status(1), "and nothing was decided")
115
116	// The same guard catches a wrong amount at the right id.
117	testing.SetRealm(testing.NewUserRealm(Owner))
118	uassert.AbortsContains(t, cur, "the id moved under you, nothing was paid", func() {
119		Approve(cross(cur), 2, zoe.String(), 999_000_000)
120	})
121
122	testing.SetRealm(testing.NewUserRealm(Owner))
123	Approve(cross(cur), 2, zoe.String(), 100_000_000)
124	uassert.Equal(t, int64(100_000_000), balanceOf(zoe))
125
126	testing.SetRealm(testing.NewUserRealm(Owner))
127	Deny(cross(cur), 1, "already funded elsewhere")
128	uassert.Equal(t, StatusDenied, Status(1))
129	uassert.Equal(t, int64(1), DeniedCount())
130	uassert.Equal(t, int64(0), Pending())
131
132	testing.SetRealm(testing.NewUserRealm(Owner))
133	Withdraw(cross(cur), Balance())
134}
135
136func TestRequestRefusesBadInput(cur realm, t *testing.T) {
137	reset()
138	tests := []struct {
139		name   string
140		to     string
141		amount int64
142		reason string
143		want   string
144	}{
145		{"not an address", "nope", 1_000_000, "hi", "is not a valid address"},
146		{"zero", zoe.String(), 0, "hi", "must be a positive number"},
147		{"negative", zoe.String(), -1, "hi", "must be a positive number"},
148		{"over the cap", zoe.String(), DefaultMaxPerRequest + 1, "hi", "is over the per-request cap"},
149		{"no reason", zoe.String(), 1_000_000, "   ", "say what it is for"},
150	}
151	for _, tt := range tests {
152		testing.SetRealm(testing.NewUserRealm(carl))
153		uassert.AbortsContains(t, cur, tt.want, func() {
154			Request(cross(cur), tt.to, tt.amount, tt.reason)
155		}, tt.name)
156	}
157	uassert.Equal(t, 0, Requests(), "none of them were filed")
158	uassert.Equal(t, int64(1), NextID(), "and the id did not move")
159}
160
161func TestApproveRefusesWhatTheFloatCannotCover(cur realm, t *testing.T) {
162	reset()
163	fund(10_000_000)
164
165	testing.SetRealm(testing.NewUserRealm(carl))
166	id := Request(cross(cur), zoe.String(), 100_000_000, "more than is there")
167
168	testing.SetRealm(testing.NewUserRealm(Owner))
169	uassert.AbortsContains(t, cur, "float is 10000000ugnot", func() {
170		Approve(cross(cur), id, zoe.String(), 100_000_000)
171	})
172	uassert.Equal(t, StatusPending, Status(id), "still open, the money simply is not there")
173
174	testing.SetRealm(testing.NewUserRealm(Owner))
175	Withdraw(cross(cur), Balance())
176}
177
178func TestOnlyTheOwnerChangesWhoApproves(cur realm, t *testing.T) {
179	reset()
180
181	testing.SetRealm(testing.NewUserRealm(carl))
182	uassert.AbortsContains(t, cur, "owner only", func() {
183		AddApprover(cross(cur), carl.String())
184	})
185
186	testing.SetRealm(testing.NewUserRealm(Owner))
187	AddApprover(cross(cur), kim.String())
188	uassert.True(t, IsApprover(kim.String()))
189
190	// A second approver can decide, but cannot widen the roster.
191	fund(200_000_000)
192	testing.SetRealm(testing.NewUserRealm(carl))
193	id := Request(cross(cur), zoe.String(), 50_000_000, "kim vouches")
194	testing.SetRealm(testing.NewUserRealm(kim))
195	Approve(cross(cur), id, zoe.String(), 50_000_000)
196	uassert.Equal(t, StatusSent, Status(id))
197
198	testing.SetRealm(testing.NewUserRealm(kim))
199	uassert.AbortsContains(t, cur, "owner only", func() {
200		AddApprover(cross(cur), carl.String())
201	})
202
203	// The owner is the one approver that cannot be removed: a faucet with no
204	// approver is a faucet with a locked float.
205	testing.SetRealm(testing.NewUserRealm(Owner))
206	uassert.AbortsContains(t, cur, "the owner is always an approver", func() {
207		RemoveApprover(cross(cur), Owner.String())
208	})
209	testing.SetRealm(testing.NewUserRealm(Owner))
210	RemoveApprover(cross(cur), kim.String())
211	uassert.False(t, IsApprover(kim.String()))
212
213	testing.SetRealm(testing.NewUserRealm(Owner))
214	Withdraw(cross(cur), Balance())
215}
216
217func TestFundAndWithdrawAreOwnerReversible(cur realm, t *testing.T) {
218	reset()
219	fund(400_000_000)
220	testing.SetOriginSend(chain.NewCoins(chain.NewCoin(Denom, 400_000_000)))
221	testing.SetRealm(testing.NewUserRealm(kim))
222	Fund(cross(cur))
223	uassert.Equal(t, int64(400_000_000), Balance())
224
225	testing.SetRealm(testing.NewUserRealm(kim))
226	uassert.AbortsContains(t, cur, "owner only", func() {
227		Withdraw(cross(cur), 1)
228	})
229
230	testing.SetRealm(testing.NewUserRealm(Owner))
231	uassert.AbortsContains(t, cur, "float is 400000000ugnot", func() {
232		Withdraw(cross(cur), 400_000_001)
233	})
234
235	before := balanceOf(Owner)
236	testing.SetRealm(testing.NewUserRealm(Owner))
237	Withdraw(cross(cur), 400_000_000)
238	uassert.Equal(t, int64(0), Balance())
239	uassert.Equal(t, before+400_000_000, balanceOf(Owner))
240}
241
242func TestSetMaxPerRequest(cur realm, t *testing.T) {
243	reset()
244	testing.SetRealm(testing.NewUserRealm(carl))
245	uassert.AbortsContains(t, cur, "owner only", func() {
246		SetMaxPerRequest(cross(cur), 1)
247	})
248
249	testing.SetRealm(testing.NewUserRealm(Owner))
250	SetMaxPerRequest(cross(cur), 1_000_000_000)
251	uassert.Equal(t, int64(1_000_000_000), MaxPerRequest())
252
253	testing.SetRealm(testing.NewUserRealm(carl))
254	id := Request(cross(cur), zoe.String(), 900_000_000, "over the old cap, under the new one")
255	uassert.Equal(t, StatusPending, Status(id))
256}
257
258// A reason is arbitrary text from an arbitrary account, and this realm's path
259// is permanent, so the escaping has to be right before it ships. Assert the
260// dangerous SEQUENCES are dead on the page, not that some particular escape
261// was chosen.
262func TestRenderEscapesTheReason(cur realm, t *testing.T) {
263	reset()
264	testing.SetRealm(testing.NewUserRealm(carl))
265	id := Request(cross(cur), zoe.String(), 1_000_000,
266		"[click](https://evil.example) ![b](https://evil.example/p.png) <gno-columns>")
267
268	for _, page := range []string{Render(""), Render("req/" + "1")} {
269		uassert.False(t, contains(page, "](https://evil.example"), "no live link")
270		uassert.False(t, contains(page, "![b]"), "no image beacon")
271		uassert.False(t, contains(page, "<gno-columns>"), "no gnoweb chrome")
272		uassert.True(t, contains(page, "click"), "the readable text survives")
273	}
274	uassert.Equal(t, StatusPending, Status(id))
275}
276
277func contains(haystack, needle string) bool {
278	for i := 0; i+len(needle) <= len(haystack); i++ {
279		if haystack[i:i+len(needle)] == needle {
280			return true
281		}
282	}
283	return false
284}
285
286func TestGnotFormatting(t *testing.T) {
287	tests := []struct {
288		in   int64
289		want string
290	}{
291		{0, "0"},
292		{1, "0.000001"},
293		{1_500_000, "1.5"},
294		{100_000_000, "100"},
295		{5_000_000_000, "5000"},
296		{-2_500_000, "-2.5"},
297	}
298	for _, tt := range tests {
299		uassert.Equal(t, tt.want, gnot(tt.in))
300	}
301}
302
303func TestReqPath(t *testing.T) {
304	tests := []struct {
305		in   string
306		id   int64
307		want bool
308	}{
309		{"", 0, false},
310		{"req/1", 1, true},
311		{"req/42", 42, true},
312		{"req/0", 0, false},
313		{"req/x", 0, false},
314		{"req/", 0, false},
315		{"other", 0, false},
316	}
317	for _, tt := range tests {
318		id, ok := reqPath(tt.in)
319		uassert.Equal(t, tt.want, ok, tt.in)
320		uassert.Equal(t, tt.id, id, tt.in)
321	}
322}
323
324// A board that renders every request ever filed is a page that grows without
325// bound and a gas cost that grows with it, and this path is permanent. Assert
326// the cap holds and that what it hides is counted rather than lost.
327func TestRenderCapsEachTable(cur realm, t *testing.T) {
328	reset()
329	fund(200_000_000)
330
331	const filed = Rows + 5
332	for i := 0; i < filed; i++ {
333		testing.SetRealm(testing.NewUserRealm(carl))
334		Request(cross(cur), zoe.String(), 1_000_000, "batch")
335	}
336	urequire.Equal(t, int64(filed), Pending())
337
338	page := Render("")
339	uassert.Equal(t, Rows, countRows(page), "the open table is capped")
340	uassert.True(t, contains(page, "5 more not shown"), "and the rest are counted")
341
342	// Decide them all: the open table empties and the decided one caps in turn,
343	// newest first, so the most recent decision is on the page and the oldest
344	// is the one hidden.
345	for id := int64(1); id <= filed; id++ {
346		testing.SetRealm(testing.NewUserRealm(Owner))
347		Deny(cross(cur), id, "batch")
348	}
349	page = Render("")
350	uassert.Equal(t, int64(0), Pending())
351	uassert.Equal(t, Rows, countRows(page), "the decided table is capped too")
352	uassert.True(t, contains(page, "5 more not shown"))
353	uassert.True(t, contains(page, ":req/"+strconv.FormatInt(int64(filed), 10)+")"), "newest is shown")
354	uassert.False(t, contains(page, ":req/1)"), "oldest is the one hidden")
355
356	testing.SetRealm(testing.NewUserRealm(Owner))
357	Withdraw(cross(cur), Balance())
358}
359
360// countRows counts request rows on a page: every one links to :req/.
361func countRows(page string) int {
362	n, needle := 0, "](/r/moul/faucet/v1:req/"
363	for i := 0; i+len(needle) <= len(page); i++ {
364		if page[i:i+len(needle)] == needle {
365			n++
366		}
367	}
368	return n
369}
370
371// Path is a hand-written const and the module line is in gnomod.toml, so
372// nothing but this test stops them diverging. They must not: Address() derives
373// the float from Path, while the banker in Approve spends from cur.Address(),
374// which is derived by the VM. A divergence would make Balance() read a
375// different account from the one the payout leaves, and the realm would refuse
376// payouts it could afford while reporting a float it does not hold.
377func TestPathMatchesTheRealmItIsDeployedAs(cur realm, t *testing.T) {
378	uassert.Equal(t, cur.Address().String(), Address().String(),
379		"Path disagrees with the module line in gnomod.toml")
380}