Search Apps Documentation Source Content File Folder Download Copy Actions Download

govdao_test.gno

12.23 Kb · 332 lines
  1package impl
  2
  3import (
  4	"fmt"
  5	"strings"
  6	"testing"
  7
  8	"gno.land/p/nt/testutils/v0"
  9	"gno.land/p/nt/urequire/v0"
 10	"gno.land/r/gov/dao"
 11	"gno.land/r/gov/dao/v3/memberstore"
 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 init(cur realm) {
 21	loadMembers(cur)
 22
 23	dao.UpdateImpl(cross(cur), dao.NewUpdateRequest(govDAO, []string{"gno.land/r/gov/dao/v3/impl"}))
 24}
 25
 26var (
 27	m1    = testutils.TestAddress("m1")
 28	m11   = testutils.TestAddress("m1.1")
 29	m111  = testutils.TestAddress("m1.1.1")
 30	m1111 = testutils.TestAddress("m1.1.1.1")
 31	m2    = testutils.TestAddress("m2")
 32	m3    = testutils.TestAddress("m3")
 33	m4    = testutils.TestAddress("m4")
 34	m5    = testutils.TestAddress("m5")
 35	m6    = testutils.TestAddress("m6")
 36
 37	noMember = testutils.TestAddress("nm1")
 38)
 39
 40func loadMembers(cur realm) {
 41	// This is needed because state is saved between unit tests,
 42	// and we want to avoid having real members used on tests
 43	mstore := memberstore.Get(0, cur)
 44	mstore.DeleteAll()
 45
 46	mstore.SetTier(memberstore.T1)
 47	mstore.SetTier(memberstore.T2)
 48	mstore.SetTier(memberstore.T3)
 49
 50	mstore.SetMember(memberstore.T1, m1, memberByTier(memberstore.T1))
 51	mstore.SetMember(memberstore.T1, m11, memberByTier(memberstore.T1))
 52	mstore.SetMember(memberstore.T1, m111, memberByTier(memberstore.T1))
 53	mstore.SetMember(memberstore.T1, m1111, memberByTier(memberstore.T1))
 54
 55	mstore.SetMember(memberstore.T2, m2, memberByTier(memberstore.T2))
 56	mstore.SetMember(memberstore.T2, m3, memberByTier(memberstore.T2))
 57	mstore.SetMember(memberstore.T3, m4, memberByTier(memberstore.T3))
 58	mstore.SetMember(memberstore.T3, m5, memberByTier(memberstore.T3))
 59	mstore.SetMember(memberstore.T3, m6, memberByTier(memberstore.T3))
 60}
 61
 62func TestCreateProposalAndVote(cur realm, t *testing.T) {
 63	loadMembers(cur)
 64
 65	portfolio := "# This is my portfolio:\n\n- THINGS"
 66
 67	testing.SetOriginCaller(noMember)
 68	testing.SetRealm(testing.NewCodeRealm("gno.land/r/gov/dao/v3/impl"))
 69
 70	nm1 := testutils.TestAddress("nm1")
 71
 72	urequire.AbortsWithMessage(t, cur, "Only T1 and T2 members can be added by proposal. To add a T3 member use AddMember function directly.", func(cur realm) {
 73		dao.MustCreateProposal(cross(cur), NewAddMemberRequest(cur, nm1, memberstore.T3, portfolio))
 74	})
 75
 76	urequire.AbortsWithMessage(t, cur, "proposer is not a member", func(cur realm) {
 77		dao.MustCreateProposal(cross(cur), NewAddMemberRequest(cur, nm1, memberstore.T2, portfolio))
 78	})
 79
 80	testing.SetOriginCaller(m1)
 81	testing.SetRealm(testing.NewCodeRealm("gno.land/r/gov/dao/v3/impl"))
 82
 83	proposalRequest := NewAddMemberRequest(cur, nm1, memberstore.T2, portfolio)
 84
 85	testing.SetOriginCaller(m1)
 86	testing.SetRealm(testing.NewUserRealm(m1))
 87	pid := dao.MustCreateProposal(cross(cur), proposalRequest)
 88	urequire.Equal(t, int(pid), 0)
 89
 90	// m1 votes yes because that member is interested on it
 91	dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(0)))
 92
 93	testing.SetOriginCaller(m11)
 94
 95	dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))
 96
 97	testing.SetOriginCaller(m2)
 98
 99	dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))
