basedao_test.gno
17.71 Kb ยท 721 lines
1package basedao
2
3import (
4 "testing"
5
6 "gno.land/p/nt/testutils"
7 "gno.land/p/nt/urequire"
8 "gno.land/p/samcrew/daocond"
9 "gno.land/p/samcrew/daokit"
10)
11
12var (
13 alice = testutils.TestAddress("alice")
14 bob = testutils.TestAddress("bob")
15 carol = testutils.TestAddress("carol")
16 dave = testutils.TestAddress("dave")
17)
18
19func TestNewDAO(t *testing.T) {
20 initialRoles := []RoleInfo{
21 {Name: "admin", Description: "Admin is the superuser"},
22 }
23 initialMembers := []Member{
24 {alice.String(), []string{"admin"}},
25 {bob.String(), []string{}},
26 {carol.String(), []string{}},
27 }
28 conf := &Config{
29 Name: "My DAO",
30 Description: "My DAO Description",
31 Members: NewMembersStore(initialRoles, initialMembers),
32 GetProfileString: func(addr address, field, def string) string { return "" },
33 }
34
35 daoRealm := testing.NewCodeRealm("gno.land/r/testing/daorealm")
36 testing.SetOriginCaller(alice)
37 testing.SetRealm(daoRealm)
38 _, dao := New(conf)
39 roles := dao.Members.GetRoles()
40 if len(roles) != 1 {
41 t.Errorf("Expected 1 role, got %d", len(roles))
42 }
43 if roles[0] != "admin" {
44 t.Errorf("Expected role 'admin', got %s", roles[0])
45 }
46
47 for _, member := range initialMembers {
48 addr := member.Address
49 if !dao.Members.IsMember(addr) {
50 t.Errorf("Expected member %s to be a member", addr)
51 }
52 if len(member.Roles) == 1 && !dao.Members.HasRole(addr, member.Roles[0]) {
53 t.Errorf("Expected member %s to have role %s", addr, member.Roles[0])
54 }
55 }
56
57 urequire.Equal(t, 4, dao.Core.ResourcesCount(), "expected 4 resources")
58 urequire.Equal(t, dao.Realm.PkgPath(), daoRealm.PkgPath())
59
60 // XXX: check realm and profile
61}
62
63func TestPropose(t *testing.T) {
64 members := []Member{
65 {alice.String(), []string{"admin"}},
66 {bob.String(), []string{}},
67 {carol.String(), []string{}},
68 }
69 tdao := newTestingDAO(t, 0.6, members)
70
71 type testNewProposalInput struct {
72 proposalReq daokit.ProposalRequest
73 proposer address
74 }
75
76 type tesNewProposalExpected struct {
77 title string
78 description string
79 proposer address
80 messsageType string
81 panic bool
82 }
83
84 type testNewProposal struct {
85 input testNewProposalInput
86 expected tesNewProposalExpected
87 }
88
89 type testNewProposalTable map[string]testNewProposal
90
91 tests := testNewProposalTable{
92 "Success": {
93 input: testNewProposalInput{
94 proposalReq: tdao.mockProposalRequest,
95 proposer: alice,
96 },
97 expected: tesNewProposalExpected{
98 title: "My Proposal",
99 description: "My Proposal Description",
100 proposer: alice,
101 messsageType: "valid",
102 panic: false,
103 },
104 },
105 "Non-member": {
106 input: testNewProposalInput{
107 proposalReq: tdao.mockProposalRequest,
108 proposer: dave,
109 },
110 expected: tesNewProposalExpected{
111 panic: true,
112 },
113 },
114 "Unknown action type": {
115 input: testNewProposalInput{
116 proposalReq: tdao.unknownProposalRequest,
117 proposer: alice,
118 },
119 expected: tesNewProposalExpected{
120 panic: true,
121 },
122 },
123 }
124
125 for testName, test := range tests {
126 t.Run(testName, func(t *testing.T) {
127 if test.expected.panic {
128 defer func() {
129 if r := recover(); r == nil {
130 t.Errorf("Expected panic, got none")
131 }
132 }()
133 }
134 *tdao.mockHandlerCalled = false
135 func() {
136 testing.SetOriginCaller(test.input.proposer)
137 }()
138 id := tdao.dao.Propose(test.input.proposalReq)
139
140 urequire.False(t, *tdao.mockHandlerCalled, "execute should not be called")
141
142 proposal := tdao.privdao.Core.Proposals.GetProposal(id)
143 if proposal.Title != test.expected.title {
144 t.Errorf("Expected title %s, got %s", test.expected.title, proposal.Title)
145 }
146 if proposal.Description != test.expected.description {
147 t.Errorf("Expected description %s, got %s", test.expected.description, proposal.Description)
148 }
149 if proposal.ProposerID != test.expected.proposer.String() {
150 t.Errorf("Expected proposer %s, got %s", test.expected.proposer, proposal.ProposerID)
151 }
152 if proposal.Action.Type() != test.expected.messsageType {
153 t.Errorf("Expected action type %s, got %s", test.expected.messsageType, proposal.Action.Type())
154 }
155 })
156 }
157}
158
159func TestVote(t *testing.T) {
160 members := []Member{
161 {alice.String(), []string{"admin"}},
162 {bob.String(), []string{}},
163 {carol.String(), []string{}},
164 }
165 tdao := newTestingDAO(t, 0.6, members)
166
167 tdao.dao.Propose(tdao.mockProposalRequest)
168
169 type testVoteInput struct {
170 proposalID uint64
171 vote daocond.Vote
172 voter address
173 }
174
175 type testVoteExpected struct {
176 eval bool
177 panic string
178 }
179
180 type testVote struct {
181 input testVoteInput
182 expected testVoteExpected
183 }
184
185 type testVoteTable map[string]testVote
186
187 tests := testVoteTable{
188 "Success no": {
189 input: testVoteInput{
190 proposalID: 1,
191 vote: "no",
192 voter: alice,
193 },
194 expected: testVoteExpected{
195 eval: false,
196 },
197 },
198 "Success yes": {
199 input: testVoteInput{
200 proposalID: 1,
201 vote: "yes",
202 voter: alice,
203 },
204 expected: testVoteExpected{
205 eval: true,
206 },
207 },
208 "Unknown proposal": {
209 input: testVoteInput{
210 proposalID: 2,
211 vote: "yes",
212 voter: alice,
213 },
214 expected: testVoteExpected{
215 panic: "proposal not found",
216 },
217 },
218 "Non-member": {
219 input: testVoteInput{
220 proposalID: 1,
221 vote: "yes",
222 voter: dave,
223 },
224 expected: testVoteExpected{
225 panic: "caller is not a member",
226 },
227 },
228 "Invalid vote": {
229 input: testVoteInput{
230 proposalID: 1,
231 vote: "very-long-vote-very-long-vote-very-long-vote",
232 voter: alice,
233 },
234 expected: testVoteExpected{
235 panic: "invalid vote",
236 },
237 },
238 }
239
240 for testName, test := range tests {
241 t.Run(testName, func(t *testing.T) {
242 run := func() {
243 func() {
244 testing.SetOriginCaller(test.input.voter)
245 }()
246 tdao.dao.Vote(test.input.proposalID, test.input.vote)
247
248 }
249
250 *tdao.mockHandlerCalled = false
251
252 if test.expected.panic != "" {
253 urequire.PanicsWithMessage(t, test.expected.panic, run)
254 } else {
255 urequire.NotPanics(t, run)
256 urequire.False(t, *tdao.mockHandlerCalled, "execute should not be called")
257 proposal := tdao.privdao.Core.Proposals.GetProposal(test.input.proposalID)
258 eval := proposal.Condition.Eval(proposal.Ballot)
259 if eval != test.expected.eval {
260 t.Errorf("Expected eval %t, got %t", test.expected.eval, eval)
261 }
262 }
263 })
264 }
265}
266
267func TestExecuteProposal(t *testing.T) {
268 members := []Member{
269 {alice.String(), []string{"admin"}},
270 {bob.String(), []string{}},
271 {carol.String(), []string{}},
272 }
273 tdao := newTestingDAO(t, 0.6, members)
274
275 tdao.dao.Propose(tdao.mockProposalRequest)
276
277 type testExecuteInput struct {
278 proposalID uint64
279 executor address
280 haveVote bool
281 voter address
282 }
283
284 type testExecuteExpected struct {
285 panic bool
286 }
287
288 type testExecute struct {
289 input testExecuteInput
290 expected testExecuteExpected
291 }
292
293 type testExecuteTable map[string]testExecute
294
295 tests := testExecuteTable{
296 "Conditions not met": {
297 input: testExecuteInput{
298 proposalID: 1,
299 executor: alice,
300 haveVote: false,
301 voter: alice,
302 },
303 expected: testExecuteExpected{
304 panic: true,
305 },
306 },
307 "Success": {
308 input: testExecuteInput{
309 proposalID: 1,
310 executor: alice,
311 haveVote: true,
312 voter: alice,
313 },
314 expected: testExecuteExpected{
315 panic: false,
316 },
317 },
318 "Unknown proposal": {
319 input: testExecuteInput{
320 proposalID: 2,
321 executor: alice,
322 haveVote: false,
323 voter: alice,
324 },
325 expected: testExecuteExpected{
326 panic: true,
327 },
328 },
329 "Non-member": {
330 input: testExecuteInput{
331 proposalID: 1,
332 executor: dave,
333 haveVote: false,
334 voter: alice,
335 },
336 expected: testExecuteExpected{
337 panic: true,
338 },
339 },
340 }
341
342 for testName, test := range tests {
343 t.Run(testName, func(t *testing.T) {
344 if test.expected.panic {
345 defer func() {
346 if r := recover(); r == nil {
347 t.Errorf("Expected panic, got none")
348 }
349 }()
350 }
351
352 if test.input.haveVote {
353 func() {
354 testing.SetOriginCaller(test.input.voter)
355 }()
356 tdao.dao.Vote(test.input.proposalID, "yes")
357
358 }
359
360 func() {
361 testing.SetOriginCaller(test.input.executor)
362 }()
363 tdao.dao.Execute(test.input.proposalID)
364
365 proposal := tdao.privdao.Core.Proposals.GetProposal(test.input.proposalID)
366
367 if proposal.Status != daokit.ProposalStatusExecuted {
368 t.Errorf("Expected status %s, got %s", daokit.ProposalStatusExecuted, proposal.Status)
369 }
370 })
371 }
372}
373
374func TestInstantExecute(t *testing.T) {
375 members := []Member{
376 {alice.String(), []string{"admin"}},
377 {bob.String(), []string{}},
378 {carol.String(), []string{}},
379 }
380 tdao := newTestingDAO(t, 0.6, members)
381
382 type testInstantExecuteInput struct {
383 proposalReq daokit.ProposalRequest
384 executor address
385 }
386
387 type testInstantExecuteExpected struct {
388 panic bool
389 }
390
391 type testInstantExecute struct {
392 input testInstantExecuteInput
393 expected testInstantExecuteExpected
394 }
395
396 type testInstantExecuteTable map[string]testInstantExecute
397
398 tests := testInstantExecuteTable{
399 "Success": {
400 input: testInstantExecuteInput{
401 proposalReq: tdao.mockProposalRequest,
402 executor: alice,
403 },
404 expected: testInstantExecuteExpected{
405 panic: false,
406 },
407 },
408 "Unknown action type": {
409 input: testInstantExecuteInput{
410 proposalReq: tdao.unknownProposalRequest,
411 executor: alice,
412 },
413 expected: testInstantExecuteExpected{
414 panic: true,
415 },
416 },
417 "Non-member": {
418 input: testInstantExecuteInput{
419 proposalReq: tdao.mockProposalRequest,
420 executor: dave,
421 },
422 expected: testInstantExecuteExpected{
423 panic: true,
424 },
425 },
426 }
427
428 for testName, test := range tests {
429 t.Run(testName, func(t *testing.T) {
430 if test.expected.panic {
431 defer func() {
432 if r := recover(); r == nil {
433 t.Errorf("Expected panic, got none")
434 }
435 }()
436 }
437
438 func() {
439 testing.SetOriginCaller(test.input.executor)
440 }()
441 tdao.dao.Propose(test.input.proposalReq)
442 })
443 }
444}
445
446func TestGetMembers(t *testing.T) {
447 members := []Member{
448 {alice.String(), []string{"admin"}},
449 {bob.String(), []string{}},
450 {carol.String(), []string{}},
451 }
452 tdao := newTestingDAO(t, 0.6, members)
453
454 expectedMembers := []string{alice.String(), bob.String(), carol.String()}
455 m := tdao.privdao.Members.GetMembers()
456 if len(m) != len(expectedMembers) {
457 t.Errorf("Expected %d members, got %d", len(expectedMembers), len(m))
458 }
459
460 for _, eMember := range expectedMembers {
461 if !tdao.privdao.Members.IsMember(eMember) {
462 t.Errorf("Expected member %s to be a member", eMember)
463 }
464 }
465}
466
467func TestAddMemberProposal(t *testing.T) {
468 members := []Member{
469 {alice.String(), []string{"admin"}},
470 }
471 tdao := newTestingDAO(t, 0.2, members)
472
473 if tdao.privdao.Members.IsMember(bob.String()) {
474 t.Errorf("Expected member %s to not be a member", bob.String())
475 }
476
477 daokit.InstantExecute(tdao.dao, daokit.ProposalRequest{
478 Title: "My Proposal",
479 Description: "My Proposal Description",
480 Action: NewAddMemberAction(&ActionAddMember{
481 Address: bob,
482 Roles: []string{"admin"},
483 }),
484 })
485
486 if !tdao.privdao.Members.IsMember(bob.String()) {
487 t.Errorf("Expected member %s to be a member", bob.String())
488 }
489
490 if !tdao.privdao.Members.HasRole(bob.String(), "admin") {
491 t.Errorf("Expected member %s to have role 'admin'", bob.String())
492 }
493
494 defer func() {
495 if r := recover(); r == nil {
496 t.Errorf("Expected panic, got none")
497 }
498 }()
499
500 proposalWithUnknownRole := daokit.ProposalRequest{
501 Title: "My Proposal",
502 Description: "My Proposal Description",
503 Action: NewAddMemberAction(&ActionAddMember{
504 Address: bob,
505 Roles: []string{"unknown"},
506 }),
507 }
508 daokit.InstantExecute(tdao.dao, proposalWithUnknownRole)
509}
510
511func TestRemoveMemberProposal(t *testing.T) {
512 members := []Member{
513 {alice.String(), []string{"admin"}},
514 {bob.String(), []string{"admin"}},
515 }
516 tdao := newTestingDAO(t, 0.2, members)
517
518 if !tdao.privdao.Members.IsMember(bob.String()) {
519 t.Errorf("Expected member %s to be a member", bob.String())
520 }
521
522 if !tdao.privdao.Members.HasRole(bob.String(), "admin") {
523 t.Errorf("Expected member %s to have role 'admin'", bob.String())
524 }
525 daokit.InstantExecute(tdao.dao, daokit.ProposalRequest{
526 Title: "My Proposal",
527 Description: "My Proposal Description",
528 Action: NewRemoveMemberAction(bob),
529 })
530
531 if tdao.privdao.Members.IsMember(bob.String()) {
532 t.Errorf("Expected user %s to not be a member", bob.String())
533 }
534
535 if tdao.privdao.Members.HasRole(bob.String(), "admin") {
536 t.Errorf("Expected user %s to not have role 'admin'", bob.String())
537 }
538}
539
540func TestAddRoleToUserProposal(t *testing.T) {
541 members := []Member{
542 {alice.String(), []string{"admin"}},
543 {bob.String(), []string{}},
544 }
545 tdao := newTestingDAO(t, 0.2, members)
546
547 if tdao.privdao.Members.HasRole(bob.String(), "admin") {
548 t.Errorf("Expected member %s to not have role 'admin'", bob.String())
549 }
550
551 daokit.InstantExecute(tdao.dao, daokit.ProposalRequest{
552 Title: "My Proposal",
553 Description: "My Proposal Description",
554 Action: NewAssignRoleAction(&ActionAssignRole{Address: bob, Role: "admin"}),
555 })
556 if !tdao.privdao.Members.HasRole(bob.String(), "admin") {
557 t.Errorf("Expected member %s to have role 'admin'", bob.String())
558 }
559
560 defer func() {
561 // FIXME: this will pass if any other steps panics
562 if r := recover(); r == nil {
563 t.Errorf("Expected panic, got none")
564 }
565 }()
566
567 daokit.InstantExecute(tdao.dao, daokit.ProposalRequest{
568 Title: "My Proposal",
569 Description: "My Proposal Description",
570 Action: NewAssignRoleAction(&ActionAssignRole{Address: alice, Role: "unknown"}),
571 })
572
573 defer func() {
574 if r := recover(); r == nil {
575 t.Errorf("Expected panic, got none")
576 }
577 }()
578
579 daokit.InstantExecute(tdao.dao, daokit.ProposalRequest{
580 Title: "My Proposal",
581 Description: "My Proposal Description",
582 Action: NewAssignRoleAction(&ActionAssignRole{Address: carol, Role: "admin"}),
583 })
584
585 defer func() {
586 if r := recover(); r == nil {
587 t.Errorf("Expected panic, got none")
588 }
589 }()
590
591 daokit.InstantExecute(tdao.dao, daokit.ProposalRequest{
592 Title: "My Proposal",
593 Description: "My Proposal Description",
594 Action: NewAssignRoleAction(&ActionAssignRole{Address: bob, Role: "admin"}),
595 })
596}
597
598func TestRemoveRoleFromUserProposal(t *testing.T) {
599 members := []Member{
600 {
601 alice.String(),
602 []string{"admin"},
603 },
604 {
605 bob.String(),
606 []string{"admin"},
607 },
608 }
609 tdao := newTestingDAO(t, 0.2, members)
610
611 if !tdao.privdao.Members.HasRole(bob.String(), "admin") {
612 t.Errorf("Expected member %s to have role 'admin'", bob.String())
613 }
614 daokit.InstantExecute(tdao.dao, daokit.ProposalRequest{
615 Title: "My Proposal",
616 Description: "My Proposal Description",
617 Action: NewUnassignRoleAction(&ActionUnassignRole{Address: bob, Role: "admin"}),
618 })
619
620 if tdao.privdao.Members.HasRole(bob.String(), "admin") {
621 t.Errorf("Expected member %s to not have role 'admin'", bob.String())
622 }
623
624 defer func() {
625 if r := recover(); r == nil {
626 t.Errorf("Expected panic, got none")
627 }
628 }()
629
630 proposalWithUnknowkRole := daokit.ProposalRequest{
631 Title: "My Proposal",
632 Description: "My Proposal Description",
633 Action: NewUnassignRoleAction(&ActionUnassignRole{Address: alice, Role: "unknown"}),
634 }
635 testing.SetOriginCaller(alice)
636 daokit.InstantExecute(tdao.dao, proposalWithUnknowkRole)
637
638 defer func() {
639 if r := recover(); r == nil {
640 t.Errorf("Expected panic, got none")
641 }
642 }()
643
644 proposalWithNonMember := daokit.ProposalRequest{
645 Title: "My Proposal",
646 Description: "My Proposal Description",
647 Action: NewUnassignRoleAction(&ActionUnassignRole{Address: carol, Role: "admin"}),
648 }
649 testing.SetOriginCaller(alice)
650 daokit.InstantExecute(tdao.dao, proposalWithNonMember)
651
652 defer func() {
653 if r := recover(); r == nil {
654 t.Errorf("Expected panic, got none")
655 }
656 }()
657
658 proposalWithNonRole := daokit.ProposalRequest{
659 Title: "My Proposal",
660 Description: "My Proposal Description",
661 Action: NewUnassignRoleAction(&ActionUnassignRole{Address: bob, Role: "admin"}),
662 }
663 testing.SetOriginCaller(alice)
664 daokit.InstantExecute(tdao.dao, proposalWithNonRole)
665}
666
667type testingDAOContext struct {
668 dao daokit.DAO
669 privdao *DAOPrivate
670 mockHandlerCalled *bool
671 mockHandler daokit.ActionHandler
672 mockProposalRequest daokit.ProposalRequest
673 unknownProposalRequest daokit.ProposalRequest
674}
675
676func newTestingDAO(t *testing.T, threshold float64, members []Member) *testingDAOContext {
677 roles := []RoleInfo{{Name: "admin", Description: "Admin is the superuser"}}
678 membersStore := NewMembersStore(roles, members)
679 initialCondition := daocond.MembersThreshold(threshold, membersStore.IsMember, membersStore.MembersCount)
680
681 conf := &Config{
682 Name: "My DAO",
683 Description: "My DAO Description",
684 Members: membersStore,
685 InitialCondition: initialCondition,
686 GetProfileString: func(addr address, field, def string) string { return "" },
687 }
688
689 daoRealm := testing.NewCodeRealm("gno.land/r/testing/daorealm")
690 testing.SetOriginCaller(alice)
691 testing.SetRealm(daoRealm)
692 pubdao, dao := New(conf)
693
694 mockHandlerCalled := false
695 mockHandler := daokit.NewActionHandler("valid", func(_ interface{}) { mockHandlerCalled = true })
696 dao.Core.Resources.Set(&daokit.Resource{
697 Handler: mockHandler,
698 Condition: daocond.MembersThreshold(0.2, dao.Members.IsMember, dao.Members.MembersCount),
699 })
700
701 mockProposalRequest := daokit.ProposalRequest{
702 Title: "My Proposal",
703 Description: "My Proposal Description",
704 Action: daokit.NewAction("valid", nil),
705 }
706
707 unknownProposalRequest := daokit.ProposalRequest{
708 Title: "My Proposal",
709 Description: "My Proposal Description",
710 Action: daokit.NewAction("unknown", nil),
711 }
712
713 return &testingDAOContext{
714 dao: pubdao,
715 privdao: dao,
716 mockHandlerCalled: &mockHandlerCalled,
717 mockHandler: mockHandler,
718 mockProposalRequest: mockProposalRequest,
719 unknownProposalRequest: unknownProposalRequest,
720 }
721}