commondao_test.gno
11.25 Kb ยท 470 lines
1package commondao
2
3import (
4 "errors"
5 "testing"
6 "time"
7
8 "gno.land/p/nt/seqid"
9 "gno.land/p/nt/uassert"
10 "gno.land/p/nt/urequire"
11)
12
13func TestNew(t *testing.T) {
14 cases := []struct {
15 name string
16 parent *CommonDAO
17 members []address
18 }{
19 {
20 name: "with parent",
21 parent: New(),
22 members: []address{"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"},
23 },
24 {
25 name: "without parent",
26 members: []address{"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"},
27 },
28 {
29 name: "multiple members",
30 members: []address{
31 "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",
32 "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
33 "g1w4ek2u3jta047h6lta047h6lta047h6l9huexc",
34 },
35 },
36 {
37 name: "no members",
38 },
39 }
40
41 for _, tc := range cases {
42 t.Run(tc.name, func(t *testing.T) {
43 membersCount := len(tc.members)
44 options := []Option{WithParent(tc.parent)}
45 for _, m := range tc.members {
46 options = append(options, WithMember(m))
47 }
48
49 dao := New(options...)
50
51 if tc.parent == nil {
52 uassert.Equal(t, nil, dao.Parent())
53 } else {
54 uassert.NotEqual(t, nil, dao.Parent())
55 }
56
57 uassert.False(t, dao.IsDeleted(), "expect DAO not to be soft deleted by default")
58 urequire.Equal(t, membersCount, dao.Members().Size(), "dao members")
59
60 var i int
61 dao.Members().IterateByOffset(0, membersCount, func(addr address) bool {
62 uassert.Equal(t, tc.members[i], addr)
63 i++
64 return false
65 })
66 })
67 }
68}
69
70func TestCommonDAOMembersAdd(t *testing.T) {
71 member := address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")
72 dao := New(WithMember("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn"))
73
74 added := dao.Members().Add(member)
75 urequire.True(t, added)
76
77 uassert.Equal(t, 2, dao.Members().Size())
78 uassert.True(t, dao.Members().Has(member))
79
80 added = dao.Members().Add(member)
81 urequire.False(t, added)
82}
83
84func TestCommonDAOMembersRemove(t *testing.T) {
85 member := address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")
86 dao := New(WithMember(member))
87
88 removed := dao.Members().Remove(member)
89 urequire.True(t, removed)
90
91 removed = dao.Members().Remove(member)
92 urequire.False(t, removed)
93}
94
95func TestCommonDAOMembersHas(t *testing.T) {
96 cases := []struct {
97 name string
98 member address
99 dao *CommonDAO
100 want bool
101 }{
102 {
103 name: "member",
104 member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
105 dao: New(WithMember("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn")),
106 want: true,
107 },
108 {
109 name: "not a dao member",
110 member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
111 dao: New(WithMember("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc")),
112 },
113 }
114
115 for _, tc := range cases {
116 t.Run(tc.name, func(t *testing.T) {
117 got := tc.dao.Members().Has(tc.member)
118 uassert.Equal(t, got, tc.want)
119 })
120 }
121}
122
123func TestCommonDAOPropose(t *testing.T) {
124 cases := []struct {
125 name string
126 setup func() *CommonDAO
127 creator address
128 def ProposalDefinition
129 err error
130 }{
131 {
132 name: "success",
133 setup: func() *CommonDAO { return New() },
134 creator: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
135 def: testPropDef{},
136 },
137 {
138 name: "nil definition",
139 setup: func() *CommonDAO { return New() },
140 err: ErrProposalDefinitionRequired,
141 },
142 {
143 name: "invalid creator address",
144 setup: func() *CommonDAO { return New() },
145 def: testPropDef{},
146 err: ErrInvalidCreatorAddress,
147 },
148 {
149 name: "proposal ID overflow",
150 setup: func() *CommonDAO {
151 dao := New()
152 dao.genID = seqid.ID(1<<64 - 1)
153 return dao
154 },
155 creator: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
156 def: testPropDef{},
157 err: ErrOverflow,
158 },
159 }
160
161 for _, tc := range cases {
162 t.Run(tc.name, func(t *testing.T) {
163 dao := tc.setup()
164
165 p, err := dao.Propose(tc.creator, tc.def)
166
167 if tc.err != nil {
168 urequire.ErrorIs(t, err, tc.err)
169 return
170 }
171
172 urequire.NoError(t, err)
173
174 found := dao.ActiveProposals().Has(p.ID())
175 urequire.True(t, found, "proposal not found")
176 uassert.Equal(t, p.Creator(), tc.creator)
177 })
178 }
179}
180
181func TestCommonDAOVote(t *testing.T) {
182 cases := []struct {
183 name string
184 setup func() *CommonDAO
185 member address
186 choice VoteChoice
187 proposalID uint64
188 err error
189 }{
190 {
191 name: "success",
192 setup: func() *CommonDAO {
193 member := address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn")
194 dao := New(WithMember(member))
195 dao.Propose(member, testPropDef{votingPeriod: time.Hour})
196 return dao
197 },
198 member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
199 choice: ChoiceYes,
200 proposalID: 1,
201 },
202 {
203 name: "success with custom vote choice",
204 setup: func() *CommonDAO {
205 member := address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn")
206 dao := New(WithMember(member))
207 dao.Propose(member, testPropDef{
208 votingPeriod: time.Hour,
209 voteChoices: []VoteChoice{"FOO", "BAR"},
210 })
211 return dao
212 },
213 member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
214 choice: VoteChoice("BAR"),
215 proposalID: 1,
216 },
217 {
218 name: "success with deadline check disabled",
219 setup: func() *CommonDAO {
220 member := address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn")
221 dao := New(
222 WithMember(member),
223 DisableVotingDeadlineCheck(),
224 )
225 dao.Propose(member, testPropDef{})
226 return dao
227 },
228 member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
229 choice: ChoiceYes,
230 proposalID: 1,
231 },
232 {
233 name: "invalid vote choice",
234 setup: func() *CommonDAO {
235 member := address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn")
236 dao := New(WithMember(member))
237 dao.Propose(member, testPropDef{votingPeriod: time.Hour})
238 return dao
239 },
240 member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
241 choice: VoteChoice("invalid"),
242 proposalID: 1,
243 err: ErrInvalidVoteChoice,
244 },
245 {
246 name: "not a member",
247 setup: func() *CommonDAO { return New() },
248 member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
249 choice: ChoiceAbstain,
250 err: ErrNotMember,
251 },
252 {
253 name: "proposal not found",
254 setup: func() *CommonDAO {
255 return New(WithMember("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn"))
256 },
257 member: "g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn",
258 choice: ChoiceAbstain,
259 proposalID: 42,
260 err: ErrProposalNotFound,
261 },
262 }
263
264 for _, tc := range cases {
265 t.Run(tc.name, func(t *testing.T) {
266 dao := tc.setup()
267
268 err := dao.Vote(tc.member, tc.proposalID, tc.choice, "")
269
270 if tc.err != nil {
271 urequire.ErrorIs(t, err, tc.err)
272 return
273 }
274
275 urequire.NoError(t, err)
276
277 p := dao.ActiveProposals().Get(tc.proposalID)
278 urequire.NotEqual(t, nil, p, "proposal not found")
279
280 record := p.VotingRecord()
281 uassert.True(t, record.HasVoted(tc.member))
282 uassert.Equal(t, record.VoteCount(tc.choice), 1)
283 })
284 }
285}
286
287func TestCommonDAOTally(t *testing.T) {
288 errTest := errors.New("test")
289 member := address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")
290 cases := []struct {
291 name string
292 setup func(*CommonDAO) (proposalID uint64)
293 passes bool
294 err error
295 }{
296 {
297 name: "pass",
298 setup: func(dao *CommonDAO) uint64 {
299 return dao.MustPropose(member, testPropDef{tallyResult: true}).ID()
300 },
301 passes: true,
302 },
303 {
304 name: "fail to pass",
305 setup: func(dao *CommonDAO) uint64 {
306 return dao.MustPropose(member, testPropDef{tallyResult: false}).ID()
307 },
308 passes: false,
309 },
310 {
311 name: "proposal not found",
312 setup: func(*CommonDAO) uint64 { return 404 },
313 err: ErrProposalNotFound,
314 },
315 {
316 name: "proposal status not active",
317 setup: func(dao *CommonDAO) uint64 {
318 p := dao.MustPropose(member, testPropDef{})
319 p.status = StatusPassed
320 return p.ID()
321 },
322 err: ErrStatusIsNotActive,
323 },
324 {
325 name: "proposal failed error",
326 setup: func(dao *CommonDAO) uint64 {
327 return dao.MustPropose(member, testPropDef{tallyErr: ErrProposalFailed}).ID()
328 },
329 passes: false,
330 },
331 {
332 name: "error",
333 setup: func(dao *CommonDAO) uint64 {
334 return dao.MustPropose(member, testPropDef{tallyErr: errTest}).ID()
335 },
336 err: errTest,
337 },
338 }
339
340 for _, tc := range cases {
341 t.Run(tc.name, func(t *testing.T) {
342 dao := New(WithMember(member))
343 proposalID := tc.setup(dao)
344
345 passes, err := dao.Tally(proposalID)
346
347 if tc.err != nil {
348 uassert.ErrorIs(t, err, tc.err, "expect an error")
349 uassert.False(t, passes, "expect tally to fail")
350 return
351 }
352
353 uassert.NoError(t, err, "expect no error")
354 uassert.Equal(t, tc.passes, passes, "expect tally success value to match")
355 })
356 }
357}
358
359func TestCommonDAOExecute(t *testing.T) {
360 errTest := errors.New("test")
361 member := address("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn")
362 cases := []struct {
363 name string
364 setup func() *CommonDAO
365 proposalID uint64
366 status ProposalStatus
367 statusReason string
368 err error
369 }{
370 // TODO: Execution success and error are implemented as filetests
371 // This is done because proposal definition's Execute() must be
372 // crossing which is not possible without defining it within a realm.
373 // {
374 // name: "success",
375 // setup: func() *CommonDAO {
376 // dao := New(WithMember(member))
377 // dao.Propose(member, testPropDef{tallyResult: true})
378 // return dao
379 // },
380 // status: StatusPassed,
381 // proposalID: 1,
382 // },
383 // {
384 // name: "execution error",
385 // setup: func() *CommonDAO {
386 // dao := New(WithMember(member))
387 // dao.Propose(member, testPropDef{
388 // tallyResult: true,
389 // executionErr: errTest,
390 // })
391 // return dao
392 // },
393 // proposalID: 1,
394 // status: StatusFailed,
395 // statusReason: errTest.Error(),
396 // },
397 {
398 name: "proposal not found",
399 setup: func() *CommonDAO { return New() },
400 proposalID: 1,
401 err: ErrProposalNotFound,
402 },
403 {
404 name: "proposal not active",
405 setup: func() *CommonDAO {
406 dao := New(WithMember(member))
407 p, _ := dao.Propose(member, testPropDef{})
408 p.status = StatusPassed
409 return dao
410 },
411 proposalID: 1,
412 err: ErrStatusIsNotActive,
413 },
414 {
415 name: "voting deadline not met",
416 setup: func() *CommonDAO {
417 dao := New(WithMember(member))
418 dao.Propose(member, testPropDef{votingPeriod: time.Minute * 5})
419 return dao
420 },
421 proposalID: 1,
422 err: ErrVotingDeadlineNotMet,
423 },
424 {
425 name: "validation error",
426 setup: func() *CommonDAO {
427 dao := New(WithMember(member))
428 dao.Propose(member, testPropDef{validationErr: errTest})
429 return dao
430 },
431 proposalID: 1,
432 status: StatusFailed,
433 statusReason: errTest.Error(),
434 },
435 {
436 name: "tally error",
437 setup: func() *CommonDAO {
438 dao := New(WithMember(member))
439 dao.Propose(member, testPropDef{tallyErr: errTest})
440 return dao
441 },
442 proposalID: 1,
443 status: StatusFailed,
444 statusReason: errTest.Error(),
445 },
446 }
447
448 for _, tc := range cases {
449 t.Run(tc.name, func(t *testing.T) {
450 dao := tc.setup()
451
452 err := dao.Execute(tc.proposalID)
453
454 if tc.err != nil {
455 urequire.ErrorIs(t, err, tc.err, "expect error to match")
456 return
457 }
458
459 urequire.NoError(t, err, "expect no error")
460
461 found := dao.ActiveProposals().Has(tc.proposalID)
462 urequire.False(t, found, "proposal should not be active")
463
464 p := dao.FinishedProposals().Get(tc.proposalID)
465 urequire.NotEqual(t, nil, p, "proposal must be found")
466 uassert.Equal(t, string(p.Status()), string(tc.status), "status must match")
467 uassert.Equal(t, string(p.StatusReason()), string(tc.statusReason), "status reason must match")
468 })
469 }
470}