million_test.gno
10.91 Kb · 319 lines
1package million
2
3import (
4 "strings"
5 "testing"
6
7 "chain"
8 "chain/banker"
9
10 "gno.land/p/nt/testutils/v0"
11 "gno.land/p/nt/uassert/v0"
12)
13
14var (
15 deployer = testutils.TestAddress("deployer")
16 alice = testutils.TestAddress("alice")
17 bob = testutils.TestAddress("bob")
18)
19
20var realmAddr = chain.PackageAddress("gno.land/r/g1sw5xklxjjuv0yvuxy5f5s3l3mnj0nqq626a9wr/million/v0")
21
22// Tests own the admin role through a test address rather than DefaultAdmin.
23func init() {
24 owner = deployer
25}
26
27// fund credits the realm with what the next paint call claims to send, as
28// the chain would before running the message, and resets the per-message
29// origin-send spending limit that the previous call used up.
30func fund(n int64) {
31 testing.SetOriginSpend(nil)
32 if n > 0 {
33 testing.IssueCoins(realmAddr, ugnot(n))
34 }
35}
36
37func balanceOf(a address) int64 {
38 return banker.NewReadonlyBanker().GetCoin(a, Denom)
39}
40
41func ugnot(n int64) chain.Coins {
42 return chain.NewCoins(chain.NewCoin(Denom, n))
43}
44
45// testing.SetRealm only affects the frame that calls it, so it cannot be
46// wrapped in a helper: every test sets the caller inline.
47
48func TestInitialState(t *testing.T) {
49 uassert.Equal(t, Width, GetWidth())
50 uassert.Equal(t, Height, GetHeight())
51 uassert.Equal(t, DefaultPrice, Price())
52 uassert.Equal(t, MaxBatch, MaxBatchSize())
53 uassert.Equal(t, 0, Pixel(0, 0))
54 uassert.Equal(t, deployer, Owner())
55 uassert.Equal(t, 16, len(strings.Split(Palette(), ",")))
56}
57
58func TestPaintOnePixel(cur realm, t *testing.T) {
59 adminBefore := balanceOf(owner)
60
61 testing.SetRealm(testing.NewUserRealm(alice))
62 testing.SetOriginSend(ugnot(price))
63 fund(price)
64 Paint(cross(cur), 10, 20, 6)
65
66 uassert.Equal(t, 6, Pixel(10, 20))
67 uassert.Equal(t, 1, Painted())
68 uassert.Equal(t, price, Revenue())
69 uassert.Equal(t, int64(1), Paints())
70 // The payment went straight to the admin, nothing stayed in the realm.
71 uassert.Equal(t, adminBefore+price, balanceOf(owner))
72 uassert.Equal(t, int64(0), balanceOf(realmAddr))
73
74 // Anyone can repaint for the same price; the count does not grow.
75 testing.SetRealm(testing.NewUserRealm(bob))
76 testing.SetOriginSend(ugnot(price))
77 fund(price)
78 Paint(cross(cur), 10, 20, 15)
79 uassert.Equal(t, 15, Pixel(10, 20))
80 uassert.Equal(t, 1, Painted())
81 uassert.Equal(t, 2*price, Revenue())
82 uassert.Equal(t, adminBefore+2*price, balanceOf(owner))
83}
84
85func TestAdminPaintsForFree(cur realm, t *testing.T) {
86 adminBefore := balanceOf(owner)
87 revenueBefore := Revenue()
88
89 testing.SetRealm(testing.NewUserRealm(owner))
90 testing.SetOriginSend(nil)
91 Paint(cross(cur), 400, 400, 8)
92 uassert.Equal(t, 8, Pixel(400, 400))
93
94 testing.SetRealm(testing.NewUserRealm(owner))
95 testing.SetOriginSend(nil)
96 PaintBatch(cross(cur), "401,400,9;402,400,10")
97 uassert.Equal(t, 10, Pixel(402, 400))
98
99 // Free means exactly free: the admin must not attach coins either.
100 testing.SetRealm(testing.NewUserRealm(owner))
101 testing.SetOriginSend(ugnot(price))
102 uassert.AbortsContains(t, cur, "must send exactly 0ugnot", func() { Paint(cross(cur), 403, 400, 1) })
103
104 uassert.Equal(t, revenueBefore, Revenue())
105 uassert.Equal(t, adminBefore, balanceOf(owner))
106}
107
108func TestPaintRejectsWrongPayment(cur realm, t *testing.T) {
109 before := Painted()
110
111 testing.SetRealm(testing.NewUserRealm(alice))
112 testing.SetOriginSend(ugnot(price-1))
113 uassert.AbortsContains(t, cur, "must send exactly", func() { Paint(cross(cur), 0, 0, 1) })
114
115 testing.SetRealm(testing.NewUserRealm(alice))
116 testing.SetOriginSend(ugnot(price+1))
117 uassert.AbortsContains(t, cur, "must send exactly", func() { Paint(cross(cur), 0, 0, 1) })
118
119 testing.SetRealm(testing.NewUserRealm(alice))
120 testing.SetOriginSend(nil)
121 uassert.AbortsContains(t, cur, "must send exactly", func() { Paint(cross(cur), 0, 0, 1) })
122
123 testing.SetRealm(testing.NewUserRealm(alice))
124 testing.SetOriginSend(chain.NewCoins(chain.NewCoin("uatom", price)))
125 uassert.AbortsContains(t, cur, "only ugnot is accepted", func() { Paint(cross(cur), 0, 0, 1) })
126
127 uassert.Equal(t, before, Painted())
128 uassert.Equal(t, 0, Pixel(0, 0))
129}
130
131func TestPaintRejectsInvalidPixels(cur realm, t *testing.T) {
132 testing.SetRealm(testing.NewUserRealm(alice))
133 testing.SetOriginSend(ugnot(price))
134 uassert.AbortsContains(t, cur, "out of range", func() { Paint(cross(cur), Width, 0, 1) })
135 uassert.AbortsContains(t, cur, "out of range", func() { Paint(cross(cur), 0, Height, 1) })
136 uassert.AbortsContains(t, cur, "out of range", func() { Paint(cross(cur), -1, 0, 1) })
137 uassert.AbortsContains(t, cur, "color must be", func() { Paint(cross(cur), 0, 0, 0) })
138 uassert.AbortsContains(t, cur, "color must be", func() { Paint(cross(cur), 0, 0, 16) })
139 uassert.AbortsContains(t, cur, "color must be", func() { Paint(cross(cur), 0, 0, -1) })
140}
141
142func TestPaintRejectsCodeRealm(cur realm, t *testing.T) {
143 // Regression for the OriginSend bypass: an intermediate realm forwards
144 // the call while the envelope claims the coins were sent.
145 testing.SetRealm(testing.NewCodeRealm("gno.land/r/attacker/wrapper"))
146 testing.SetOriginSend(ugnot(price))
147 uassert.AbortsContains(t, cur, "directly by a user", func() { Paint(cross(cur), 1, 1, 1) })
148 uassert.Equal(t, 0, Pixel(1, 1))
149}
150
151func TestPaintBatch(cur realm, t *testing.T) {
152 before := Painted()
153 testing.SetRealm(testing.NewUserRealm(alice))
154 testing.SetOriginSend(ugnot(3 * price))
155 fund(3 * price)
156 PaintBatch(cross(cur), "100,100,1;101,100,2;102,100,3;")
157
158 uassert.Equal(t, 1, Pixel(100, 100))
159 uassert.Equal(t, 2, Pixel(101, 100))
160 uassert.Equal(t, 3, Pixel(102, 100))
161 uassert.Equal(t, before+3, Painted())
162
163 row := Rows(100, 101)
164 uassert.Equal(t, Width, len(row))
165 uassert.Equal(t, "123", row[100:103])
166}
167
168func TestPaintBatchIsAtomic(cur realm, t *testing.T) {
169 // One bad pixel rejects the whole batch before anything is written.
170 testing.SetRealm(testing.NewUserRealm(alice))
171 testing.SetOriginSend(ugnot(2*price))
172 uassert.AbortsContains(t, cur, "out of range", func() { PaintBatch(cross(cur), "200,200,1;9999,0,1") })
173 uassert.Equal(t, 0, Pixel(200, 200))
174
175 testing.SetRealm(testing.NewUserRealm(alice))
176 testing.SetOriginSend(ugnot(price))
177 uassert.AbortsContains(t, cur, "must send exactly", func() { PaintBatch(cross(cur), "200,200,1;201,200,1") })
178 uassert.Equal(t, 0, Pixel(200, 200))
179
180 testing.SetRealm(testing.NewUserRealm(alice))
181 testing.SetOriginSend(ugnot(price))
182 uassert.AbortsContains(t, cur, "x,y,color", func() { PaintBatch(cross(cur), "garbage") })
183 uassert.AbortsContains(t, cur, "x,y,color", func() { PaintBatch(cross(cur), "") })
184}
185
186func TestPaintBatchTooLarge(cur realm, t *testing.T) {
187 var sb strings.Builder
188 for i := 0; i <= MaxBatch; i++ {
189 if i > 0 {
190 sb.WriteByte(';')
191 }
192 sb.WriteString("0,")
193 sb.WriteString(itoa(i))
194 sb.WriteString(",1")
195 }
196 testing.SetRealm(testing.NewUserRealm(alice))
197 testing.SetOriginSend(ugnot(int64(MaxBatch+1)*price))
198 uassert.AbortsContains(t, cur, "too many pixels", func() { PaintBatch(cross(cur), sb.String()) })
199}
200
201func TestSetPrice(cur realm, t *testing.T) {
202 testing.SetRealm(testing.NewUserRealm(alice))
203 testing.SetOriginSend(nil)
204 uassert.AbortsWithMessage(t, cur, "owner only", func() { SetPrice(cross(cur), 1) })
205
206 testing.SetRealm(testing.NewUserRealm(owner))
207 testing.SetOriginSend(nil)
208 uassert.AbortsWithMessage(t, cur, "price must not be negative", func() { SetPrice(cross(cur), -1) })
209 SetPrice(cross(cur), 0)
210 uassert.Equal(t, int64(0), Price())
211
212 // Free painting requires sending nothing.
213 testing.SetRealm(testing.NewUserRealm(alice))
214 testing.SetOriginSend(nil)
215 Paint(cross(cur), 300, 300, 4)
216 uassert.Equal(t, 4, Pixel(300, 300))
217 testing.SetRealm(testing.NewUserRealm(alice))
218 testing.SetOriginSend(ugnot(1))
219 uassert.AbortsContains(t, cur, "must send exactly", func() { Paint(cross(cur), 300, 301, 4) })
220
221 testing.SetRealm(testing.NewUserRealm(owner))
222 testing.SetOriginSend(nil)
223 SetPrice(cross(cur), DefaultPrice)
224}
225
226func TestWithdraw(cur realm, t *testing.T) {
227 testing.IssueCoins(realmAddr, ugnot(1_000))
228 ro := banker.NewReadonlyBanker()
229 bobBefore := ro.GetCoin(bob, Denom)
230
231 testing.SetRealm(testing.NewUserRealm(alice))
232 testing.SetOriginSend(nil)
233 uassert.AbortsWithMessage(t, cur, "owner only", func() { Withdraw(cross(cur), alice, 1) })
234
235 testing.SetRealm(testing.NewUserRealm(owner))
236 testing.SetOriginSend(nil)
237 uassert.AbortsWithMessage(t, cur, "amount must be positive", func() { Withdraw(cross(cur), bob, 0) })
238 uassert.AbortsWithMessage(t, cur, "invalid address", func() { Withdraw(cross(cur), address("nope"), 1) })
239 Withdraw(cross(cur), bob, 600)
240
241 uassert.Equal(t, bobBefore+600, ro.GetCoin(bob, Denom))
242 uassert.Equal(t, int64(400), ro.GetCoin(realmAddr, Denom))
243 // Leave the realm empty for the tests that assert on it.
244 Withdraw(cross(cur), bob, 400)
245}
246
247func TestTransferOwnership(cur realm, t *testing.T) {
248 original := owner
249
250 testing.SetRealm(testing.NewUserRealm(alice))
251 testing.SetOriginSend(nil)
252 uassert.AbortsWithMessage(t, cur, "owner only", func() { TransferOwnership(cross(cur), alice) })
253
254 testing.SetRealm(testing.NewUserRealm(original))
255 testing.SetOriginSend(nil)
256 uassert.AbortsWithMessage(t, cur, "invalid address", func() { TransferOwnership(cross(cur), address("nope")) })
257 TransferOwnership(cross(cur), alice)
258 uassert.Equal(t, alice, Owner())
259
260 testing.SetRealm(testing.NewUserRealm(alice))
261 testing.SetOriginSend(nil)
262 TransferOwnership(cross(cur), original)
263 uassert.Equal(t, original, Owner())
264}
265
266func TestVersionAndDirtyRows(cur realm, t *testing.T) {
267 v := Version()
268 uassert.Equal(t, "", DirtyRows(v))
269
270 testing.SetRealm(testing.NewUserRealm(alice))
271 testing.SetOriginSend(ugnot(3 * price))
272 fund(3 * price)
273 PaintBatch(cross(cur), "0,700,1;1,700,2;0,701,3")
274
275 uassert.Equal(t, v+1, Version())
276 uassert.Equal(t, "700,701", DirtyRows(v))
277 uassert.Equal(t, "", DirtyRows(Version()))
278
279 // A second call on one of those rows moves only that row forward.
280 testing.SetRealm(testing.NewUserRealm(bob))
281 testing.SetOriginSend(ugnot(price))
282 fund(price)
283 Paint(cross(cur), 5, 701, 4)
284 uassert.Equal(t, v+2, Version())
285 uassert.Equal(t, "701", DirtyRows(v+1))
286 uassert.Equal(t, "700,701", DirtyRows(v))
287
288 // A rejected paint changes nothing.
289 testing.SetRealm(testing.NewUserRealm(bob))
290 testing.SetOriginSend(ugnot(1))
291 uassert.AbortsContains(t, cur, "must send exactly", func() { Paint(cross(cur), 5, 702, 4) })
292 uassert.Equal(t, v+2, Version())
293 uassert.Equal(t, "", DirtyRows(v+2))
294
295 // Negative "since" behaves like zero: painted rows only, never blank ones.
296 uassert.Equal(t, DirtyRows(0), DirtyRows(-1))
297 uassert.True(t, !strings.Contains(","+DirtyRows(0)+",", ",799,"), "row 799 was never painted")
298}
299
300func TestRender(t *testing.T) {
301 out := Render("")
302 uassert.True(t, strings.Contains(out, "# Million Gno"), "title")
303 uassert.True(t, strings.Contains(out, "| Size | 1250 x 800 |"), "size row")
304 uassert.True(t, strings.Contains(out, "| Price per pixel | 50000ugnot |"), "price row")
305 uassert.True(t, strings.Contains(out, "| Admin | "+owner.String()+" |"), "admin row")
306 uassert.True(t, strings.Contains(out, "$help&func=PaintBatch"), "help link")
307}
308
309func itoa(n int) string {
310 if n == 0 {
311 return "0"
312 }
313 var b []byte
314 for n > 0 {
315 b = append([]byte{byte('0' + n%10)}, b...)
316 n /= 10
317 }
318 return string(b)
319}