Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}