Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}