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}