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

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}