Search Apps Documentation Source Content File Folder Download Copy Actions Download

proposal_test.gno

11.54 Kb · 459 lines
  1package commondao_test
  2
  3import (
  4	"errors"
  5	"testing"
  6	"time"
  7
  8	"gno.land/p/nt/uassert/v0"
  9	"gno.land/p/nt/urequire/v0"
 10
 11	"gno.land/p/nt/commondao/v0"
 12)
 13
 14func TestProposalNew(t *testing.T) {
 15	cases := []struct {
 16		name       string
 17		creator    address
 18		definition commondao.ProposalDefinition
 19		err        error
 20	}{
 21		{
 22			name:       "success",
 23			creator:    "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
 24			definition: testPropDef{votingPeriod: time.Minute * 10},
 25		},
 26		{
 27			name:       "invalid creator address",
 28			creator:    "invalid",
 29			definition: testPropDef{},
 30			err:        commondao.ErrInvalidCreatorAddress,
 31		},
 32		{
 33			name:    "max custom vote choices exceeded",
 34			creator: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
 35			definition: testPropDef{
 36				voteChoices: make([]commondao.VoteChoice, commondao.MaxCustomVoteChoices+1),
 37			},
 38			err: commondao.ErrMaxCustomVoteChoices,
 39		},
 40	}
 41
 42	for _, tc := range cases {
 43		t.Run(tc.name, func(t *testing.T) {
 44			id := uint64(1)
 45
 46			p, err := commondao.NewProposal(id, tc.creator, tc.definition)
 47
 48			if tc.err != nil {
 49				urequire.ErrorIs(t, err, tc.err, "expected an error")
 50				return
 51			}
 52
 53			urequire.NoError(t, err, "unexpected error")
 54			uassert.Equal(t, p.ID(), id)
 55			uassert.NotEqual(t, p.Definition(), nil)
 56			uassert.True(t, p.Status() == commondao.StatusActive)
 57			uassert.Equal(t, p.Creator(), tc.creator)
 58			uassert.False(t, p.CreatedAt().IsZero())
 59			uassert.NotEqual(t, p.VotingRecord(), nil)
 60			uassert.Empty(t, p.StatusReason())
 61			uassert.True(t, p.VotingDeadline() == p.CreatedAt().Add(tc.definition.VotingPeriod()))
 62		})
 63	}
 64}
 65
 66func TestProposalVoteChoices(t *testing.T) {
 67	cases := []struct {
 68		name       string
 69		definition commondao.ProposalDefinition
 70		choices    []commondao.VoteChoice
 71	}{
 72		{
 73			name:       "custom choices",
 74			definition: testPropDef{voteChoices: []commondao.VoteChoice{"FOO", "BAR", "BAZ"}},
 75			choices: []commondao.VoteChoice{
 76				"BAR",
 77				"BAZ",
 78				"FOO",
 79			},
 80		},
 81		{
 82			name:       "defaults because of empty custom choice list",
 83			definition: testPropDef{voteChoices: []commondao.VoteChoice{}},
 84			choices: []commondao.VoteChoice{
 85				commondao.ChoiceAbstain,
 86				commondao.ChoiceNo,
 87				commondao.ChoiceYes,
 88			},
 89		},
 90		{
 91			name:       "defaults because of single custom choice list",
 92			definition: testPropDef{voteChoices: []commondao.VoteChoice{"FOO"}},
 93			choices: []commondao.VoteChoice{
 94				commondao.ChoiceAbstain,
 95				commondao.ChoiceNo,
 96				commondao.ChoiceYes,
 97			},
 98		},
 99	}
100
101	for _, tc := range cases {
102		t.Run(tc.name, func(t *testing.T) {
103			p, _ := commondao.NewProposal(1, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", testPropDef{
104				voteChoices: tc.choices,
105			})
106
107			choices := p.VoteChoices()
108
109			urequire.Equal(t, len(choices), len(tc.choices), "expect vote choice count to match")
110			for i, c := range choices {
111				urequire.True(t, tc.choices[i] == c, "expect vote choice to match")
112			}
113		})
114	}
115}
116
117func TestIsQuorumReached(t *testing.T) {
118	cases := []struct {
119		name    string
120		quorum  float64
121		members []address
122		votes   []commondao.Vote
123		fail    bool
124	}{
125		{
126			name:   "one third",
127			quorum: commondao.QuorumOneThird,
128			members: []address{
129				"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
130				"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
131				"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
132			},
133			votes: []commondao.Vote{
134				{
135					Address: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
136					Choice:  commondao.ChoiceYes,
137				},
138			},
139		},
140		{
141			name:   "one third no quorum",
142			quorum: commondao.QuorumOneThird,
143			members: []address{
144				"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
145				"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
146				"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
147			},
148			fail: true,
149		},
150		{
151			name:   "simple majority",
152			quorum: commondao.QuorumMoreThanHalf,
153			members: []address{
154				"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
155				"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
156				"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
157				"g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt",
158			},
159			votes: []commondao.Vote{
160				{
161					Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
162					Choice:  commondao.ChoiceYes,
163				},
164				{
165					Address: "g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
166					Choice:  commondao.ChoiceNo,
167				},
168				{
169					Address: "g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt",
170					Choice:  commondao.ChoiceNo,
171				},
172			},
173		},
174		{
175			name:   "simple majority no quorum",
176			quorum: commondao.QuorumMoreThanHalf,
177			members: []address{
178				"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
179				"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
180				"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
181				"g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt",
182			},
183			votes: []commondao.Vote{
184				{
185					Address: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
186					Choice:  commondao.ChoiceYes,
187				},
188			},
189			fail: true,
190		},
191		{
192			name:   "two thirds",
193			quorum: commondao.QuorumTwoThirds,
194			members: []address{
195				"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
196				"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
197				"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
198			},
199			votes: []commondao.Vote{
200				{
201					Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
202					Choice:  commondao.ChoiceYes,
203				},
204				{
205					Address: "g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
206					Choice:  commondao.ChoiceNo,
207				},
208			},
209		},
210		{
211			name:   "two thirds no quorum",
212			quorum: commondao.QuorumTwoThirds,
213			members: []address{
214				"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
215				"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
216				"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
217			},
218			votes: []commondao.Vote{
219				{
220					Address: "g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
221					Choice:  commondao.ChoiceNo,
222				},
223			},
224			fail: true,
225		},
226		{
227			name:   "three fourths",
228			quorum: commondao.QuorumThreeFourths,
229			members: []address{
230				"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
231				"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
232				"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
233				"g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt",
234			},
235			votes: []commondao.Vote{
236				{
237					Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
238					Choice:  commondao.ChoiceYes,
239				},
240				{
241					Address: "g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
242					Choice:  commondao.ChoiceNo,
243				},
244				{
245					Address: "g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt",
246					Choice:  commondao.ChoiceNo,
247				},
248			},
249		},
250		{
251			name:   "three fourths no quorum",
252			quorum: commondao.QuorumThreeFourths,
253			members: []address{
254				"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
255				"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
256				"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
257				"g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt",
258			},
259			votes: []commondao.Vote{
260				{
261					Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
262					Choice:  commondao.ChoiceYes,
263				},
264			},
265			fail: true,
266		},
267		{
268			name:   "full",
269			quorum: commondao.QuorumFull,
270			members: []address{
271				"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
272				"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
273			},
274			votes: []commondao.Vote{
275				{
276					Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
277					Choice:  commondao.ChoiceNo,
278				},
279				{
280					Address: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
281					Choice:  commondao.ChoiceNo,
282				},
283			},
284		},
285		{
286			name:   "full no quorum",
287			quorum: commondao.QuorumFull,
288			members: []address{
289				"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
290				"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
291			},
292			votes: []commondao.Vote{
293				{
294					Address: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
295					Choice:  commondao.ChoiceNo,
296				},
297			},
298			fail: true,
299		},
300		{
301			name:   "no quorum with empty vote",
302			quorum: commondao.QuorumMoreThanHalf,
303			members: []address{
304				"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
305				"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
306			},
307			votes: []commondao.Vote{
308				{
309					Address: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
310					Choice:  commondao.ChoiceNone,
311				},
312			},
313			fail: true,
314		},
315		{
316			name:   "no quorum with abstention",
317			quorum: commondao.QuorumMoreThanHalf,
318			members: []address{
319				"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
320				"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
321			},
322			votes: []commondao.Vote{
323				{
324					Address: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
325					Choice:  commondao.ChoiceAbstain,
326				},
327			},
328			fail: true,
329		},
330		{
331			name:   "invalid quorum percentage",
332			quorum: -1,
333			fail:   true,
334		},
335	}
336
337	for _, tc := range cases {
338		t.Run(tc.name, func(t *testing.T) {
339			members := commondao.NewMemberStorage()
340			storage := commondao.MustNewReadonlyMemberStorage(members)
341			for _, m := range tc.members {
342				members.Add(m)
343			}
344
345			var record commondao.VotingRecord
346			for _, v := range tc.votes {
347				record.AddVote(v)
348			}
349
350			success := commondao.IsQuorumReached(tc.quorum, record.Readonly(), *storage)
351
352			if tc.fail {
353				uassert.False(t, success, "expect quorum to fail")
354			} else {
355				uassert.True(t, success, "expect quorum to succeed")
356			}
357		})
358	}
359}
360
361func TestProposalTally(t *testing.T) {
362	errTest := errors.New("test")
363	creator := address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")
364	cases := []struct {
365		name   string
366		setup  func() *commondao.Proposal
367		status commondao.ProposalStatus
368		err    error
369	}{
370		{
371			name: "passed",
372			setup: func() *commondao.Proposal {
373				p, _ := commondao.NewProposal(1, creator, testPropDef{tallyResult: true})
374				return p
375			},
376			status: commondao.StatusPassed,
377		},
378		{
379			name: "rejected",
380			setup: func() *commondao.Proposal {
381				p, _ := commondao.NewProposal(1, creator, testPropDef{tallyResult: false})
382				return p
383			},
384			status: commondao.StatusRejected,
385		},
386		{
387			name: "proposal is not active",
388			setup: func() *commondao.Proposal {
389				p, _ := commondao.NewProposal(1, creator, testPropDef{tallyResult: true})
390				p.Tally(commondao.NewMemberStorage())
391				return p
392			},
393			err: commondao.ErrStatusIsNotActive,
394		},
395		{
396			name: "tally error",
397			setup: func() *commondao.Proposal {
398				p, _ := commondao.NewProposal(1, creator, testPropDef{tallyErr: errTest})
399				return p
400			},
401			err: errTest,
402		},
403	}
404
405	for _, tc := range cases {
406		t.Run(tc.name, func(t *testing.T) {
407			p := tc.setup()
408			members := commondao.NewMemberStorage()
409
410			err := p.Tally(members)
411
412			if tc.err != nil {
413				urequire.ErrorIs(t, err, tc.err)
414				return
415			}
416
417			urequire.NoError(t, err)
418			urequire.Equal(t, string(tc.status), string(p.Status()))
419		})
420	}
421}
422
423func TestMustValidate(t *testing.T) {
424	uassert.NotPanics(t, func() {
425		commondao.MustValidate(testPropDef{})
426	}, "expect validation to succeed")
427
428	uassert.PanicsWithMessage(t, "validable proposal definition is nil", func() {
429		commondao.MustValidate(nil)
430	}, "expect validation to panic with nil definition")
431
432	uassert.PanicsWithMessage(t, "boom!", func() {
433		commondao.MustValidate(testPropDef{validationErr: errors.New("boom!")})
434	}, "expect validation to panic")
435}
436
437// Executable non crossing proposal definition for unit tests
438type testPropDef struct {
439	votingPeriod            time.Duration
440	tallyResult             bool
441	validationErr, tallyErr error
442	voteChoices             []commondao.VoteChoice
443}
444
445func (testPropDef) Title() string                 { return "" }
446func (testPropDef) Body() string                  { return "" }
447func (d testPropDef) VotingPeriod() time.Duration { return d.votingPeriod }
448func (d testPropDef) Validate() error             { return d.validationErr }
449
450func (d testPropDef) Tally(commondao.VotingContext) (bool, error) {
451	return d.tallyResult, d.tallyErr
452}
453
454func (d testPropDef) CustomVoteChoices() []commondao.VoteChoice {
455	if len(d.voteChoices) > 0 {
456		return d.voteChoices
457	}
458	return []commondao.VoteChoice{commondao.ChoiceYes, commondao.ChoiceNo, commondao.ChoiceAbstain}
459}