donate_test.gno
11.01 Kb · 254 lines
1package bolo
2
3import (
4 "chain"
5 "chain/banker"
6 "math"
7 "testing"
8
9 "gno.land/p/nt/seqid/v0"
10 "gno.land/p/nt/testutils/v0"
11)
12
13// The helpers' tests come first: a write's test that panics through a crossing
14// call stops the package's run, so a test after it would never report.
15
16func TestCheckGift(t *testing.T) {
17 defer func(p bool, m int64) { paused, minDonation = p, m }(paused, minDonation)
18 minDonation = 1000
19 c := testCampaign(9303, modeEscrow, 0)
20 if got := checkGift(c, 1500, 500); got != 1000 {
21 t.Errorf("checkGift(1500, 500) = %d, want 1000", got)
22 }
23 for name, k := range map[string]struct {
24 status string
25 paused bool
26 total, tip int64
27 err Error
28 }{
29 "a negative tip": {statusOpen, false, 1500, -1, ErrBadAmount},
30 "all tip": {statusOpen, false, 1500, 1500, ErrBadAmount},
31 "a tip above the envelope": {statusOpen, false, 1500, 1501, ErrBadAmount},
32 "nothing sent": {statusOpen, false, 0, 0, ErrBadAmount},
33 "under the minimum": {statusOpen, false, 1499, 500, ErrBadAmount},
34 "closed": {statusClosed, false, 1500, 0, ErrCampaignClosed},
35 "settled": {statusSettled, false, 1500, 0, ErrCampaignSettled},
36 "paused": {statusOpen, true, 1500, 0, ErrPaused},
37 "closed, and paused": {statusClosed, true, 1500, 0, ErrCampaignClosed},
38 "nothing left, and closed": {statusClosed, false, 500, 500, ErrBadAmount},
39 "paused, and under": {statusOpen, true, 10, 0, ErrPaused},
40 } {
41 c.state.status, paused = k.status, k.paused
42 if !panicsWith(func() { checkGift(c, k.total, k.tip) }, k.err) {
43 t.Errorf("%s: not %s", name, k.err)
44 }
45 }
46}
47
48func TestCheckRoom(t *testing.T) {
49 defer func(cap int64) { escrowCap = cap }(escrowCap)
50 escrowCap = 1000
51 c := testCampaign(9304, modeEscrow, 0)
52 c.state.raised, c.state.withdrawn = 900, 300 // tracked: 600
53 checkRoom(c, 400)
54 if !panicsWith(func() { checkRoom(c, 401) }, ErrCapReached) {
55 t.Errorf("a gift past the cap: not ErrCapReached")
56 }
57 // The cap reads the tracked balance, never the bank's: coins sent straight to
58 // the escrow don't block a gift.
59 testing.IssueCoins(escrowAddr(c.id), chain.NewCoins(chain.NewCoin("ugnot", 5000)))
60 checkRoom(c, 400)
61 // No cap, and direct mode, take any gift the books can hold.
62 escrowCap = 0
63 checkRoom(c, 1_000_000)
64 escrowCap = 1000
65 d := testCampaign(9305, modeDirect, 0)
66 checkRoom(d, 1_000_000)
67 d.state.raised = math.MaxInt64 - 5
68 checkRoom(d, 5)
69 if !panicsWith(func() { checkRoom(d, 6) }, ErrBadAmount) {
70 t.Errorf("Raised past int64: not ErrBadAmount")
71 }
72}
73
74// TestDonate gives as a user, straight from the test's body as AssertOriginCall
75// requires (plan §3.3), with the envelope and the realm's coins a real transaction
76// would bring. Its refusals are in txtar (give.txtar) and filetests.
77func TestDonate(cur realm, t *testing.T) {
78 donor := testutils.TestAddress("donor")
79 rb := banker.NewReadonlyBanker()
80 c := testCampaign(9301, modeEscrow, 0)
81 id, unstore := storeCampaign(c)
82 defer unstore()
83 // A tip recipient other than the admin, which it starts as: a tip must go to
84 // whoever SetTipRecipient named, never to the launch admin.
85 launch := tipRecipient
86 tipRecipient = testutils.TestAddress("tips")
87 defer func() { tipRecipient = launch }()
88 escrowBefore, tipsBefore, realmBefore := rb.GetCoin(escrowAddr(c.id), "ugnot"), rb.GetCoin(tipRecipient, "ugnot"), rb.GetCoin(realmAddr(), "ugnot")
89
90 // A public gift with a tip: the gift to the escrow, the tip to the tip
91 // recipient, nothing kept by the realm (INV-3).
92 testing.SetRealm(testing.NewUserRealm(donor))
93 testing.SetOriginSend(chain.NewCoins(chain.NewCoin("ugnot", 2_500_000)))
94 testing.IssueCoins(realmAddr(), chain.NewCoins(chain.NewCoin("ugnot", 2_500_000)))
95 if rid := Donate(cross(cur), id, 500_000, " Courage! ", false); rid != "9301/1" {
96 t.Fatalf("Donate returned %q, want 9301/1", rid)
97 }
98 checkBooks(t, c)
99 if got := rb.GetCoin(escrowAddr(c.id), "ugnot") - escrowBefore; got != 2_000_000 {
100 t.Errorf("the escrow got %d, want 2000000", got)
101 }
102 if got := rb.GetCoin(tipRecipient, "ugnot") - tipsBefore; got != 500_000 {
103 t.Errorf("the tip recipient got %d, want 500000", got)
104 }
105 if got := rb.GetCoin(realmAddr(), "ugnot"); got != realmBefore {
106 t.Errorf("the realm holds %d, it held %d", got, realmBefore)
107 }
108 s := c.state
109 if s.raised != 2_000_000 || s.donationCount != 1 || s.donorCount != 1 || s.lastDonationAt != now() {
110 t.Errorf("after one gift: raised %d, %d gifts, %d donors, last %d", s.raised, s.donationCount, s.donorCount, s.lastDonationAt)
111 }
112 k := c.contributions.Get(donor.String()).(*contribution)
113 if k.amount != 2_000_000 || !k.public || !byDonor.Has(donor.String()+"/"+c.id.Binary()) {
114 t.Errorf("contribution %v; by-donor entry %t", *k, byDonor.Has(donor.String()+"/"+c.id.Binary()))
115 }
116 defer byDonor.Remove(donor.String() + "/" + c.id.Binary())
117 r := c.receipts.Get(seqid.ID(1).Binary()).(*receipt)
118 want := receipt{seq: 1, donor: donor, amount: 2_000_000, tip: 500_000, message: "Courage!", at: now(), raisedAfter: 2_000_000}
119 if *r != want {
120 t.Errorf("receipt = %v, want %v", *r, want)
121 }
122
123 // The same donor again, anonymously and with no tip: one donor still, the
124 // contribution summed, nothing sent to the tip recipient (Q2).
125 tipsBefore = rb.GetCoin(tipRecipient, "ugnot")
126 testing.SetOriginSend(chain.NewCoins(chain.NewCoin("ugnot", 1_000_000)))
127 testing.IssueCoins(realmAddr(), chain.NewCoins(chain.NewCoin("ugnot", 1_000_000)))
128 Donate(cross(cur), id, 0, "", true)
129 checkBooks(t, c)
130 if s.donorCount != 1 || s.donationCount != 2 || k.amount != 3_000_000 || !k.public {
131 t.Errorf("a second gift: %d donors, %d gifts, contribution %v", s.donorCount, s.donationCount, *k)
132 }
133 if got := rb.GetCoin(tipRecipient, "ugnot"); got != tipsBefore {
134 t.Errorf("a gift with no tip sent %d to the tip recipient", got-tipsBefore)
135 }
136 if v := campaignView(c); v.Balance != 3_000_000 || v.Raised != 3_000_000 {
137 t.Errorf("balance %d, raised %d, want 3000000 each (INV-2)", v.Balance, v.Raised)
138 }
139
140 // An anonymous first gift adds no by-donor entry.
141 shy := testutils.TestAddress("shy")
142 testing.SetRealm(testing.NewUserRealm(shy))
143 testing.IssueCoins(realmAddr(), chain.NewCoins(chain.NewCoin("ugnot", 1_000_000)))
144 Donate(cross(cur), id, 0, "", true)
145 checkBooks(t, c)
146 if s.donorCount != 2 || byDonor.Has(shy.String()+"/"+c.id.Binary()) || c.contributions.Get(shy.String()).(*contribution).public {
147 t.Errorf("an anonymous donor: %d donors, by-donor entry %t", s.donorCount, byDonor.Has(shy.String()+"/"+c.id.Binary()))
148 }
149
150 // The same donor, publicly this time: in the by-donor index now, and still
151 // counted once.
152 testing.IssueCoins(realmAddr(), chain.NewCoins(chain.NewCoin("ugnot", 1_000_000)))
153 Donate(cross(cur), id, 0, "", false)
154 checkBooks(t, c)
155 defer byDonor.Remove(shy.String() + "/" + c.id.Binary())
156 if s.donorCount != 2 || !byDonor.Has(shy.String()+"/"+c.id.Binary()) || !c.contributions.Get(shy.String()).(*contribution).public {
157 t.Errorf("anonymous, then public: %d donors, by-donor entry %t", s.donorCount, byDonor.Has(shy.String()+"/"+c.id.Binary()))
158 }
159}
160
161// TestDonateDirect: a direct gift goes to the beneficiary, and the campaign's
162// balance stays 0 (INV-2). A delisted campaign takes gifts, and so does one in a
163// sunset version.
164func TestDonateDirect(cur realm, t *testing.T) {
165 defer func(s bool) { sunset = s }(sunset)
166 rb := banker.NewReadonlyBanker()
167 c := testCampaign(9302, modeDirect, 0)
168 c.state.listed = false
169 id, unstore := storeCampaign(c)
170 defer unstore()
171 sunset = true
172 before := rb.GetCoin(c.beneficiary, "ugnot")
173 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("donor")))
174 testing.SetOriginSend(chain.NewCoins(chain.NewCoin("ugnot", 1_500_000)))
175 testing.IssueCoins(realmAddr(), chain.NewCoins(chain.NewCoin("ugnot", 1_500_000)))
176 Donate(cross(cur), id, 0, "", true)
177 checkBooks(t, c)
178 if got := rb.GetCoin(c.beneficiary, "ugnot") - before; got != 1_500_000 {
179 t.Errorf("the beneficiary got %d, want 1500000", got)
180 }
181 if v := campaignView(c); v.Balance != 0 || v.Raised != 1_500_000 || v.NetRaised != 1_500_000 {
182 t.Errorf("direct: balance %d, raised %d", v.Balance, v.Raised)
183 }
184}
185
186func TestReceipts(cur realm, t *testing.T) {
187 c := testCampaign(9306, modeEscrow, 0)
188 id, unstore := storeCampaign(c)
189 defer unstore()
190 donors := []address{testutils.TestAddress("ada"), testutils.TestAddress("bo")}
191 for i := 0; i < 25; i++ {
192 testing.SetRealm(testing.NewUserRealm(donors[i%2]))
193 testing.SetOriginSend(chain.NewCoins(chain.NewCoin("ugnot", int64(1_000_000+i))))
194 testing.IssueCoins(realmAddr(), chain.NewCoins(chain.NewCoin("ugnot", int64(1_000_000+i))))
195 Donate(cross(cur), id, 0, "gift", i%2 == 1)
196 }
197 defer byDonor.Remove(donors[0].String() + "/" + c.id.Binary())
198
199 // The ring holds the last 20, newest first.
200 recent := ListRecent(id)
201 if len(recent) != recentSize || recent[0].Seq != 25 || recent[recentSize-1].Seq != 6 {
202 t.Fatalf("ListRecent: %d receipts, %d first, %d last", len(recent), recent[0].Seq, recent[len(recent)-1].Seq)
203 }
204 for i, r := range recent {
205 if want := GetReceipt(id, r.Seq); r != want || r.Seq != int64(25-i) {
206 t.Errorf("recent[%d] = %v, GetReceipt says %v", i, r, want)
207 }
208 }
209
210 // Anonymous gifts hide their donor; a hidden message is blanked; a refund shows.
211 public, anon := GetReceipt(id, 1), GetReceipt(id, 2)
212 if public.Donor != donors[0] || anon.Donor != "" || !anon.Anonymous || public.ID != id+"/1" || public.CampaignID != id {
213 t.Errorf("public %v, anonymous %v", public, anon)
214 }
215 c.receipts.Get(seqid.ID(1).Binary()).(*receipt).messageHidden = true
216 c.contributions.Get(donors[0].String()).(*contribution).refunded = 7
217 if r := GetReceipt(id, 1); r.Message != "" || !r.MessageHidden || r.DonorRefunded != 7 || r.Amount != 1_000_000 {
218 t.Errorf("hidden and refunded: %v", r)
219 }
220 for _, seq := range []int64{0, -1, 26, math.MaxInt64} {
221 if !panicsWith(func() { GetReceipt(id, seq) }, ErrNotFound) {
222 t.Errorf("GetReceipt(%d): not ErrNotFound", seq)
223 }
224 }
225 if !panicsWith(func() { ListRecent("042") }, ErrNotFound) {
226 t.Errorf("ListRecent(\"042\"): not ErrNotFound")
227 }
228 empty, unstoreEmpty := storeCampaign(testCampaign(9307, modeEscrow, 0))
229 defer unstoreEmpty()
230 if got := ListRecent(empty); len(got) != 0 {
231 t.Errorf("a campaign without gifts lists %d", len(got))
232 }
233}
234
235// TestSweepStrays: what sits at the realm goes to the tip recipient; then there is
236// nothing to sweep, and nothing is sent (Q3).
237func TestSweepStrays(cur realm, t *testing.T) {
238 rb := banker.NewReadonlyBanker()
239 SweepStrays(cross(cur)) // whatever earlier tests left
240 launch := tipRecipient
241 tipRecipient = testutils.TestAddress("tips")
242 defer func() { tipRecipient = launch }()
243 testing.IssueCoins(realmAddr(), chain.NewCoins(chain.NewCoin("ugnot", 123)))
244 before := rb.GetCoin(tipRecipient, "ugnot")
245 if got := SweepStrays(cross(cur)); got != 123 {
246 t.Errorf("SweepStrays = %d, want 123", got)
247 }
248 if got := rb.GetCoin(tipRecipient, "ugnot") - before; got != 123 || rb.GetCoin(realmAddr(), "ugnot") != 0 {
249 t.Errorf("the tip recipient got %d; the realm holds %d", got, rb.GetCoin(realmAddr(), "ugnot"))
250 }
251 if got := SweepStrays(cross(cur)); got != 0 {
252 t.Errorf("SweepStrays with nothing there = %d", got)
253 }
254}