100
101	testing.SetOriginCaller(m3)
102
103	dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))
104
105	testing.SetOriginCaller(m4)
106
107	urequire.AbortsWithMessage(t, cur, "member on specified tier is not allowed to vote on this proposal", func() {
108		dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))
109	})
110
111	testing.SetOriginCaller(m111)
112
113	// Same effect as:
114	// dao.MustVoteOnProposal(dao.VoteRequest{
115	// 	Option:     dao.NoVote,
116	// 	ProposalID: dao.ProposalID(0),
117	// })
118	dao.MustVoteOnProposalSimple(cross(cur), 0, "NO")
119
120	urequire.Equal(t, true, strings.Contains(dao.Render(cross(cur), ""), "Prop #0 - New T2 Member Proposal"))
121	// urequire.Equal(t, true, strings.Contains(dao.Render(cross(cur), ""), "Author: "+m1.String()))
122
123	urequire.AbortsWithMessage(t, cur, "proposal didn't reach supermajority yet: 66.66", func() {
124		dao.ExecuteProposal(cross(cur), dao.ProposalID(0))
125	})
126
127	testing.SetOriginCaller(m1111)
128	dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.NoVote, dao.ProposalID(0)))
129
130	accepted := dao.ExecuteProposal(cross(cur), dao.ProposalID(0))
131	urequire.Equal(t, false, accepted)
132
133	urequire.Equal(t, true, contains(dao.Render(cross(cur), "0"), "**PROPOSAL HAS BEEN DENIED**"))
134	urequire.Equal(t, true, contains(dao.Render(cross(cur), "0"), "NO PERCENT: 81.25%"))
135}
136
137func TestExecutorCreationRealm(cur realm, t *testing.T) {
138	loadMembers(cur)
139
140	// Test that executor creation realm is captured correctly
141	testing.SetOriginCaller(m1)
142	testing.SetRealm(testing.NewCodeRealm("gno.land/r/template/contract"))
143
144	// Create executor in the template contract realm
145	executor := dao.NewSimpleExecutor(0, cur, func(realm) error { return nil }, "Test executor from template")
146
147	proposalRequest := dao.NewProposalRequest(
148		"Test Proposal",
149		"This proposal tests executor creation realm tracking",
150		executor,
151	)
152
153	// Create proposal from user realm (user can call DAO directly)
154	testing.SetRealm(testing.NewUserRealm(m1))
155	pid := dao.MustCreateProposal(cross(cur), proposalRequest)
156
157	// Get the proposal
158	prop := dao.MustGetProposal(pid)
159
160	// Verify the author is m1
161	urequire.Equal(t, m1, prop.Author())
162
163	// Verify the executor creation realm is captured correctly
164	urequire.Equal(t, "gno.land/r/template/contract", prop.ExecutorCreationRealm())
165
166	// Check that it's displayed in the individual proposal render output
167	individualRendered := dao.Render(cross(cur), pid.String())
168	urequire.Equal(t, true, contains(individualRendered, "Executor created in: gno.land/r/template/contract"))
169	urequire.Equal(t, true, contains(individualRendered, "Test executor from template"))
170
171	// Also verify the main content is there
172	urequire.Equal(t, true, contains(individualRendered, "Test Proposal"))
173	urequire.Equal(t, true, contains(individualRendered, "This proposal tests executor creation realm tracking"))
174}
175
176func TestProposalPagination(cur realm, t *testing.T) {
177	loadMembers(cur)
178	portfolio := "### This is my portfolio:\n\n- THINGS"
179
180	testing.SetOriginCaller(m1)
181	testing.SetRealm(testing.NewCodeRealm("gno.land/r/gov/dao/v3/impl"))
182
183	nm1 := testutils.TestAddress("nm1")
184
185	var pid dao.ProposalID
186
187	proposalRequest := NewAddMemberRequest(cur, nm1, memberstore.T2, portfolio)
188
189	testing.SetOriginCaller(m1)
190	testing.SetRealm(testing.NewUserRealm(m1))
191	pid = dao.MustCreateProposal(cross(cur), proposalRequest)
192
193	// TODO: tests keep the same vm state: https://github.com/gnolang/gno/issues/1982
194	urequire.Equal(t, 2, int(pid))
195
196	testing.SetRealm(testing.NewUserRealm(m1))
197	pid = dao.MustCreateProposal(cross(cur), proposalRequest)
198	urequire.Equal(t, 3, int(pid))
199
200	testing.SetRealm(testing.NewUserRealm(m1))
201	pid = dao.MustCreateProposal(cross(cur), proposalRequest)
202	urequire.Equal(t, 4, int(pid))
203
204	testing.SetRealm(testing.NewUserRealm(m1))
205	pid = dao.MustCreateProposal(cross(cur), proposalRequest)
206	urequire.Equal(t, 5, int(pid))
207
208	testing.SetRealm(testing.NewUserRealm(m1))
209	pid = dao.MustCreateProposal(cross(cur), proposalRequest)
210	urequire.Equal(t, 6, int(pid))
211
212	testing.SetRealm(testing.NewUserRealm(m1))
213	pid = dao.MustCreateProposal(cross(cur), proposalRequest)
214	urequire.Equal(t, 7, int(pid))
215
216	fmt.Println(dao.Render(cross(cur), ""))
217	urequire.Equal(t, true, contains(dao.Render(cross(cur), ""), "### [Prop #7 - New T2 Member Proposal](/r/gov/dao:7)"))
218	urequire.Equal(t, true, contains(dao.Render(cross(cur), ""), "### [Prop #6 - New T2 Member Proposal](/r/gov/dao:6)"))
219	urequire.Equal(t, true, contains(dao.Render(cross(cur), ""), "### [Prop #5 - New T2 Member Proposal](/r/gov/dao:5)"))
220	urequire.Equal(t, true, contains(dao.Render(cross(cur), ""), "### [Prop #4 - New T2 Member Proposal](/r/gov/dao:4)"))
221	urequire.Equal(t, true, contains(dao.Render(cross(cur), ""), "### [Prop #3 - New T2 Member Proposal](/r/gov/dao:3)"))
222
223	urequire.Equal(t, true, contains(dao.Render(cross(cur), "?page=2"), "### [Prop #2 - New T2 Member Proposal](/r/gov/dao:2)"))
224	urequire.Equal(t, true, contains(dao.Render(cross(cur), "?page=2"), "### [Prop #1 - Test Proposal](/r/gov/dao:1)"))
225	urequire.Equal(t, true, contains(dao.Render(cross(cur), "?page=2"), "### [Prop #0 - New T2 Member Proposal](/r/gov/dao:0)"))
226}
227
228func TestUpgradeDaoImplementation(cur realm, t *testing.T) {
229	loadMembers(cur)
230
231	testing.SetOriginCaller(noMember)
232	testing.SetRealm(testing.NewCodeRealm("gno.land/r/gov/dao/v3/impl"))
233
234	urequire.PanicsWithMessage(t, cur, "proposer is not a member", func() {
235		NewUpgradeDaoImplRequest(cur, govDAO, "gno.land/r/gov/dao/v4/impl", "Something happened and we have to fix it.")
236	})
237
238	testing.SetOriginCaller(m1)
239	testing.SetRealm(testing.NewCodeRealm("gno.land/r/gov/dao/v3/impl"))
240
241	preq := NewUpgradeDaoImplRequest(cur, govDAO, "gno.land/r/gov/dao/v4/impl", "Something happened and we have to fix it.")
242
243	testing.SetOriginCaller(m1)
244	testing.SetRealm(testing.NewUserRealm(m1))
245	pid := dao.MustCreateProposal(cross(cur), preq)
246	urequire.Equal(t, int(pid), 8)
247
248	// m1 votes yes because that member is interested on it
249	dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))
250
251	testing.SetOriginCaller(m11)
252
253	dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))
254
255	testing.SetOriginCaller(m2)
256
257	dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))
258
259	testing.SetOriginCaller(m3)
260
261	dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, dao.ProposalID(pid)))
262
263	testing.SetOriginCaller(m111)
264
265	// Same effect as:
266	// dao.MustVoteOnProposal(dao.VoteRequest{
267	// 	Option:     dao.YesVote,
268	// 	ProposalID: dao.ProposalID(pid),
269	// })
270	dao.MustVoteOnProposalSimple(cross(cur), int64(pid), "YES")
271
272	urequire.Equal(t, true, contains(dao.Render(cross(cur), "8"), "**Proposal is open for votes**"))
273	urequire.Equal(t, true, contains(dao.Render(cross(cur), "8"), "68.42105263157895%"))
274	urequire.Equal(t, true, contains(dao.Render(cross(cur), "8"), "0%"))
275
276	accepted := dao.ExecuteProposal(cross(cur), dao.ProposalID(pid))
277	urequire.Equal(t, true, accepted)
278	urequire.Equal(t, true, contains(dao.Render(cross(cur), "8"), "**PROPOSAL HAS BEEN ACCEPTED**"))
279	urequire.Equal(t, true, contains(dao.Render(cross(cur), "8"), "YES PERCENT: 68.42105263157895%"))
280}
281
282func TestAbstainVote(cur realm, t *testing.T) {
283	loadMembers(cur)
284
285	portfolio := "# This is my portfolio:\n\n- THINGS"
286
287	testing.SetOriginCaller(m1)
288	testing.SetRealm(testing.NewCodeRealm("gno.land/r/gov/dao/v3/impl"))
289
290	nm1 := testutils.TestAddress("nm1")
291	proposalRequest := NewAddMemberRequest(cur, nm1, memberstore.T2, portfolio)
292
293	testing.SetOriginCaller(m1)
294	testing.SetRealm(testing.NewUserRealm(m1))
295	pid := dao.MustCreateProposal(cross(cur), proposalRequest)
296
297	// m1 votes abstain
298	dao.MustVoteOnProposalSimple(cross(cur), int64(pid), "ABSTAIN")
299
300	// Other members vote YES to reach supermajority
301	testing.SetOriginCaller(m11)
302	dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))
303
304	testing.SetOriginCaller(m111)
305	dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))
306
307	testing.SetOriginCaller(m1111)
308	dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))
309
310	testing.SetOriginCaller(m2)
311	dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))
312
313	testing.SetOriginCaller(m3)
314	dao.MustVoteOnProposal(cross(cur), dao.NewVoteRequest(dao.YesVote, pid))
315
316	// Verify render shows correct percentages
317	urequire.Equal(t, true, contains(dao.Render(cross(cur), pid.String()), "YES PERCENT: 81.25%"))
318	urequire.Equal(t, true, contains(dao.Render(cross(cur), pid.String()), "NO PERCENT: 0%"))
319	urequire.Equal(t, true, contains(dao.Render(cross(cur), pid.String()), "ABSTAIN PERCENT: 18.75%"))
320
321	// Supermajority reached despite one abstain voter
322	accepted := dao.ExecuteProposal(cross(cur), pid)
323	urequire.Equal(t, true, accepted)
324	urequire.Equal(t, true, contains(dao.Render(cross(cur), pid.String()), "**PROPOSAL HAS BEEN ACCEPTED**"))
325
326	// Abstain voter appears in vote list
327	urequire.Equal(t, true, contains(dao.Render(cross(cur), fmt.Sprintf("%v/votes", int64(pid))), "ABSTAIN"))
328}
329
330func contains(s, substr string) bool {
331	return strings.Index(s, substr) >= 0
332}