daocond_test.gno
6.01 Kb ยท 269 lines
1package daocond_test
2
3import (
4 "errors"
5 "testing"
6
7 "gno.land/p/nt/urequire"
8 "gno.land/p/samcrew/daocond"
9)
10
11func TestCondition(t *testing.T) {
12 dao := newMockDAO()
13
14 // leaf conditions
15 membersMajority := daocond.MembersThreshold(0.6, dao.isMember, dao.membersCount)
16 publicRelationships := daocond.RoleCount(1, "public-relationships", dao.hasRole)
17 financeOfficer := daocond.RoleCount(1, "finance-officer", dao.hasRole)
18
19 urequire.Equal(t, "60% of members", membersMajority.Render())
20 urequire.Equal(t, "1 public-relationships", publicRelationships.Render())
21 urequire.Equal(t, "1 finance-officer", financeOfficer.Render())
22
23 // ressource expressions
24 ressources := map[string]daocond.Condition{
25 "social.post": daocond.And(publicRelationships, membersMajority),
26 "finance.invest": daocond.Or(financeOfficer, membersMajority),
27 }
28
29 urequire.Equal(t, "[1 public-relationships AND 60% of members]", ressources["social.post"].Render())
30 urequire.Equal(t, "[1 finance-officer OR 60% of members]", ressources["finance.invest"].Render())
31}
32
33func TestEval(t *testing.T) {
34 setups := []struct {
35 name string
36 setup func(dao *mockDAO)
37 }{
38 {name: "basic", setup: func(dao *mockDAO) {
39 membersMajority := daocond.MembersThreshold(0.6, dao.isMember, dao.membersCount)
40 publicRelationships := daocond.RoleCount(1, "public-relationships", dao.hasRole)
41 financeOfficer := daocond.RoleCount(1, "finance-officer", dao.hasRole)
42 dao.resources = map[string]daocond.Condition{
43 "social.post": daocond.And(publicRelationships, membersMajority),
44 "finance.invest": daocond.Or(financeOfficer, membersMajority),
45 }
46 }},
47 }
48
49 cases := []struct {
50 name string
51 resource string
52 phases []testPhase
53 }{
54 {
55 name: "post with public-relationships",
56 resource: "social.post",
57 phases: []testPhase{{
58 votes: map[string]daocond.Vote{
59 "alice": "yes",
60 "bob": "yes",
61 "eve": "no",
62 },
63 result: true,
64 }},
65 },
66 {
67 name: "post without public-relationships",
68 resource: "social.post",
69 phases: []testPhase{{
70 votes: map[string]daocond.Vote{
71 "alice": "yes",
72 "bob": "no",
73 "eve": "yes",
74 },
75 result: false,
76 }},
77 },
78 {
79 name: "post after public-relationships changes",
80 resource: "social.post",
81 phases: []testPhase{
82 {
83 votes: map[string]daocond.Vote{
84 "alice": "yes",
85 "bob": "yes",
86 "eve": "no",
87 },
88 result: true,
89 },
90 {
91 changes: func(dao *mockDAO) {
92 dao.unassignRole("bob", "public-relationships")
93 },
94 result: false,
95 },
96 {
97 changes: func(dao *mockDAO) {
98 dao.assignRole("alice", "public-relationships")
99 },
100 result: true,
101 },
102 },
103 },
104 {
105 name: "post public-relationships alone",
106 resource: "social.post",
107 phases: []testPhase{{
108 votes: map[string]daocond.Vote{
109 "alice": "no",
110 "bob": "yes",
111 "eve": "no",
112 },
113 result: false,
114 }},
115 },
116 {
117 name: "invest with finance officer",
118 resource: "finance.invest",
119 phases: []testPhase{{
120 votes: map[string]daocond.Vote{
121 "alice": "yes",
122 "bob": "no",
123 "eve": "no",
124 },
125 result: true,
126 }},
127 },
128 {
129 name: "invest without finance officer",
130 resource: "finance.invest",
131 phases: []testPhase{{
132 votes: map[string]daocond.Vote{
133 "alice": "no",
134 "bob": "yes",
135 "eve": "yes",
136 },
137 result: true,
138 }},
139 },
140 {
141 name: "invest alone",
142 resource: "finance.invest",
143 phases: []testPhase{{
144 votes: map[string]daocond.Vote{
145 "alice": "no",
146 "bob": "no",
147 "eve": "yes",
148 },
149 result: false,
150 }},
151 },
152 }
153
154 for _, tc := range cases {
155 for _, s := range setups {
156 t.Run(tc.name+" "+s.name, func(t *testing.T) {
157 dao := newMockDAO()
158 s.setup(dao)
159
160 resource, ok := dao.resources[tc.resource]
161 urequire.True(t, ok)
162
163 ballot := daocond.NewBallot()
164 for _, phase := range tc.phases {
165 if phase.changes != nil {
166 phase.changes(dao)
167 }
168 if phase.votes != nil {
169 for memberId, vote := range phase.votes {
170 ballot.Vote(memberId, vote)
171 }
172 }
173 result := resource.Eval(ballot)
174 if phase.result != result {
175 println("State:", resource.RenderWithVotes(ballot))
176 }
177 urequire.Equal(t, phase.result, result)
178 }
179 })
180 }
181 }
182}
183
184type testPhase struct {
185 changes func(dao *mockDAO)
186 votes map[string]daocond.Vote
187 result bool
188}
189
190type mockDAO struct {
191 members map[string][]string
192 roles map[string][]string
193 resources map[string]daocond.Condition
194}
195
196func newMockDAO() *mockDAO {
197 return &mockDAO{
198 members: map[string][]string{
199 "alice": []string{"finance-officer"},
200 "bob": []string{"public-relationships"},
201 "eve": []string{},
202 },
203 roles: map[string][]string{
204 "finance-officer": []string{"alice"},
205 "public-relationships": []string{"bob"},
206 }, // roles to users
207 resources: make(map[string]daocond.Condition),
208 }
209}
210
211func (m *mockDAO) assignRole(userId string, role string) {
212 roles, ok := m.members[userId]
213 if !ok {
214 panic(errors.New("unknown member"))
215 }
216 m.members[userId], ok = strsadd(roles, role)
217}
218
219func (m *mockDAO) unassignRole(userId string, role string) {
220 roles, ok := m.members[userId]
221 if !ok {
222 panic(errors.New("unknown member"))
223 }
224 m.members[userId], ok = strsrm(roles, role)
225}
226
227func (m *mockDAO) isMember(memberId string) bool {
228 _, ok := m.members[memberId]
229 return ok
230}
231
232func (m *mockDAO) membersCount() uint64 {
233 return uint64(len(m.members))
234}
235
236func (m *mockDAO) hasRole(memberId string, role string) bool {
237 roles, ok := m.members[memberId]
238 if !ok {
239 return false
240 }
241 for _, memberRole := range roles {
242 if memberRole == role {
243 return true
244 }
245 }
246 return false
247}
248
249func strsrm(strs []string, val string) ([]string, bool) {
250 removed := false
251 res := []string{}
252 for _, str := range strs {
253 if str == val {
254 removed = true
255 continue
256 }
257 res = append(res, str)
258 }
259 return res, removed
260}
261
262func strsadd(strs []string, val string) ([]string, bool) {
263 for _, str := range strs {
264 if str == val {
265 return strs, false
266 }
267 }
268 return append(strs, val), true
269}