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