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

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