proposal_test.gno
11.32 Kb ยท 454 lines
1package commondao_test
2
3import (
4 "errors"
5 "testing"
6 "time"
7
8 "gno.land/p/nt/uassert"
9 "gno.land/p/nt/urequire"
10
11 "gno.land/p/nt/commondao"
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: "half",
152 quorum: commondao.QuorumHalf,
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 },
170 {
171 name: "half no quorum",
172 quorum: commondao.QuorumHalf,
173 members: []address{
174 "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
175 "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
176 "g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
177 "g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt",
178 },
179 votes: []commondao.Vote{
180 {
181 Address: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
182 Choice: commondao.ChoiceYes,
183 },
184 },
185 fail: true,
186 },
187 {
188 name: "two thirds",
189 quorum: commondao.QuorumTwoThirds,
190 members: []address{
191 "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
192 "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
193 "g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
194 },
195 votes: []commondao.Vote{
196 {
197 Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
198 Choice: commondao.ChoiceYes,
199 },
200 {
201 Address: "g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
202 Choice: commondao.ChoiceNo,
203 },
204 },
205 },
206 {
207 name: "two thirds no quorum",
208 quorum: commondao.QuorumTwoThirds,
209 members: []address{
210 "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
211 "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
212 "g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
213 },
214 votes: []commondao.Vote{
215 {
216 Address: "g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
217 Choice: commondao.ChoiceNo,
218 },
219 },
220 fail: true,
221 },
222 {
223 name: "three fourths",
224 quorum: commondao.QuorumThreeFourths,
225 members: []address{
226 "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
227 "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
228 "g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
229 "g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt",
230 },
231 votes: []commondao.Vote{
232 {
233 Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
234 Choice: commondao.ChoiceYes,
235 },
236 {
237 Address: "g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
238 Choice: commondao.ChoiceNo,
239 },
240 {
241 Address: "g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt",
242 Choice: commondao.ChoiceNo,
243 },
244 },
245 },
246 {
247 name: "three fourths no quorum",
248 quorum: commondao.QuorumThreeFourths,
249 members: []address{
250 "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
251 "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
252 "g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
253 "g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt",
254 },
255 votes: []commondao.Vote{
256 {
257 Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
258 Choice: commondao.ChoiceYes,
259 },
260 },
261 fail: true,
262 },
263 {
264 name: "full",
265 quorum: commondao.QuorumFull,
266 members: []address{
267 "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
268 "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
269 },
270 votes: []commondao.Vote{
271 {
272 Address: "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
273 Choice: commondao.ChoiceNo,
274 },
275 {
276 Address: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
277 Choice: commondao.ChoiceNo,
278 },
279 },
280 },
281 {
282 name: "full no quorum",
283 quorum: commondao.QuorumFull,
284 members: []address{
285 "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
286 "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
287 },
288 votes: []commondao.Vote{
289 {
290 Address: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
291 Choice: commondao.ChoiceNo,
292 },
293 },
294 fail: true,
295 },
296 {
297 name: "no quorum with empty vote",
298 quorum: commondao.QuorumHalf,
299 members: []address{
300 "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
301 "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
302 },
303 votes: []commondao.Vote{
304 {
305 Address: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
306 Choice: commondao.ChoiceNone,
307 },
308 },
309 fail: true,
310 },
311 {
312 name: "no quorum with abstention",
313 quorum: commondao.QuorumHalf,
314 members: []address{
315 "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
316 "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
317 },
318 votes: []commondao.Vote{
319 {
320 Address: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
321 Choice: commondao.ChoiceAbstain,
322 },
323 },
324 fail: true,
325 },
326 {
327 name: "invalid quorum percentage",
328 quorum: -1,
329 fail: true,
330 },
331 }
332
333 for _, tc := range cases {
334 t.Run(tc.name, func(t *testing.T) {
335 members := commondao.NewMemberStorage()
336 storage := commondao.MustNewReadonlyMemberStorage(members)
337 for _, m := range tc.members {
338 members.Add(m)
339 }
340
341 var record commondao.VotingRecord
342 for _, v := range tc.votes {
343 record.AddVote(v)
344 }
345
346 success := commondao.IsQuorumReached(tc.quorum, record.Readonly(), *storage)
347
348 if tc.fail {
349 uassert.False(t, success, "expect quorum to fail")
350 } else {
351 uassert.True(t, success, "expect quorum to succeed")
352 }
353 })
354 }
355}
356
357func TestProposalTally(t *testing.T) {
358 errTest := errors.New("test")
359 creator := address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")
360 cases := []struct {
361 name string
362 setup func() *commondao.Proposal
363 status commondao.ProposalStatus
364 err error
365 }{
366 {
367 name: "passed",
368 setup: func() *commondao.Proposal {
369 p, _ := commondao.NewProposal(1, creator, testPropDef{tallyResult: true})
370 return p
371 },
372 status: commondao.StatusPassed,
373 },
374 {
375 name: "rejected",
376 setup: func() *commondao.Proposal {
377 p, _ := commondao.NewProposal(1, creator, testPropDef{tallyResult: false})
378 return p
379 },
380 status: commondao.StatusRejected,
381 },
382 {
383 name: "proposal is not active",
384 setup: func() *commondao.Proposal {
385 p, _ := commondao.NewProposal(1, creator, testPropDef{tallyResult: true})
386 p.Tally(commondao.NewMemberStorage())
387 return p
388 },
389 err: commondao.ErrStatusIsNotActive,
390 },
391 {
392 name: "tally error",
393 setup: func() *commondao.Proposal {
394 p, _ := commondao.NewProposal(1, creator, testPropDef{tallyErr: errTest})
395 return p
396 },
397 err: errTest,
398 },
399 }
400
401 for _, tc := range cases {
402 t.Run(tc.name, func(t *testing.T) {
403 p := tc.setup()
404 members := commondao.NewMemberStorage()
405
406 err := p.Tally(members)
407
408 if tc.err != nil {
409 urequire.ErrorIs(t, err, tc.err)
410 return
411 }
412
413 urequire.NoError(t, err)
414 urequire.Equal(t, string(tc.status), string(p.Status()))
415 })
416 }
417}
418
419func TestMustValidate(t *testing.T) {
420 uassert.NotPanics(t, func() {
421 commondao.MustValidate(testPropDef{})
422 }, "expect validation to succeed")
423
424 uassert.PanicsWithMessage(t, "validable proposal definition is nil", func() {
425 commondao.MustValidate(nil)
426 }, "expect validation to panic with nil definition")
427
428 uassert.PanicsWithMessage(t, "boom!", func() {
429 commondao.MustValidate(testPropDef{validationErr: errors.New("boom!")})
430 }, "expect validation to panic")
431}
432
433type testPropDef struct {
434 votingPeriod time.Duration
435 tallyResult bool
436 validationErr, tallyErr error
437 voteChoices []commondao.VoteChoice
438}
439
440func (testPropDef) Title() string { return "" }
441func (testPropDef) Body() string { return "" }
442func (d testPropDef) VotingPeriod() time.Duration { return d.votingPeriod }
443func (d testPropDef) Validate() error { return d.validationErr }
444
445func (d testPropDef) Tally(commondao.VotingContext) (bool, error) {
446 return d.tallyResult, d.tallyErr
447}
448
449func (d testPropDef) CustomVoteChoices() []commondao.VoteChoice {
450 if len(d.voteChoices) > 0 {
451 return d.voteChoices
452 }
453 return []commondao.VoteChoice{commondao.ChoiceYes, commondao.ChoiceNo, commondao.ChoiceAbstain}
454}