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)  <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}