grants_test.gno
19.57 Kb · 527 lines
1package grants
2
3import (
4 "testing"
5
6 "gno.land/p/nt/testutils/v0"
7 "gno.land/p/nt/uassert/v0"
8 "gno.land/p/nt/urequire/v0"
9)
10
11var (
12 alice = testutils.TestAddress("alice") // member
13 bob = testutils.TestAddress("bob") // member
14 carol = testutils.TestAddress("carol") // member
15 dave = testutils.TestAddress("dave") // applicant, not a member
16 mallory = testutils.TestAddress("mallory") // nobody
17)
18
19// board3 is the fixture most tests want: three members, so a majority of the
20// eligible set is two whenever the applicant is an outsider.
21func board3() *Board { return New(alice, bob, carol) }
22
23// submitOne files a two-milestone grant from dave, the outsider applicant.
24func submitOne(t *testing.T, b *Board) *Request {
25 t.Helper()
26 r, err := b.Submit(dave, "Port the thing", "with tests",
27 []*Milestone{NewMilestone("design", 100), NewMilestone("ship", 400)}, 10)
28 urequire.NoError(t, err)
29 return r
30}
31
32func TestNewSeedsFoundersAtHeightZero(t *testing.T) {
33 b := board3()
34 uassert.Equal(t, 3, b.MemberCount())
35 for _, m := range []address{alice, bob, carol} {
36 uassert.True(t, b.IsMember(m), m.String()+" should be a founder")
37 uassert.Equal(t, int64(0), b.JoinedAt(m))
38 }
39 uassert.False(t, b.IsMember(dave))
40 uassert.Equal(t, int64(-1), b.JoinedAt(dave))
41
42 // Members is address-ordered, and the order is what avl gives, not
43 // insertion order. Only the stability matters, so compare two calls.
44 first, second := b.Members(), b.Members()
45 urequire.Equal(t, 3, len(first))
46 for i := range first {
47 uassert.Equal(t, first[i].String(), second[i].String())
48 }
49}
50
51func TestNewSkipsInvalidFounders(t *testing.T) {
52 b := New(alice, address(""), alice)
53 uassert.Equal(t, 1, b.MemberCount(), "empty address dropped, duplicate collapsed")
54}
55
56func TestSubmitValidation(t *testing.T) {
57 long := ""
58 for i := 0; i < MaxTitle+1; i++ {
59 long += "x"
60 }
61 body := ""
62 for i := 0; i < MaxBody+1; i++ {
63 body += "y"
64 }
65 many := make([]*Milestone, MaxMilestones+1)
66 for i := range many {
67 many[i] = NewMilestone("m", 1)
68 }
69
70 cases := []struct {
71 name string
72 title string
73 body string
74 ms []*Milestone
75 want error
76 }{
77 {"ok", "t", "b", []*Milestone{NewMilestone("m", 1)}, nil},
78 {"empty title", "", "b", []*Milestone{NewMilestone("m", 1)}, ErrBadTitle},
79 {"long title", long, "b", []*Milestone{NewMilestone("m", 1)}, ErrBadTitle},
80 {"long body", "t", body, []*Milestone{NewMilestone("m", 1)}, ErrBadBody},
81 {"no milestone", "t", "b", nil, ErrBadMilestones},
82 {"too many milestones", "t", "b", many, ErrBadMilestones},
83 {"zero amount", "t", "b", []*Milestone{NewMilestone("m", 0)}, ErrBadAmount},
84 {"negative amount", "t", "b", []*Milestone{NewMilestone("m", -1)}, ErrBadAmount},
85 {"untitled milestone", "t", "b", []*Milestone{NewMilestone("", 1)}, ErrBadTitle},
86 }
87 for _, tc := range cases {
88 _, err := board3().Submit(dave, tc.title, tc.body, tc.ms, 1)
89 if tc.want == nil {
90 uassert.NoError(t, err, tc.name)
91 continue
92 }
93 uassert.ErrorIs(t, err, tc.want, tc.name)
94 }
95
96 _, err := board3().Submit(address(""), "t", "b", []*Milestone{NewMilestone("m", 1)}, 1)
97 uassert.ErrorIs(t, err, ErrBadAddress, "invalid applicant")
98}
99
100func TestRequestArithmetic(t *testing.T) {
101 b := board3()
102 r := submitOne(t, b)
103 uassert.Equal(t, int64(500), r.Total())
104 uassert.Equal(t, int64(0), r.Paid())
105 uassert.Equal(t, int64(0), r.Outstanding(), "a pending grant commits nothing")
106 uassert.Equal(t, 0, r.Next())
107 uassert.Equal(t, "pending", r.Status.String())
108 uassert.Equal(t, "grant", r.Kind.String())
109 uassert.True(t, r.Excludes(dave), "the applicant never votes on their own grant")
110 uassert.False(t, r.Excludes(alice))
111 uassert.Equal(t, dave.String(), r.Payee().String(), "a self request pays its applicant")
112 uassert.False(t, r.OnBehalf())
113 uassert.Equal(t, 1, r.ID)
114 uassert.Equal(t, r.ID, b.Get(1).ID)
115 uassert.True(t, b.Get(42) == nil, "unknown id")
116}
117
118func TestMajorityCountsOnlyEligibleMembers(t *testing.T) {
119 b := board3()
120
121 outsider := submitOne(t, b)
122 uassert.Equal(t, 3, b.Eligible(outsider))
123 uassert.Equal(t, 2, b.Majority(outsider))
124
125 // A member applying for their own grant shrinks the eligible set, and
126 // with it the bar: two of the remaining two must agree.
127 insider, err := b.Submit(alice, "self", "", []*Milestone{NewMilestone("m", 1)}, 1)
128 urequire.NoError(t, err)
129 uassert.Equal(t, 2, b.Eligible(insider))
130 uassert.Equal(t, 2, b.Majority(insider))
131}
132
133func TestApprovalNeedsAMajority(t *testing.T) {
134 b := board3()
135 r := submitOne(t, b)
136
137 st, err := b.Vote(alice, r.ID, true, "good", 11)
138 urequire.NoError(t, err)
139 uassert.Equal(t, "pending", st.String(), "one of three is not a majority")
140
141 st, err = b.Vote(bob, r.ID, true, "agreed", 12)
142 urequire.NoError(t, err)
143 uassert.Equal(t, "approved", st.String())
144 uassert.Equal(t, int64(12), r.DecidedAt)
145 uassert.Equal(t, int64(500), r.Outstanding(), "approval commits the full ask")
146 uassert.Equal(t, int64(500), b.Committed())
147
148 yes, no := r.Tally()
149 uassert.Equal(t, 2, yes)
150 uassert.Equal(t, 0, no)
151 uassert.Equal(t, "good", r.BallotOf(alice).Reason)
152 uassert.True(t, r.BallotOf(carol) == nil, "carol never voted")
153}
154
155func TestRejectionNeedsAMajorityToo(t *testing.T) {
156 b := board3()
157 r := submitOne(t, b)
158 urequire.NoError(t, errOf(b.Vote(alice, r.ID, false, "out of scope", 11)))
159 st, err := b.Vote(bob, r.ID, false, "same", 12)
160 urequire.NoError(t, err)
161 uassert.Equal(t, "rejected", st.String())
162 uassert.Equal(t, int64(0), b.Committed(), "a rejected grant commits nothing")
163}
164
165func TestVoteRefusals(t *testing.T) {
166 b := board3()
167 r := submitOne(t, b)
168 self, err := b.Submit(alice, "self", "", []*Milestone{NewMilestone("m", 1)}, 1)
169 urequire.NoError(t, err)
170
171 _, err = b.Vote(alice, 999, true, "", 1)
172 uassert.ErrorIs(t, err, ErrNoRequest)
173
174 _, err = b.Vote(mallory, r.ID, true, "", 1)
175 uassert.ErrorIs(t, err, ErrNotMember)
176
177 _, err = b.Vote(alice, self.ID, true, "vote for myself", 1)
178 uassert.ErrorIs(t, err, ErrConflict)
179
180 long := ""
181 for i := 0; i < MaxReason+1; i++ {
182 long += "z"
183 }
184 _, err = b.Vote(alice, r.ID, true, long, 1)
185 uassert.ErrorIs(t, err, ErrBadReason)
186
187 urequire.NoError(t, errOf(b.Vote(alice, r.ID, true, "yes", 1)))
188 _, err = b.Vote(alice, r.ID, false, "changed my mind", 2)
189 uassert.ErrorIs(t, err, ErrAlreadyVoted, "a ballot is permanent")
190
191 urequire.NoError(t, errOf(b.Vote(bob, r.ID, true, "yes", 3)))
192 _, err = b.Vote(carol, r.ID, true, "too late", 4)
193 uassert.ErrorIs(t, err, ErrNotPending, "decided requests take no more ballots")
194}
195
196func TestDecidesPreviewsWithoutCasting(t *testing.T) {
197 b := board3()
198 r := submitOne(t, b)
199
200 settles, to := b.Decides(r.ID, alice, true)
201 uassert.False(t, settles, "the first of three does not settle it")
202 uassert.Equal(t, 0, len(r.Votes), "previewing casts nothing")
203
204 urequire.NoError(t, errOf(b.Vote(alice, r.ID, true, "", 11)))
205 settles, to = b.Decides(r.ID, bob, true)
206 uassert.True(t, settles)
207 uassert.Equal(t, "approved", to.String())
208
209 settles, _ = b.Decides(r.ID, bob, false)
210 uassert.False(t, settles, "a no here only makes it one-all")
211
212 settles, _ = b.Decides(r.ID, mallory, true)
213 uassert.False(t, settles, "a non-member decides nothing")
214 settles, _ = b.Decides(r.ID, alice, true)
215 uassert.False(t, settles, "a member who already voted decides nothing")
216 settles, _ = b.Decides(999, bob, true)
217 uassert.False(t, settles, "an unknown request decides nothing")
218}
219
220func TestWithdraw(t *testing.T) {
221 b := board3()
222 r := submitOne(t, b)
223
224 uassert.ErrorIs(t, b.Withdraw(mallory, r.ID, 5), ErrNotApplicant)
225 uassert.ErrorIs(t, b.Withdraw(dave, 999, 5), ErrNoRequest)
226 urequire.NoError(t, b.Withdraw(dave, r.ID, 5))
227 uassert.Equal(t, "withdrawn", r.Status.String())
228 uassert.ErrorIs(t, b.Withdraw(dave, r.ID, 6), ErrNotPending)
229}
230
231func TestProofRefusals(t *testing.T) {
232 b := board3()
233 r := submitOne(t, b)
234 good := Proof{Kind: "url", Ref: "https://example.com/pr/1", Height: 20}
235
236 uassert.ErrorIs(t, b.SubmitProof(dave, r.ID, 0, good), ErrNotApproved, "not approved yet")
237
238 urequire.NoError(t, errOf(b.Vote(alice, r.ID, true, "", 11)))
239 urequire.NoError(t, errOf(b.Vote(bob, r.ID, true, "", 12)))
240
241 uassert.ErrorIs(t, b.SubmitProof(mallory, r.ID, 0, good), ErrNotApplicant)
242 uassert.ErrorIs(t, b.SubmitProof(dave, r.ID, 7, good), ErrNoMilestone)
243 uassert.ErrorIs(t, b.SubmitProof(dave, r.ID, 1, good), ErrOutOfOrder, "milestone 1 before 0")
244
245 bad := []Proof{
246 {Kind: "screenshot", Ref: "x"},
247 {Kind: "url", Ref: ""},
248 {Kind: "text", Ref: "ok", Note: longString(MaxNote + 1)},
249 {Kind: "url", Ref: longString(MaxRef + 1)},
250 }
251 for _, p := range bad {
252 uassert.ErrorIs(t, b.SubmitProof(dave, r.ID, 0, p), ErrBadProof, p.Kind)
253 }
254
255 urequire.NoError(t, b.SubmitProof(dave, r.ID, 0, good))
256 uassert.ErrorIs(t, b.SubmitProof(dave, r.ID, 0, good), ErrProofPending, "one at a time")
257}
258
259func TestReleaseRunsThroughToCompletion(t *testing.T) {
260 b := board3()
261 r := submitOne(t, b)
262 urequire.NoError(t, errOf(b.Vote(alice, r.ID, true, "", 11)))
263 urequire.NoError(t, errOf(b.Vote(bob, r.ID, true, "", 12)))
264
265 // Milestone 0.
266 urequire.NoError(t, b.SubmitProof(dave, r.ID, 0, Proof{Kind: "url", Ref: "https://x/1", Height: 20}))
267 out, err := b.Review(alice, r.ID, 0, true, "looks done", 21)
268 urequire.NoError(t, err)
269 uassert.Equal(t, "under review", out.String(), "one reviewer is not a majority")
270 uassert.False(t, r.Milestones[0].Released)
271
272 out, err = b.Review(carol, r.ID, 0, true, "confirmed", 22)
273 urequire.NoError(t, err)
274 uassert.Equal(t, "accepted", out.String())
275 uassert.True(t, r.Milestones[0].Released)
276 uassert.Equal(t, int64(22), r.Milestones[0].ReleasedAt)
277 uassert.Equal(t, int64(100), r.Paid())
278 uassert.Equal(t, int64(400), r.Outstanding())
279 uassert.Equal(t, "approved", r.Status.String(), "one tranche left")
280 uassert.Equal(t, 1, r.Next())
281
282 // Milestone 1 completes the grant.
283 urequire.NoError(t, b.SubmitProof(dave, r.ID, 1, Proof{Kind: "hash", Ref: "deadbeef", Height: 30}))
284 urequire.NoError(t, errOf2(b.Review(alice, r.ID, 1, true, "", 31)))
285 out, err = b.Review(bob, r.ID, 1, true, "", 32)
286 urequire.NoError(t, err)
287 uassert.Equal(t, "accepted", out.String())
288 uassert.Equal(t, "completed", r.Status.String())
289 uassert.Equal(t, -1, r.Next())
290 uassert.Equal(t, int64(500), r.Paid())
291 uassert.Equal(t, int64(0), r.Outstanding(), "a completed grant commits nothing")
292 uassert.Equal(t, int64(500), b.Disbursed())
293 uassert.Equal(t, int64(0), b.Committed())
294}
295
296func TestRefusedProofKeepsTheGrantAliveAndOnTheRecord(t *testing.T) {
297 b := board3()
298 r := submitOne(t, b)
299 urequire.NoError(t, errOf(b.Vote(alice, r.ID, true, "", 11)))
300 urequire.NoError(t, errOf(b.Vote(bob, r.ID, true, "", 12)))
301
302 urequire.NoError(t, b.SubmitProof(dave, r.ID, 0, Proof{Kind: "url", Ref: "https://x/draft", Height: 20}))
303 urequire.NoError(t, errOf2(b.Review(alice, r.ID, 0, false, "this is a stub", 21)))
304 out, err := b.Review(bob, r.ID, 0, false, "agreed, no tests", 22)
305 urequire.NoError(t, err)
306 uassert.Equal(t, "refused", out.String())
307 uassert.False(t, r.Milestones[0].Released)
308 uassert.Equal(t, "approved", r.Status.String(), "a refused proof does not kill the grant")
309
310 // The refused attempt stays readable, reasons and all.
311 m := r.Milestones[0]
312 urequire.Equal(t, 1, len(m.Attempts))
313 uassert.Equal(t, "this is a stub", m.Attempts[0].Reviews[0].Reason)
314 uassert.True(t, m.Current() == nil, "the attempt is closed")
315
316 // And a second attempt can carry.
317 urequire.NoError(t, b.SubmitProof(dave, r.ID, 0, Proof{Kind: "url", Ref: "https://x/final", Height: 30}))
318 urequire.NoError(t, errOf2(b.Review(alice, r.ID, 0, true, "now it is done", 31)))
319 urequire.NoError(t, errOf2(b.Review(bob, r.ID, 0, true, "", 32)))
320 uassert.True(t, r.Milestones[0].Released)
321 uassert.Equal(t, 2, len(m.Attempts), "both attempts are kept")
322}
323
324func TestReviewRefusals(t *testing.T) {
325 b := board3()
326 r := submitOne(t, b)
327 urequire.NoError(t, errOf(b.Vote(alice, r.ID, true, "", 11)))
328 urequire.NoError(t, errOf(b.Vote(bob, r.ID, true, "", 12)))
329
330 uassert.ErrorIs(t, errOf2(b.Review(alice, r.ID, 0, true, "", 20)), ErrNoProof)
331 urequire.NoError(t, b.SubmitProof(dave, r.ID, 0, Proof{Kind: "text", Ref: "done", Height: 20}))
332
333 uassert.ErrorIs(t, errOf2(b.Review(mallory, r.ID, 0, true, "", 21)), ErrNotMember)
334 uassert.ErrorIs(t, errOf2(b.Review(alice, 999, 0, true, "", 21)), ErrNoRequest)
335 uassert.ErrorIs(t, errOf2(b.Review(alice, r.ID, 9, true, "", 21)), ErrNoMilestone)
336 urequire.NoError(t, errOf2(b.Review(alice, r.ID, 0, true, "", 21)))
337 uassert.ErrorIs(t, errOf2(b.Review(alice, r.ID, 0, false, "", 22)), ErrAlreadyVoted)
338}
339
340func TestApplicantMayNotReviewTheirOwnProof(t *testing.T) {
341 b := New(alice, bob, dave) // dave is a member AND the applicant here
342 r, err := b.Submit(dave, "self funded", "", []*Milestone{NewMilestone("m", 10)}, 1)
343 urequire.NoError(t, err)
344 urequire.NoError(t, errOf(b.Vote(alice, r.ID, true, "", 2)))
345 urequire.NoError(t, errOf(b.Vote(bob, r.ID, true, "", 3)))
346 urequire.NoError(t, b.SubmitProof(dave, r.ID, 0, Proof{Kind: "text", Ref: "done", Height: 4}))
347 uassert.ErrorIs(t, errOf2(b.Review(dave, r.ID, 0, true, "ship it", 5)), ErrConflict)
348}
349
350func TestMembershipRequestAddsAndRemoves(t *testing.T) {
351 b := board3()
352
353 add, err := b.SubmitMemberChange(alice, dave, true, "he reviews everything anyway", 5)
354 urequire.NoError(t, err)
355 uassert.Equal(t, "membership", add.Kind.String())
356 uassert.Equal(t, "Add "+dave.String(), add.Title)
357 uassert.Equal(t, int64(0), add.Total(), "a membership request asks for no money")
358
359 urequire.NoError(t, errOf(b.Vote(alice, add.ID, true, "", 6)))
360 st, err := b.Vote(bob, add.ID, true, "", 7)
361 urequire.NoError(t, err)
362 uassert.Equal(t, "completed", st.String(), "membership executes on approval")
363 uassert.True(t, b.IsMember(dave))
364 uassert.Equal(t, int64(7), b.JoinedAt(dave), "joined at the deciding height")
365
366 // Now four members: a majority excluding the subject is still two.
367 rm, err := b.SubmitMemberChange(alice, carol, false, "inactive", 8)
368 urequire.NoError(t, err)
369 uassert.Equal(t, 3, b.Eligible(rm))
370 uassert.ErrorIs(t, errOf(b.Vote(carol, rm.ID, false, "no", 9)), ErrConflict, "the subject may not vote")
371 urequire.NoError(t, errOf(b.Vote(alice, rm.ID, true, "", 10)))
372 urequire.NoError(t, errOf(b.Vote(bob, rm.ID, true, "", 11)))
373 uassert.False(t, b.IsMember(carol))
374 uassert.Equal(t, 3, b.MemberCount())
375}
376
377func TestMemberChangeRefusals(t *testing.T) {
378 b := board3()
379
380 _, err := b.SubmitMemberChange(mallory, dave, true, "", 1)
381 uassert.ErrorIs(t, err, ErrNotMember, "only members touch the board's composition")
382 _, err = b.SubmitMemberChange(alice, alice, true, "", 1)
383 uassert.ErrorIs(t, err, ErrIsMember)
384 _, err = b.SubmitMemberChange(alice, dave, false, "", 1)
385 uassert.ErrorIs(t, err, ErrNotMember, "cannot remove a non-member")
386 _, err = b.SubmitMemberChange(alice, address(""), true, "", 1)
387 uassert.ErrorIs(t, err, ErrBadAddress)
388
389 solo := New(alice)
390 _, err = solo.SubmitMemberChange(alice, alice, false, "", 1)
391 uassert.ErrorIs(t, err, ErrLastMember, "the board cannot empty itself")
392}
393
394// Standing is the whole reason a removed member's yes does not keep carrying
395// decisions they are no longer part of.
396func TestStandingDropsBallotsFromFormerMembers(t *testing.T) {
397 b := board3()
398 r := submitOne(t, b)
399 urequire.NoError(t, errOf(b.Vote(carol, r.ID, true, "I like it", 11)))
400
401 rm, err := b.SubmitMemberChange(alice, carol, false, "left the project", 12)
402 urequire.NoError(t, err)
403 urequire.NoError(t, errOf(b.Vote(alice, rm.ID, true, "", 13)))
404 urequire.NoError(t, errOf(b.Vote(bob, rm.ID, true, "", 14)))
405 urequire.Equal(t, 2, b.MemberCount())
406
407 yes, _ := r.Tally()
408 uassert.Equal(t, 1, yes, "the ballot stays on the record")
409 yes, _ = b.Standing(r)
410 uassert.Equal(t, 0, yes, "but it no longer counts")
411
412 // With two members and an outsider applicant the bar is two, so alice
413 // alone still cannot carry it.
414 uassert.Equal(t, 2, b.Majority(r))
415 st, err := b.Vote(alice, r.ID, true, "", 15)
416 urequire.NoError(t, err)
417 uassert.Equal(t, "pending", st.String())
418}
419
420func TestListAndSizeStayInIDOrder(t *testing.T) {
421 b := board3()
422 for i := 0; i < 12; i++ {
423 _, err := b.Submit(dave, "r", "", []*Milestone{NewMilestone("m", 1)}, int64(i))
424 urequire.NoError(t, err)
425 }
426 all := b.List()
427 urequire.Equal(t, 12, len(all))
428 uassert.Equal(t, 12, b.Size())
429 for i, r := range all {
430 uassert.Equal(t, i+1, r.ID, "ids come back in numeric order past nine")
431 }
432}
433
434func longString(n int) string {
435 s := ""
436 for i := 0; i < n; i++ {
437 s += "x"
438 }
439 return s
440}
441
442// errOf and errOf2 drop the leading return value of Vote and Review so a call
443// reads as a single assertion.
444func errOf(_ Status, err error) error { return err }
445func errOf2(_ Outcome, err error) error { return err }
446
447func TestFilingForSomeoneElseNeedsAReason(t *testing.T) {
448 b := board3()
449 ms := func() []*Milestone { return []*Milestone{NewMilestone("m", 100)} }
450
451 _, err := b.SubmitFor(dave, mallory, "", "t", "", ms(), 10)
452 uassert.ErrorIs(t, err, ErrNeedReason, "a request for someone else has to say why")
453
454 // The same address is just a self request, and needs nothing extra.
455 self, err := b.SubmitFor(dave, dave, "", "t", "", ms(), 10)
456 urequire.NoError(t, err)
457 uassert.False(t, self.OnBehalf())
458
459 _, err = b.SubmitFor(dave, address("not-an-address"), "because", "t", "", ms(), 10)
460 uassert.ErrorIs(t, err, ErrBadAddress)
461
462 long := ""
463 for i := 0; i <= MaxReason; i++ {
464 long += "x"
465 }
466 _, err = b.SubmitFor(dave, mallory, long, "t", "", ms(), 10)
467 uassert.ErrorIs(t, err, ErrBadReason)
468
469 r, err := b.SubmitFor(dave, mallory, "their account is empty", "t", "", ms(), 10)
470 urequire.NoError(t, err)
471 uassert.True(t, r.OnBehalf())
472 uassert.Equal(t, mallory.String(), r.Payee().String())
473 uassert.Equal(t, dave.String(), r.Applicant.String())
474}
475
476// A member who is paid by a request someone else filed is the same conflict of
477// interest as a member who filed it, with one address in between.
478func TestBothPartiesAreBarredFromVotingOnAGrant(t *testing.T) {
479 b := board3()
480 r, err := b.SubmitFor(alice, bob, "bob asked me to file it", "t", "",
481 []*Milestone{NewMilestone("m", 100)}, 10)
482 urequire.NoError(t, err)
483
484 uassert.True(t, r.Excludes(alice), "the applicant")
485 uassert.True(t, r.Excludes(bob), "the beneficiary")
486 uassert.False(t, r.Excludes(carol))
487 uassert.Equal(t, 1, b.Eligible(r), "three members, two of them party to it")
488 uassert.Equal(t, 1, b.Majority(r))
489
490 _, err = b.Vote(alice, r.ID, true, "", 11)
491 uassert.ErrorIs(t, err, ErrConflict, "the applicant may not vote")
492 _, err = b.Vote(bob, r.ID, true, "", 11)
493 uassert.ErrorIs(t, err, ErrConflict, "the payee may not vote")
494
495 st, err := b.Vote(carol, r.ID, true, "fine", 12)
496 urequire.NoError(t, err)
497 uassert.Equal(t, "approved", st.String(), "the one eligible member carries it")
498
499 // And the same exclusion holds on the proof review, which is where the
500 // money actually moves.
501 urequire.NoError(t, b.SubmitProof(alice, r.ID, 0, Proof{Kind: "text", Ref: "done", Height: 13}))
502 _, err = b.Review(bob, r.ID, 0, true, "", 14)
503 uassert.ErrorIs(t, err, ErrConflict, "the payee may not review their own proof")
504}
505
506// The payee of a request filed for an empty account cannot transact until the
507// first tranche lands, so the filer has to be able to prove the work; once the
508// payee can transact, they have to be able to prove their own.
509func TestEitherPartyMaySubmitAProof(t *testing.T) {
510 b := board3()
511 r, err := b.SubmitFor(dave, mallory, "empty account", "t", "",
512 []*Milestone{NewMilestone("one", 100), NewMilestone("two", 100)}, 10)
513 urequire.NoError(t, err)
514 b.Vote(alice, r.ID, true, "", 11)
515 b.Vote(bob, r.ID, true, "", 12)
516 urequire.Equal(t, "approved", r.Status.String())
517
518 uassert.ErrorIs(t, b.SubmitProof(carol, r.ID, 0, Proof{Kind: "text", Ref: "x", Height: 13}),
519 ErrNotApplicant, "a third party may not")
520
521 urequire.NoError(t, b.SubmitProof(dave, r.ID, 0, Proof{Kind: "text", Ref: "filer", Height: 13}))
522 b.Review(alice, r.ID, 0, true, "", 14)
523 b.Review(bob, r.ID, 0, true, "", 15)
524 urequire.True(t, r.Milestones[0].Released)
525
526 urequire.NoError(t, b.SubmitProof(mallory, r.ID, 1, Proof{Kind: "text", Ref: "payee", Height: 16}))
527}