Search Apps Documentation Source Content File Folder Download Copy Actions Download

proposal_test.gno

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