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}