program_test.gno
8.67 Kb · 209 lines
1package grants
2
3import (
4 "testing"
5
6 "gno.land/p/nt/uassert/v0"
7 "gno.land/p/nt/urequire/v0"
8)
9
10// prog is the fixture: three members, and dave applying from outside, so a
11// majority of the eligible set is two.
12func prog() *Program { return NewProgram("ugnot", alice, bob, carol) }
13
14func TestParseMilestones(t *testing.T) {
15 cases := []struct {
16 name string
17 spec string
18 titles []string
19 amounts []int64
20 }{
21 {"one", "ship:400", []string{"ship"}, []int64{400}},
22 {"two", "design:100,ship:400", []string{"design", "ship"}, []int64{100, 400}},
23 {"spaces", " design : 100 , ship : 400 ", []string{"design", "ship"}, []int64{100, 400}},
24 {"trailing comma", "ship:400,", []string{"ship"}, []int64{400}},
25 {"colon in the title", "port gno:land tooling:250", []string{"port gno:land tooling"}, []int64{250}},
26 }
27 for _, tc := range cases {
28 ms, err := ParseMilestones(tc.spec)
29 if !uassert.NoError(t, err, tc.name) {
30 continue
31 }
32 urequire.Equal(t, len(tc.titles), len(ms), tc.name)
33 for i := range ms {
34 uassert.Equal(t, tc.titles[i], ms[i].Title, tc.name)
35 uassert.Equal(t, tc.amounts[i], ms[i].Amount, tc.name)
36 }
37 }
38
39 for _, bad := range []string{"", " ", ",,", "ship", "ship:", "ship:many", "ship:1:2:x"} {
40 _, err := ParseMilestones(bad)
41 uassert.ErrorIs(t, err, ErrBadSpec, "should be rejected: "+bad)
42 }
43}
44
45func TestFundRecordsTheDonor(t *testing.T) {
46 p := prog()
47 uassert.ErrorIs(t, p.Fund(dave, 0, 10), ErrNothingSent)
48 uassert.ErrorIs(t, p.Fund(dave, -5, 10), ErrNothingSent)
49 uassert.ErrorIs(t, p.Fund(address(""), 5, 10), ErrBadAddress)
50 uassert.Equal(t, 0, len(p.Donations), "none of those were recorded")
51
52 urequire.NoError(t, p.Fund(dave, 500, 10))
53 urequire.NoError(t, p.Fund(mallory, 250, 11))
54 urequire.Equal(t, 2, len(p.Donations))
55 uassert.Equal(t, dave.String(), p.Donations[0].From.String())
56 uassert.Equal(t, int64(500), p.Donations[0].Amount)
57 uassert.Equal(t, int64(10), p.Donations[0].Height)
58 uassert.Equal(t, int64(750), p.Raised())
59}
60
61func TestApplyGoesThroughTheSpec(t *testing.T) {
62 p := prog()
63 r, err := p.Apply(dave, "Ship it", "two tranches", "alpha:100,beta:400", 10)
64 urequire.NoError(t, err)
65 uassert.Equal(t, int64(500), r.Total())
66 uassert.Equal(t, 2, len(r.Milestones))
67 uassert.Equal(t, "beta", r.Milestones[1].Title)
68
69 _, err = p.Apply(dave, "Bad", "", "no amount here", 10)
70 uassert.ErrorIs(t, err, ErrBadSpec)
71 uassert.Equal(t, 1, p.Board.Size(), "the bad one was never filed")
72}
73
74// Available is allowed to go negative. That is the whole reason Review takes
75// a balance instead of trusting the board's own arithmetic.
76func TestAvailableGoesNegativeWhenTheBoardOverPromises(t *testing.T) {
77 p := prog()
78 r, err := p.Apply(dave, "Ship it", "", "alpha:100,beta:400", 10)
79 urequire.NoError(t, err)
80 uassert.Equal(t, int64(0), p.Committed(), "pending promises nothing")
81 uassert.Equal(t, int64(0), p.Available(0))
82
83 urequire.NoError(t, errOf(p.Board.Vote(alice, r.ID, true, "", 11)))
84 urequire.NoError(t, errOf(p.Board.Vote(bob, r.ID, true, "", 12)))
85 uassert.Equal(t, int64(500), p.Committed())
86 uassert.Equal(t, int64(-500), p.Available(0), "approved with an empty treasury")
87 uassert.Equal(t, int64(100), p.Available(600))
88}
89
90func TestReviewRefusesTheVerdictItCannotPay(t *testing.T) {
91 p := prog()
92 r, err := p.Apply(dave, "Ship it", "", "alpha:100,beta:400", 10)
93 urequire.NoError(t, err)
94 urequire.NoError(t, errOf(p.Board.Vote(alice, r.ID, true, "", 11)))
95 urequire.NoError(t, errOf(p.Board.Vote(bob, r.ID, true, "", 12)))
96 urequire.NoError(t, p.Board.SubmitProof(dave, r.ID, 0, Proof{Kind: "url", Ref: "https://x/1", Height: 20}))
97
98 // Not the deciding ballot, so the balance is irrelevant: it records.
99 out, pay, err := p.Review(alice, r.ID, 0, true, "looks done", 21, 0)
100 urequire.NoError(t, err)
101 uassert.Equal(t, "under review", out.String())
102 uassert.True(t, pay == nil, "nothing to pay yet")
103
104 // The deciding ballot against an empty treasury changes nothing at all.
105 out, pay, err = p.Review(bob, r.ID, 0, true, "agreed", 22, 99)
106 uassert.ErrorIs(t, err, ErrUnderfunded)
107 uassert.True(t, pay == nil)
108 uassert.Equal(t, 1, len(r.Milestones[0].Current().Reviews), "the ballot was not recorded")
109 uassert.False(t, r.Milestones[0].Released)
110 uassert.Equal(t, 0, len(p.Payments))
111
112 // A refusal from the same member costs the treasury nothing, so it lands,
113 // even against a balance of zero. It does not settle anything on its own:
114 // alice already accepted, so this is one all against a bar of two.
115 out, pay, err = p.Review(bob, r.ID, 0, false, "not convinced", 23, 0)
116 urequire.NoError(t, err)
117 uassert.Equal(t, "under review", out.String())
118 uassert.True(t, pay == nil)
119 uassert.Equal(t, 2, len(r.Milestones[0].Current().Reviews), "this one WAS recorded")
120
121 // The second refusal reaches the bar and closes the attempt, still with
122 // an empty treasury, because refusing costs nothing.
123 out, pay, err = p.Review(carol, r.ID, 0, false, "same", 24, 0)
124 urequire.NoError(t, err)
125 uassert.Equal(t, "refused", out.String())
126 uassert.True(t, pay == nil)
127 uassert.False(t, r.Milestones[0].Released)
128 uassert.Equal(t, "approved", r.Status.String(), "a refused proof does not kill the grant")
129}
130
131func TestReviewHandsBackThePaymentToExecute(t *testing.T) {
132 p := prog()
133 r, err := p.Apply(dave, "Ship it", "", "alpha:100,beta:400", 10)
134 urequire.NoError(t, err)
135 urequire.NoError(t, errOf(p.Board.Vote(alice, r.ID, true, "", 11)))
136 urequire.NoError(t, errOf(p.Board.Vote(bob, r.ID, true, "", 12)))
137
138 urequire.NoError(t, p.Board.SubmitProof(dave, r.ID, 0, Proof{Kind: "url", Ref: "https://x/1", Height: 20}))
139 urequire.NoError(t, errOf3(p.Review(alice, r.ID, 0, true, "", 21, 500)))
140 out, pay, err := p.Review(carol, r.ID, 0, true, "", 22, 500)
141 urequire.NoError(t, err)
142 uassert.Equal(t, "accepted", out.String())
143 urequire.True(t, pay != nil, "the caller is handed a payment to execute")
144 uassert.Equal(t, dave.String(), pay.To.String())
145 uassert.Equal(t, int64(100), pay.Amount)
146 uassert.Equal(t, r.ID, pay.Request)
147 uassert.Equal(t, 0, pay.Milestone)
148 uassert.Equal(t, int64(22), pay.Height)
149
150 urequire.Equal(t, 1, len(p.Payments), "and it is on the ledger")
151 uassert.Equal(t, int64(100), p.Disbursed())
152 uassert.Equal(t, int64(400), p.Committed())
153 uassert.Equal(t, int64(300), p.Available(700))
154
155 // Second tranche completes it.
156 urequire.NoError(t, p.Board.SubmitProof(dave, r.ID, 1, Proof{Kind: "hash", Ref: "deadbeef", Height: 30}))
157 urequire.NoError(t, errOf3(p.Review(alice, r.ID, 1, true, "", 31, 400)))
158 out, pay, err = p.Review(bob, r.ID, 1, true, "", 32, 400)
159 urequire.NoError(t, err)
160 uassert.Equal(t, "accepted", out.String())
161 urequire.True(t, pay != nil)
162 uassert.Equal(t, int64(400), pay.Amount)
163 uassert.Equal(t, "completed", r.Status.String())
164 uassert.Equal(t, int64(500), p.Disbursed())
165 uassert.Equal(t, int64(0), p.Committed())
166 uassert.Equal(t, 2, len(p.Payments))
167}
168
169// A one-member program is a legitimate configuration (it is what a personal
170// board is), and its majority is one. It still cannot self-grant.
171func TestOneMemberBoard(t *testing.T) {
172 p := NewProgram("ugnot", alice)
173 r, err := p.Apply(dave, "Ship it", "", "alpha:100", 10)
174 urequire.NoError(t, err)
175 uassert.Equal(t, 1, p.Board.Eligible(r))
176 uassert.Equal(t, 1, p.Board.Majority(r))
177 st, err := p.Board.Vote(alice, r.ID, true, "sure", 11)
178 urequire.NoError(t, err)
179 uassert.Equal(t, "approved", st.String(), "one signature carries a one-member board")
180
181 own, err := p.Apply(alice, "Pay myself", "", "alpha:100", 12)
182 urequire.NoError(t, err)
183 uassert.Equal(t, 0, p.Board.Eligible(own), "the applicant is the whole board")
184 uassert.Equal(t, 1, p.Board.Majority(own), "so the bar is unreachable, not zero")
185 uassert.ErrorIs(t, errOf(p.Board.Vote(alice, own.ID, true, "", 13)), ErrConflict)
186 uassert.Equal(t, "pending", own.Status.String(), "a sole member cannot self-grant")
187}
188
189func errOf3(_ Outcome, _ *Payment, err error) error { return err }
190
191func TestTheTrancheGoesToTheBeneficiary(t *testing.T) {
192 p := NewProgram("ugnot", alice, bob)
193 r, err := p.ApplyFor(dave, mallory, "mallory cannot pay the gas to ask", "t", "", "one:700", 10)
194 urequire.NoError(t, err)
195 p.Board.Vote(alice, r.ID, true, "", 11)
196 p.Board.Vote(bob, r.ID, true, "", 12)
197 urequire.NoError(t, p.Board.SubmitProof(dave, r.ID, 0, Proof{Kind: "text", Ref: "done", Height: 13}))
198
199 _, pay, err := p.Review(alice, r.ID, 0, true, "", 14, 700)
200 urequire.NoError(t, err)
201 uassert.True(t, pay == nil, "neither party sits on the board, so both members must accept")
202
203 _, pay, err = p.Review(bob, r.ID, 0, true, "", 15, 700)
204 urequire.NoError(t, err)
205 urequire.True(t, pay != nil, "the second accept releases it")
206 uassert.Equal(t, mallory.String(), pay.To.String(), "the money goes to the payee, not the filer")
207 uassert.Equal(t, int64(700), pay.Amount)
208 uassert.Equal(t, mallory.String(), p.Payments[0].To.String(), "and so does the ledger row")
209}