github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/sharing/group_test.go (about) 1 package sharing 2 3 import ( 4 "strings" 5 "testing" 6 "time" 7 8 "github.com/cozy/cozy-stack/model/contact" 9 "github.com/cozy/cozy-stack/model/instance" 10 "github.com/cozy/cozy-stack/model/instance/lifecycle" 11 "github.com/cozy/cozy-stack/model/job" 12 "github.com/cozy/cozy-stack/pkg/config/config" 13 "github.com/cozy/cozy-stack/pkg/consts" 14 "github.com/cozy/cozy-stack/pkg/couchdb" 15 "github.com/cozy/cozy-stack/tests/testutils" 16 "github.com/stretchr/testify/assert" 17 "github.com/stretchr/testify/require" 18 19 _ "github.com/cozy/cozy-stack/worker/mails" 20 ) 21 22 func TestGroups(t *testing.T) { 23 if testing.Short() { 24 t.Skip("an instance is required for this test: test skipped due to the use of --short flag") 25 } 26 27 config.UseTestFile(t) 28 testutils.NeedCouchdb(t) 29 setup := testutils.NewSetup(t, t.Name()) 30 inst := setup.GetTestInstance(&lifecycle.Options{ 31 Email: "alice@example.net", 32 PublicName: "Alice", 33 }) 34 35 t.Run("RevokeGroup", func(t *testing.T) { 36 now := time.Now() 37 friends := createGroup(t, inst, "Friends") 38 football := createGroup(t, inst, "Football") 39 bob := createContactInGroups(t, inst, "Bob", []string{friends.ID()}) 40 _ = createContactInGroups(t, inst, "Charlie", []string{friends.ID(), football.ID()}) 41 _ = createContactInGroups(t, inst, "Dave", []string{football.ID()}) 42 43 s := &Sharing{ 44 Active: true, 45 Owner: true, 46 Description: "Just testing groups", 47 Members: []Member{ 48 {Status: MemberStatusOwner, Name: "Alice", Email: "alice@cozy.tools"}, 49 }, 50 Rules: []Rule{ 51 { 52 Title: "Just testing groups", 53 DocType: "io.cozy.tests", 54 Values: []string{uuidv7()}, 55 }, 56 }, 57 CreatedAt: now, 58 UpdatedAt: now, 59 } 60 require.NoError(t, couchdb.CreateDoc(inst, s)) 61 require.NoError(t, s.AddContact(inst, bob.ID(), false)) 62 require.NoError(t, s.AddGroup(inst, friends.ID(), false)) 63 require.NoError(t, s.AddGroup(inst, football.ID(), false)) 64 65 require.Len(t, s.Members, 4) 66 require.Equal(t, s.Members[0].Name, "Alice") 67 require.Equal(t, s.Members[1].Name, "Bob") 68 assert.False(t, s.Members[1].OnlyInGroups) 69 assert.Equal(t, s.Members[1].Groups, []int{0}) 70 require.Equal(t, s.Members[2].Name, "Charlie") 71 assert.True(t, s.Members[2].OnlyInGroups) 72 assert.Equal(t, s.Members[2].Groups, []int{0, 1}) 73 require.Equal(t, s.Members[3].Name, "Dave") 74 assert.True(t, s.Members[3].OnlyInGroups) 75 assert.Equal(t, s.Members[3].Groups, []int{1}) 76 77 require.Len(t, s.Groups, 2) 78 require.Equal(t, s.Groups[0].Name, "Friends") 79 assert.False(t, s.Groups[0].Revoked) 80 require.Equal(t, s.Groups[1].Name, "Football") 81 assert.False(t, s.Groups[1].Revoked) 82 83 require.NoError(t, s.RevokeGroup(inst, 1)) // Revoke the football group 84 85 require.Len(t, s.Members, 4) 86 assert.NotEqual(t, s.Members[1].Status, MemberStatusRevoked) 87 assert.Equal(t, s.Members[1].Groups, []int{0}) 88 assert.NotEqual(t, s.Members[2].Status, MemberStatusRevoked) 89 assert.Equal(t, s.Members[2].Groups, []int{0}) 90 assert.Equal(t, s.Members[3].Status, MemberStatusRevoked) 91 assert.Empty(t, s.Members[3].Groups) 92 93 require.Len(t, s.Groups, 2) 94 assert.False(t, s.Groups[0].Revoked) 95 assert.True(t, s.Groups[1].Revoked) 96 97 require.NoError(t, s.RevokeGroup(inst, 0)) // Revoke the fiends group 98 99 require.Len(t, s.Members, 4) 100 assert.NotEqual(t, s.Members[1].Status, MemberStatusRevoked) 101 assert.Empty(t, s.Members[1].Groups) 102 assert.Equal(t, s.Members[2].Status, MemberStatusRevoked) 103 assert.Empty(t, s.Members[2].Groups) 104 assert.Equal(t, s.Members[3].Status, MemberStatusRevoked) 105 assert.Empty(t, s.Members[3].Groups) 106 107 require.Len(t, s.Groups, 2) 108 assert.True(t, s.Groups[0].Revoked) 109 assert.True(t, s.Groups[1].Revoked) 110 }) 111 112 t.Run("UpdateGroups", func(t *testing.T) { 113 now := time.Now() 114 friends := createGroup(t, inst, "Friends") 115 football := createGroup(t, inst, "Football") 116 _ = createContactInGroups(t, inst, "Bob", []string{friends.ID()}) 117 charlie := createContactInGroups(t, inst, "Charlie", []string{football.ID()}) 118 dave := createContactWithoutEmail(t, inst, "Dave", []string{football.ID()}) 119 120 s := &Sharing{ 121 Active: true, 122 Owner: true, 123 Description: "Just testing groups", 124 Members: []Member{ 125 {Status: MemberStatusOwner, Name: "Alice", Email: "alice@cozy.tools"}, 126 }, 127 Rules: []Rule{ 128 { 129 Title: "Just testing groups", 130 DocType: "io.cozy.tests", 131 Values: []string{uuidv7()}, 132 }, 133 }, 134 CreatedAt: now, 135 UpdatedAt: now, 136 } 137 require.NoError(t, s.AddGroup(inst, friends.ID(), false)) 138 require.NoError(t, s.AddGroup(inst, football.ID(), false)) 139 perms, err := s.Create(inst) 140 require.NoError(t, err) 141 require.NoError(t, s.SendInvitations(inst, perms)) 142 sid := s.ID() 143 144 require.Len(t, s.Members, 4) 145 require.Equal(t, s.Members[0].Name, "Alice") 146 require.Equal(t, s.Members[1].Name, "Bob") 147 assert.True(t, s.Members[1].OnlyInGroups) 148 assert.Equal(t, s.Members[1].Groups, []int{0}) 149 assert.Equal(t, s.Members[1].Status, MemberStatusPendingInvitation) 150 require.Equal(t, s.Members[2].Name, "Charlie") 151 assert.True(t, s.Members[2].OnlyInGroups) 152 assert.Equal(t, s.Members[2].Groups, []int{1}) 153 assert.Equal(t, s.Members[2].Status, MemberStatusPendingInvitation) 154 require.Equal(t, s.Members[3].Name, "Dave") 155 assert.True(t, s.Members[3].OnlyInGroups) 156 assert.Equal(t, s.Members[3].Groups, []int{1}) 157 assert.Equal(t, s.Members[3].Status, MemberStatusMailNotSent) 158 159 require.Len(t, s.Groups, 2) 160 require.Equal(t, s.Groups[0].Name, "Friends") 161 assert.False(t, s.Groups[0].Revoked) 162 require.Equal(t, s.Groups[1].Name, "Football") 163 assert.False(t, s.Groups[1].Revoked) 164 165 // Charlie is added to the friends group 166 msg1 := job.ShareGroupMessage{ 167 ContactID: charlie.ID(), 168 GroupsAdded: []string{friends.ID()}, 169 } 170 require.NoError(t, UpdateGroups(inst, msg1)) 171 172 s = &Sharing{} 173 require.NoError(t, couchdb.GetDoc(inst, consts.Sharings, sid, s)) 174 175 require.Len(t, s.Members, 4) 176 require.Equal(t, s.Members[0].Name, "Alice") 177 require.Equal(t, s.Members[1].Name, "Bob") 178 assert.True(t, s.Members[1].OnlyInGroups) 179 assert.Equal(t, s.Members[1].Groups, []int{0}) 180 require.Equal(t, s.Members[2].Name, "Charlie") 181 assert.True(t, s.Members[2].OnlyInGroups) 182 assert.Equal(t, s.Members[2].Groups, []int{0, 1}) 183 require.Equal(t, s.Members[3].Name, "Dave") 184 assert.True(t, s.Members[3].OnlyInGroups) 185 assert.Equal(t, s.Members[3].Groups, []int{1}) 186 187 // Charlie is removed of the football group 188 msg2 := job.ShareGroupMessage{ 189 ContactID: charlie.ID(), 190 GroupsRemoved: []string{football.ID()}, 191 } 192 require.NoError(t, UpdateGroups(inst, msg2)) 193 194 s = &Sharing{} 195 require.NoError(t, couchdb.GetDoc(inst, consts.Sharings, sid, s)) 196 197 require.Len(t, s.Members, 4) 198 require.Equal(t, s.Members[0].Name, "Alice") 199 require.Equal(t, s.Members[1].Name, "Bob") 200 assert.True(t, s.Members[1].OnlyInGroups) 201 assert.Equal(t, s.Members[1].Groups, []int{0}) 202 require.Equal(t, s.Members[2].Name, "Charlie") 203 assert.True(t, s.Members[2].OnlyInGroups) 204 assert.Equal(t, s.Members[2].Groups, []int{0}) 205 require.Equal(t, s.Members[3].Name, "Dave") 206 assert.True(t, s.Members[3].OnlyInGroups) 207 assert.Equal(t, s.Members[3].Groups, []int{1}) 208 209 // Email address is added for Dave, and an invitation can now be sent 210 addEmailToContact(t, inst, dave) 211 msg3 := job.ShareGroupMessage{ 212 ContactID: dave.ID(), 213 BecomeInvitable: true, 214 } 215 require.NoError(t, UpdateGroups(inst, msg3)) 216 217 s = &Sharing{} 218 require.NoError(t, couchdb.GetDoc(inst, consts.Sharings, sid, s)) 219 220 require.Len(t, s.Members, 4) 221 require.Equal(t, s.Members[0].Name, "Alice") 222 require.Equal(t, s.Members[1].Name, "Bob") 223 assert.True(t, s.Members[1].OnlyInGroups) 224 assert.Equal(t, s.Members[1].Groups, []int{0}) 225 assert.Equal(t, s.Members[1].Status, MemberStatusPendingInvitation) 226 require.Equal(t, s.Members[2].Name, "Charlie") 227 assert.True(t, s.Members[2].OnlyInGroups) 228 assert.Equal(t, s.Members[2].Groups, []int{0}) 229 assert.Equal(t, s.Members[1].Status, MemberStatusPendingInvitation) 230 require.Equal(t, s.Members[3].Name, "Dave") 231 assert.True(t, s.Members[3].OnlyInGroups) 232 assert.Equal(t, s.Members[3].Groups, []int{1}) 233 assert.Equal(t, s.Members[3].Status, MemberStatusPendingInvitation) 234 }) 235 } 236 237 func createGroup(t *testing.T, inst *instance.Instance, name string) *contact.Group { 238 t.Helper() 239 g := contact.NewGroup() 240 g.M["name"] = name 241 require.NoError(t, couchdb.CreateDoc(inst, g)) 242 return g 243 } 244 245 func createContactInGroups(t *testing.T, inst *instance.Instance, contactName string, groupIDs []string) *contact.Contact { 246 t.Helper() 247 email := strings.ToLower(contactName) + "@cozy.tools" 248 mail := map[string]interface{}{"address": email} 249 250 var groups []interface{} 251 for _, id := range groupIDs { 252 groups = append(groups, map[string]interface{}{ 253 "_id": id, 254 "_type": consts.Groups, 255 }) 256 } 257 258 c := contact.New() 259 c.M["fullname"] = contactName 260 c.M["email"] = []interface{}{mail} 261 c.M["relationships"] = map[string]interface{}{ 262 "groups": map[string]interface{}{"data": groups}, 263 } 264 require.NoError(t, couchdb.CreateDoc(inst, c)) 265 return c 266 } 267 268 func createContactWithoutEmail(t *testing.T, inst *instance.Instance, contactName string, groupIDs []string) *contact.Contact { 269 t.Helper() 270 271 var groups []interface{} 272 for _, id := range groupIDs { 273 groups = append(groups, map[string]interface{}{ 274 "_id": id, 275 "_type": consts.Groups, 276 }) 277 } 278 279 c := contact.New() 280 c.M["fullname"] = contactName 281 c.M["relationships"] = map[string]interface{}{ 282 "groups": map[string]interface{}{"data": groups}, 283 } 284 require.NoError(t, couchdb.CreateDoc(inst, c)) 285 return c 286 } 287 288 func addEmailToContact(t *testing.T, inst *instance.Instance, c *contact.Contact) { 289 t.Helper() 290 291 email := strings.ToLower(c.PrimaryName()) + "@cozy.tools" 292 mail := map[string]interface{}{"address": email} 293 c.M["email"] = []interface{}{mail} 294 require.NoError(t, couchdb.UpdateDoc(inst, c)) 295 }