grant_test.gno
9.64 Kb · 244 lines
1package grant
2
3import (
4 "chain"
5 "chain/banker"
6 "testing"
7
8 "gno.land/p/moul/grants/v0"
9 "gno.land/p/nt/uassert/v0"
10 "gno.land/p/nt/urequire/v0"
11)
12
13const (
14 zoe = address("g12cs4cehujpffpjpywmkqj43m6u5ya53nj69sjz") // an applicant
15 kim = address("g1jvh5ukk07dvd57fxefcp5aa29xaydxmxs7myyp") // a second one
16)
17
18func balanceOf(a address) int64 {
19 return banker.NewReadonlyBanker().GetCoins(a).AmountOf(Denom)
20}
21
22func issue(n int64) { testing.IssueCoins(Address(), chain.NewCoins(chain.NewCoin(Denom, n))) }
23
24func TestFreshBoardIsOwnedAndEmpty(t *testing.T) {
25 reset()
26 uassert.True(t, Owner.IsValid(), "the owner is a real address")
27 uassert.True(t, zoe.IsValid())
28 uassert.True(t, kim.IsValid())
29 uassert.True(t, Address().IsValid(), "the treasury address")
30
31 urequire.Equal(t, 1, len(Members()))
32 uassert.Equal(t, Owner.String(), Members()[0].String())
33 uassert.Equal(t, 0, Requests(), "nothing seeded, this board is real")
34 uassert.Equal(t, int64(0), Committed())
35 uassert.Equal(t, int64(0), Raised())
36 uassert.Equal(t, int64(0), Disbursed())
37}
38
39// The whole path with real coins, on a one-member board: the owner is the
40// majority, so one ballot carries and one review pays.
41func TestApplyApproveProveGetPaid(cur realm, t *testing.T) {
42 reset()
43 urequire.Equal(t, int64(0), Balance(), "start from an empty treasury")
44
45 // Someone funds the board, by name.
46 issue(500)
47 testing.SetOriginSend(chain.NewCoins(chain.NewCoin(Denom, 500)))
48 testing.SetRealm(testing.NewUserRealm(kim))
49 Fund(cross(cur))
50 uassert.Equal(t, int64(500), Balance())
51 uassert.Equal(t, int64(500), Raised())
52 uassert.Equal(t, kim.String(), program.Donations[0].From.String())
53
54 // An outsider applies. Anyone may.
55 testing.SetRealm(testing.NewUserRealm(zoe))
56 id := Apply(cross(cur), "Ship the indexer", "two tranches", "alpha:100,beta:400")
57 uassert.Equal(t, 1, id)
58 uassert.Equal(t, int64(0), Committed(), "a pending request promises nothing")
59
60 // A non-member cannot vote, not even the applicant.
61 testing.SetRealm(testing.NewUserRealm(zoe))
62 uassert.AbortsContains(t, cur, grants.ErrNotMember.Error(), func() {
63 Vote(cross(cur), id, true, "me again")
64 })
65
66 testing.SetRealm(testing.NewUserRealm(Owner))
67 Vote(cross(cur), id, true, "worth it")
68 uassert.Equal(t, int64(500), Committed(), "approval promises the full ask")
69 uassert.Equal(t, int64(0), Available())
70
71 // Milestone 1: proof, then the one review that carries also pays.
72 testing.SetRealm(testing.NewUserRealm(kim))
73 uassert.AbortsContains(t, cur, grants.ErrNotApplicant.Error(), func() {
74 SubmitProof(cross(cur), id, 0, "url", "https://example.com/pr/1", "not mine to prove")
75 })
76 testing.SetRealm(testing.NewUserRealm(zoe))
77 SubmitProof(cross(cur), id, 0, "url", "https://example.com/pr/1", "alpha is live")
78 testing.SetRealm(testing.NewUserRealm(Owner))
79 Review(cross(cur), id, 0, true, "confirmed")
80
81 uassert.Equal(t, int64(100), balanceOf(zoe), "the tranche landed")
82 uassert.Equal(t, int64(400), Balance())
83 uassert.Equal(t, int64(100), Disbursed())
84 urequire.Equal(t, 1, len(program.Payments))
85 uassert.Equal(t, int64(100), program.Payments[0].Amount)
86
87 // Out of order is refused.
88 testing.SetRealm(testing.NewUserRealm(zoe))
89 uassert.AbortsContains(t, cur, grants.ErrOutOfOrder.Error(), func() {
90 SubmitProof(cross(cur), id, 0, "url", "https://example.com/pr/1", "again")
91 })
92
93 // Milestone 2: a proof the owner refuses, then one they accept.
94 testing.SetRealm(testing.NewUserRealm(zoe))
95 SubmitProof(cross(cur), id, 1, "text", "trust me", "")
96 testing.SetRealm(testing.NewUserRealm(Owner))
97 Review(cross(cur), id, 1, false, "no evidence")
98 uassert.Equal(t, int64(100), balanceOf(zoe), "a refused proof pays nothing")
99
100 testing.SetRealm(testing.NewUserRealm(zoe))
101 SubmitProof(cross(cur), id, 1, "hash", "sha256:deadbeef", "beta, signed release")
102 testing.SetRealm(testing.NewUserRealm(Owner))
103 Review(cross(cur), id, 1, true, "verified")
104
105 uassert.Equal(t, int64(500), balanceOf(zoe), "paid in full")
106 uassert.Equal(t, int64(0), Balance(), "the treasury is back to empty")
107 uassert.Equal(t, int64(0), Committed())
108 uassert.Equal(t, int64(500), Disbursed())
109 uassert.Equal(t, 2, len(program.Payments))
110
111 // Both attempts on milestone 2 are on the record, refusal included.
112 m2 := program.Board.Get(id).Milestones[1]
113 urequire.Equal(t, 2, len(m2.Attempts))
114 uassert.Equal(t, "refused", m2.Attempts[0].Outcome.String())
115 uassert.Equal(t, "no evidence", m2.Attempts[0].Reviews[0].Reason)
116 uassert.Equal(t, "accepted", m2.Attempts[1].Outcome.String())
117}
118
119// The verdict that would release a tranche the treasury cannot pay is refused
120// outright and leaves no trace.
121func TestReviewRefusesToReleaseWhatItCannotPay(cur realm, t *testing.T) {
122 reset()
123 urequire.Equal(t, int64(0), Balance(), "no coins anywhere")
124
125 testing.SetRealm(testing.NewUserRealm(zoe))
126 id := Apply(cross(cur), "Ship the indexer", "", "alpha:100")
127 testing.SetRealm(testing.NewUserRealm(Owner))
128 Vote(cross(cur), id, true, "worth it")
129 testing.SetRealm(testing.NewUserRealm(zoe))
130 SubmitProof(cross(cur), id, 0, "url", "https://example.com/pr/1", "")
131
132 testing.SetRealm(testing.NewUserRealm(Owner))
133 uassert.AbortsContains(t, cur, grants.ErrUnderfunded.Error(), func() {
134 Review(cross(cur), id, 0, true, "confirmed")
135 })
136 m := program.Board.Get(id).Milestones[0]
137 uassert.Equal(t, 0, len(m.Current().Reviews), "the ballot was not recorded")
138 uassert.False(t, m.Released)
139 uassert.Equal(t, 0, len(program.Payments))
140 uassert.Equal(t, int64(-100), Available(), "and the shortfall is visible")
141}
142
143func TestRetractAndBadInput(cur realm, t *testing.T) {
144 reset()
145 testing.SetRealm(testing.NewUserRealm(zoe))
146 uassert.AbortsContains(t, cur, grants.ErrBadSpec.Error(), func() {
147 Apply(cross(cur), "Bad", "", "no amount here")
148 })
149 uassert.Equal(t, 0, Requests(), "nothing was filed")
150
151 testing.SetRealm(testing.NewUserRealm(zoe))
152 id := Apply(cross(cur), "Never mind", "", "thing:1")
153 testing.SetRealm(testing.NewUserRealm(kim))
154 uassert.AbortsContains(t, cur, grants.ErrNotApplicant.Error(), func() {
155 Retract(cross(cur), id)
156 })
157 testing.SetRealm(testing.NewUserRealm(zoe))
158 Retract(cross(cur), id)
159 uassert.Equal(t, "withdrawn", program.Board.Get(id).Status.String())
160
161 testing.SetOriginSend(chain.NewCoins())
162 testing.SetRealm(testing.NewUserRealm(kim))
163 uassert.AbortsContains(t, cur, grants.ErrNothingSent.Error(), func() { Fund(cross(cur)) })
164 uassert.Equal(t, 0, len(program.Donations))
165}
166
167// The owner can hand out a seat, and once there are two members the bar moves
168// to two. That is the path from this personal board to a real one.
169func TestProposeMemberGrowsTheBoardAndRaisesTheBar(cur realm, t *testing.T) {
170 reset()
171 testing.SetRealm(testing.NewUserRealm(zoe))
172 uassert.AbortsContains(t, cur, grants.ErrNotMember.Error(), func() {
173 ProposeMember(cross(cur), zoe.String(), true, "let me in")
174 })
175
176 testing.SetRealm(testing.NewUserRealm(Owner))
177 id := ProposeMember(cross(cur), kim.String(), true, "reviews everything anyway")
178 testing.SetRealm(testing.NewUserRealm(Owner))
179 Vote(cross(cur), id, true, "proposed it")
180 uassert.Equal(t, "completed", program.Board.Get(id).Status.String())
181 urequire.Equal(t, 2, len(Members()))
182
183 testing.SetRealm(testing.NewUserRealm(zoe))
184 next := Apply(cross(cur), "Ship the indexer", "", "alpha:100")
185 r := program.Board.Get(next)
186 uassert.Equal(t, 2, program.Board.Eligible(r))
187 uassert.Equal(t, 2, program.Board.Majority(r), "two members, two signatures")
188
189 testing.SetRealm(testing.NewUserRealm(Owner))
190 Vote(cross(cur), next, true, "worth it")
191 uassert.Equal(t, "pending", program.Board.Get(next).Status.String(), "the owner is no longer a majority")
192 testing.SetRealm(testing.NewUserRealm(kim))
193 Vote(cross(cur), next, true, "agreed")
194 uassert.Equal(t, "approved", program.Board.Get(next).Status.String())
195}
196
197func TestRenderUnknownPaths(t *testing.T) {
198 reset()
199 uassert.True(t, len(Render("")) > 0)
200 uassert.Equal(t, "# moul's grant board\n\nThere is no request 99.\n", Render("request/99"))
201 uassert.True(t, len(Render("ledger")) > 0)
202 uassert.True(t, len(Render("nope")) > 0)
203}
204
205// The case this board exists for: an account with nothing in it cannot pay the
206// gas to ask for its first coins, so someone else asks for it. The treasury
207// ends where it started, at zero, which is what the pinned pages render.
208func TestApplyForPaysTheBeneficiaryNotTheFiler(cur realm, t *testing.T) {
209 reset()
210 urequire.Equal(t, int64(0), Balance())
211 before := balanceOf(kim)
212
213 issue(300)
214 testing.SetOriginSend(chain.NewCoins(chain.NewCoin(Denom, 300)))
215 testing.SetRealm(testing.NewUserRealm(address(Owner)))
216 Fund(cross(cur))
217
218 // zoe files for kim, whose account is empty.
219 testing.SetRealm(testing.NewUserRealm(zoe))
220 id := ApplyFor(cross(cur), kim.String(), "kim's account is empty, she cannot pay the gas",
221 "Fund a first key", "so she can transact at all", "first transfer:300")
222 r := program.Board.Get(id)
223 urequire.True(t, r.OnBehalf())
224 uassert.Equal(t, kim.String(), r.Payee().String())
225
226 // The owner carries it alone, and either party may prove it. Here the
227 // filer does, because the payee still cannot send a transaction.
228 testing.SetRealm(testing.NewUserRealm(address(Owner)))
229 Vote(cross(cur), id, true, "I know kim.")
230 testing.SetRealm(testing.NewUserRealm(zoe))
231 SubmitProof(cross(cur), id, 0, "text", "she has a key and no coins", "")
232 testing.SetRealm(testing.NewUserRealm(address(Owner)))
233 Review(cross(cur), id, 0, true, "Sent.")
234
235 uassert.Equal(t, before+300, balanceOf(kim), "the payee got the coins")
236 uassert.Equal(t, int64(0), Balance(), "and the treasury is back to empty")
237 uassert.Equal(t, kim.String(), program.Payments[0].To.String())
238
239 // A reason is not optional when the payee is someone else.
240 testing.SetRealm(testing.NewUserRealm(zoe))
241 uassert.AbortsContains(t, cur, "needs a reason", func() {
242 ApplyFor(cross(cur), kim.String(), "", "no reason given", "", "x:1")
243 })
244}