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}