diff --git a/api4/channel.go b/api4/channel.go index c7be76b059..dcf6d5b503 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -18,6 +18,7 @@ func (api *API) InitChannel() { api.BaseRoutes.Channels.Handle("", api.ApiSessionRequired(createChannel)).Methods("POST") api.BaseRoutes.Channels.Handle("/direct", api.ApiSessionRequired(createDirectChannel)).Methods("POST") api.BaseRoutes.Channels.Handle("/search", api.ApiSessionRequired(searchAllChannels)).Methods("POST") + api.BaseRoutes.Channels.Handle("/group/search", api.ApiSessionRequired(searchGroupChannels)).Methods("POST") api.BaseRoutes.Channels.Handle("/group", api.ApiSessionRequired(createGroupChannel)).Methods("POST") api.BaseRoutes.Channels.Handle("/members/{user_id:[A-Za-z0-9]+}/view", api.ApiSessionRequired(viewChannel)).Methods("POST") api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/scheme", api.ApiSessionRequired(updateChannelScheme)).Methods("PUT") @@ -356,6 +357,22 @@ func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(sc.ToJson())) } +func searchGroupChannels(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.ChannelSearchFromJson(r.Body) + if props == nil { + c.SetInvalidParam("channel_search") + return + } + + groupChannels, err := c.App.SearchGroupChannels(c.App.Session.UserId, props.Term) + if err != nil { + c.Err = err + return + } + + w.Write([]byte(groupChannels.ToJson())) +} + func createGroupChannel(c *Context, w http.ResponseWriter, r *http.Request) { userIds := model.ArrayFromJson(r.Body) diff --git a/api4/channel_test.go b/api4/channel_test.go index 4e513934ae..6c8edc0e62 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -918,6 +918,64 @@ func TestSearchAllChannels(t *testing.T) { CheckForbiddenStatus(t, resp) } +func TestSearchGroupChannels(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + Client := th.Client + + u1 := th.CreateUserWithClient(th.SystemAdminClient) + + // Create a group channel in which base user belongs but not sysadmin + gc1, resp := th.Client.CreateGroupChannel([]string{th.BasicUser.Id, th.BasicUser2.Id, u1.Id}) + CheckNoError(t, resp) + defer th.Client.DeleteChannel(gc1.Id) + + gc2, resp := th.Client.CreateGroupChannel([]string{th.BasicUser.Id, th.BasicUser2.Id, th.SystemAdminUser.Id}) + CheckNoError(t, resp) + defer th.Client.DeleteChannel(gc2.Id) + + search := &model.ChannelSearch{Term: th.BasicUser2.Username} + + // sysadmin should only find gc2 as he doesn't belong to gc1 + channels, resp := th.SystemAdminClient.SearchGroupChannels(search) + CheckNoError(t, resp) + + assert.Len(t, channels, 1) + assert.Equal(t, channels[0].Id, gc2.Id) + + // basic user should find both + Client.Login(th.BasicUser.Username, th.BasicUser.Password) + channels, resp = Client.SearchGroupChannels(search) + CheckNoError(t, resp) + + assert.Len(t, channels, 2) + channelIds := []string{} + for _, c := range channels { + channelIds = append(channelIds, c.Id) + } + assert.ElementsMatch(t, channelIds, []string{gc1.Id, gc2.Id}) + + // searching for sysadmin, it should only find gc1 + search = &model.ChannelSearch{Term: th.SystemAdminUser.Username} + channels, resp = Client.SearchGroupChannels(search) + CheckNoError(t, resp) + + assert.Len(t, channels, 1) + assert.Equal(t, channels[0].Id, gc2.Id) + + // with an empty search, response should be empty + search = &model.ChannelSearch{Term: ""} + channels, resp = Client.SearchGroupChannels(search) + CheckNoError(t, resp) + + assert.Len(t, channels, 0) + + // search unprivileged, forbidden + th.Client.Logout() + _, resp = Client.SearchAllChannels(search) + CheckUnauthorizedStatus(t, resp) +} + func TestDeleteChannel(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() diff --git a/api4/user.go b/api4/user.go index d95a9b467b..dd73c8e7ea 100644 --- a/api4/user.go +++ b/api4/user.go @@ -4,6 +4,7 @@ package api4 import ( + "encoding/json" "fmt" "io" "io/ioutil" @@ -25,6 +26,7 @@ func (api *API) InitUser() { api.BaseRoutes.Users.Handle("/search", api.ApiSessionRequired(searchUsers)).Methods("POST") api.BaseRoutes.Users.Handle("/autocomplete", api.ApiSessionRequired(autocompleteUsers)).Methods("GET") api.BaseRoutes.Users.Handle("/stats", api.ApiSessionRequired(getTotalUsersStats)).Methods("GET") + api.BaseRoutes.Users.Handle("/group_channels", api.ApiSessionRequired(getUsersByGroupChannelIds)).Methods("POST") api.BaseRoutes.User.Handle("", api.ApiSessionRequired(getUser)).Methods("GET") api.BaseRoutes.User.Handle("/image/default", api.ApiSessionRequiredTrustRequester(getDefaultProfileImage)).Methods("GET") @@ -447,6 +449,24 @@ func getTotalUsersStats(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(stats.ToJson())) } +func getUsersByGroupChannelIds(c *Context, w http.ResponseWriter, r *http.Request) { + channelIds := model.ArrayFromJson(r.Body) + + if len(channelIds) == 0 { + c.SetInvalidParam("channel_ids") + return + } + + usersByChannelId, err := c.App.GetUsersByGroupChannelIds(channelIds, c.IsSystemAdmin()) + if err != nil { + c.Err = err + return + } + + b, _ := json.Marshal(usersByChannelId) + w.Write(b) +} + func getUsers(c *Context, w http.ResponseWriter, r *http.Request) { inTeamId := r.URL.Query().Get("in_team") notInTeamId := r.URL.Query().Get("not_in_team") diff --git a/api4/user_test.go b/api4/user_test.go index d77cd64605..323da9913f 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -1127,6 +1127,35 @@ func TestGetUsersByIds(t *testing.T) { CheckUnauthorizedStatus(t, resp) } +func TestGetUsersByGroupChannelIds(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + gc1, err := th.App.CreateGroupChannel([]string{th.BasicUser.Id, th.SystemAdminUser.Id, th.TeamAdminUser.Id}, th.BasicUser.Id) + require.Nil(t, err) + + usersByChannelId, resp := th.Client.GetUsersByGroupChannelIds([]string{gc1.Id}) + CheckNoError(t, resp) + + users, _ := usersByChannelId[gc1.Id] + userIds := []string{} + for _, user := range users { + userIds = append(userIds, user.Id) + } + + require.ElementsMatch(t, []string{th.SystemAdminUser.Id, th.TeamAdminUser.Id}, userIds) + + th.LoginBasic2() + usersByChannelId, resp = th.Client.GetUsersByGroupChannelIds([]string{gc1.Id}) + + _, ok := usersByChannelId[gc1.Id] + require.False(t, ok) + + th.Client.Logout() + _, resp = th.Client.GetUsersByGroupChannelIds([]string{gc1.Id}) + CheckUnauthorizedStatus(t, resp) +} + func TestGetUsersByUsernames(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() diff --git a/app/channel.go b/app/channel.go index f9a9878286..3b4c4d2ef1 100644 --- a/app/channel.go +++ b/app/channel.go @@ -1801,6 +1801,18 @@ func (a *App) SearchChannels(teamId string, term string) (*model.ChannelList, *m return a.Srv.Store.Channel().SearchInTeam(teamId, term, includeDeleted) } +func (a *App) SearchGroupChannels(userId, term string) (*model.ChannelList, *model.AppError) { + if term == "" { + return &model.ChannelList{}, nil + } + + channelList, err := a.Srv.Store.Channel().SearchGroupChannels(userId, term) + if err != nil { + return nil, err + } + return channelList, nil +} + func (a *App) SearchChannelsUserNotIn(teamId string, userId string, term string) (*model.ChannelList, *model.AppError) { term = strings.TrimSpace(term) return a.Srv.Store.Channel().SearchMore(userId, teamId, term) diff --git a/app/user.go b/app/user.go index 81eda4aa8c..fdb9332807 100644 --- a/app/user.go +++ b/app/user.go @@ -637,6 +637,18 @@ func (a *App) GetUsersByIds(userIds []string, asAdmin bool, viewRestrictions *mo return a.sanitizeProfiles(result.Data.([]*model.User), asAdmin), nil } +func (a *App) GetUsersByGroupChannelIds(channelIds []string, asAdmin bool) (map[string][]*model.User, *model.AppError) { + usersByChannelId, err := a.Srv.Store.User().GetProfileByGroupChannelIdsForUser(a.Session.UserId, channelIds) + if err != nil { + return nil, err + } + for channelId, userList := range usersByChannelId { + usersByChannelId[channelId] = a.sanitizeProfiles(userList, asAdmin) + } + + return usersByChannelId, nil +} + func (a *App) GetUsersByUsernames(usernames []string, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { result := <-a.Srv.Store.User().GetProfilesByUsernames(usernames, viewRestrictions) if result.Err != nil { diff --git a/i18n/en.json b/i18n/en.json index 80f0531394..dd068ffb3b 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -5658,6 +5658,10 @@ "id": "store.sql_channel.search.app_error", "translation": "We encountered an error searching channels" }, + { + "id": "store.sql_channel.search_group_channels.app_error", + "translation": "Unable to get the group channels for the given user and term" + }, { "id": "store.sql_channel.set_delete_at.commit_transaction.app_error", "translation": "Unable to commit transaction" @@ -6758,6 +6762,10 @@ "id": "store.sql_user.get_new_users.app_error", "translation": "We encountered an error while finding the new users" }, + { + "id": "store.sql_user.get_profile_by_group_channel_ids_for_user.app_error", + "translation": "We encountered an error while finding user profiles" + }, { "id": "store.sql_user.get_profiles.app_error", "translation": "We encountered an error while finding user profiles" diff --git a/model/client4.go b/model/client4.go index e1decede65..4d04c903cb 100644 --- a/model/client4.go +++ b/model/client4.go @@ -946,6 +946,20 @@ func (c *Client4) GetUsersByUsernames(usernames []string) ([]*User, *Response) { return UserListFromJson(r.Body), BuildResponse(r) } +// GetUsersByGroupChannelIds returns a map with channel ids as keys +// and a list of users as values based on the provided user ids. +func (c *Client4) GetUsersByGroupChannelIds(groupChannelIds []string) (map[string][]*User, *Response) { + r, err := c.DoApiPost(c.GetUsersRoute()+"/group_channels", ArrayToJson(groupChannelIds)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + + usersByChannelId := map[string][]*User{} + json.NewDecoder(r.Body).Decode(&usersByChannelId) + return usersByChannelId, BuildResponse(r) +} + // SearchUsers returns a list of users based on some search criteria. func (c *Client4) SearchUsers(search *UserSearch) ([]*User, *Response) { r, err := c.doApiPostBytes(c.GetUsersRoute()+"/search", search.ToJson()) @@ -2056,6 +2070,16 @@ func (c *Client4) SearchAllChannels(search *ChannelSearch) (*ChannelListWithTeam return ChannelListWithTeamDataFromJson(r.Body), BuildResponse(r) } +// SearchGroupChannels returns the group channels of the user whose members' usernames match the search term. +func (c *Client4) SearchGroupChannels(search *ChannelSearch) ([]*Channel, *Response) { + r, err := c.DoApiPost(c.GetChannelsRoute()+"/group/search", search.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ChannelSliceFromJson(r.Body), BuildResponse(r) +} + // DeleteChannel deletes channel based on the provided channel id string. func (c *Client4) DeleteChannel(channelId string) (bool, *Response) { r, err := c.DoApiDelete(c.GetChannelRoute(channelId)) diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index 5d194bf0c2..9d60c28636 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -1473,7 +1473,7 @@ func (s SqlChannelStore) GetMemberForPost(postId string, userId string) (*model. Schemes TeamScheme ON Teams.SchemeId = TeamScheme.Id WHERE ChannelMembers.UserId = :UserId - AND + AND Posts.Id = :PostId` if err := s.GetReplica().SelectOne(&dbMember, query, map[string]interface{}{"UserId": userId, "PostId": postId}); err != nil { return nil, model.NewAppError("SqlChannelStore.GetMemberForPost", "store.sql_channel.get_member_for_post.app_error", nil, "postId="+postId+", err="+err.Error(), http.StatusInternalServerError) @@ -2237,6 +2237,104 @@ func (s SqlChannelStore) performSearch(searchQuery string, term string, paramete return &channels, nil } +func (s SqlChannelStore) getSearchGroupChannelsQuery(userId, term string, isPostgreSQL bool) (string, map[string]interface{}) { + var query, baseLikeClause string + if isPostgreSQL { + baseLikeClause = "ARRAY_TO_STRING(ARRAY_AGG(u.Username), ', ') LIKE %s" + query = ` + SELECT + * + FROM + Channels + WHERE + Id IN ( + SELECT + cc.Id + FROM ( + SELECT + c.Id + FROM + Channels c + JOIN + ChannelMembers cm on c.Id = cm.ChannelId + JOIN + Users u on u.Id = cm.UserId + WHERE + c.Type = 'G' + AND + u.Id = :UserId + GROUP BY + c.Id + ) cc + JOIN + ChannelMembers cm on cc.Id = cm.ChannelId + JOIN + Users u on u.Id = cm.UserId + GROUP BY + cc.Id + HAVING + %s + LIMIT + ` + strconv.Itoa(model.CHANNEL_SEARCH_DEFAULT_LIMIT) + ` + )` + } else { + baseLikeClause = "GROUP_CONCAT(u.Username SEPARATOR ', ') LIKE %s" + query = ` + SELECT + cc.* + FROM ( + SELECT + c.* + FROM + Channels c + JOIN + ChannelMembers cm on c.Id = cm.ChannelId + JOIN + Users u on u.Id = cm.UserId + WHERE + c.Type = 'G' + AND + u.Id = :UserId + GROUP BY + c.Id + ) cc + JOIN + ChannelMembers cm on cc.Id = cm.ChannelId + JOIN + Users u on u.Id = cm.UserId + GROUP BY + cc.Id + HAVING + %s + LIMIT + ` + strconv.Itoa(model.CHANNEL_SEARCH_DEFAULT_LIMIT) + } + + var likeClauses []string + args := map[string]interface{}{"UserId": userId} + terms := strings.Split(strings.ToLower(strings.Trim(term, " ")), " ") + + for idx, term := range terms { + argName := fmt.Sprintf("Term%v", idx) + likeClauses = append(likeClauses, fmt.Sprintf(baseLikeClause, ":"+argName)) + args[argName] = "%" + term + "%" + } + + query = fmt.Sprintf(query, strings.Join(likeClauses, " AND ")) + return query, args +} + +func (s SqlChannelStore) SearchGroupChannels(userId, term string) (*model.ChannelList, *model.AppError) { + isPostgreSQL := s.DriverName() == model.DATABASE_DRIVER_POSTGRES + queryString, args := s.getSearchGroupChannelsQuery(userId, term, isPostgreSQL) + + var groupChannels model.ChannelList + if _, err := s.GetReplica().Select(&groupChannels, queryString, args); err != nil { + return nil, model.NewAppError("SqlChannelStore.SearchGroupChannels", "store.sql_channel.search_group_channels.app_error", nil, "userId="+userId+", term="+term+", err="+err.Error(), http.StatusInternalServerError) + } + return &groupChannels, nil +} + func (s SqlChannelStore) GetMembersByIds(channelId string, userIds []string) (*model.ChannelMembers, *model.AppError) { var dbMembers channelMemberWithSchemeRolesList props := make(map[string]interface{}) diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index c5665b3f46..1886d19919 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -20,10 +20,11 @@ import ( ) const ( - PROFILES_IN_CHANNEL_CACHE_SIZE = model.CHANNEL_CACHE_SIZE - PROFILES_IN_CHANNEL_CACHE_SEC = 900 // 15 mins - PROFILE_BY_IDS_CACHE_SIZE = model.SESSION_CACHE_SIZE - PROFILE_BY_IDS_CACHE_SEC = 900 // 15 mins + PROFILES_IN_CHANNEL_CACHE_SIZE = model.CHANNEL_CACHE_SIZE + PROFILES_IN_CHANNEL_CACHE_SEC = 900 // 15 mins + PROFILE_BY_IDS_CACHE_SIZE = model.SESSION_CACHE_SIZE + PROFILE_BY_IDS_CACHE_SEC = 900 // 15 mins + MAX_GROUP_CHANNELS_FOR_PROFILES = 50 ) var ( @@ -920,6 +921,60 @@ func (us SqlUserStore) GetProfileByIds(userIds []string, allowFromCache bool, vi }) } +type UserWithChannel struct { + model.User + ChannelId string +} + +func (us SqlUserStore) GetProfileByGroupChannelIdsForUser(userId string, channelIds []string) (map[string][]*model.User, *model.AppError) { + if len(channelIds) > MAX_GROUP_CHANNELS_FOR_PROFILES { + channelIds = channelIds[0:MAX_GROUP_CHANNELS_FOR_PROFILES] + } + + isMemberQuery := fmt.Sprintf(` + EXISTS( + SELECT + 1 + FROM + ChannelMembers + WHERE + UserId = '%s' + AND + ChannelId = cm.ChannelId + )`, userId) + + query := us.getQueryBuilder(). + Select("u.*, cm.ChannelId"). + From("Users u"). + Join("ChannelMembers cm ON u.Id = cm.UserId"). + Join("Channels c ON cm.ChannelId = c.Id"). + Where(sq.Eq{"c.Type": model.CHANNEL_GROUP, "cm.ChannelId": channelIds}). + Where(isMemberQuery). + Where(sq.NotEq{"u.Id": userId}). + OrderBy("u.Username ASC") + + queryString, args, err := query.ToSql() + if err != nil { + return nil, model.NewAppError("SqlUserStore.GetProfileByGroupChannelIdsForUser", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + usersWithChannel := []*UserWithChannel{} + if _, err := us.GetReplica().Select(&usersWithChannel, queryString, args...); err != nil { + return nil, model.NewAppError("SqlUserStore.GetProfileByGroupChannelIdsForUser", "store.sql_user.get_profile_by_group_channel_ids_for_user.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + usersByChannelId := map[string][]*model.User{} + for _, user := range usersWithChannel { + if val, ok := usersByChannelId[user.ChannelId]; ok { + usersByChannelId[user.ChannelId] = append(val, &user.User) + } else { + usersByChannelId[user.ChannelId] = []*model.User{&user.User} + } + } + + return usersByChannelId, nil +} + func (us SqlUserStore) GetSystemAdminProfiles() store.StoreChannel { return store.Do(func(result *store.StoreResult) { query := us.usersQuery. diff --git a/store/store.go b/store/store.go index dfd64092e9..83af4d0960 100644 --- a/store/store.go +++ b/store/store.go @@ -185,6 +185,7 @@ type ChannelStore interface { SearchAllChannels(term string, opts ChannelSearchOpts) StoreChannel SearchInTeam(teamId string, term string, includeDeleted bool) (*model.ChannelList, *model.AppError) SearchMore(userId string, teamId string, term string) (*model.ChannelList, *model.AppError) + SearchGroupChannels(userId, term string) (*model.ChannelList, *model.AppError) GetMembersByIds(channelId string, userIds []string) (*model.ChannelMembers, *model.AppError) AnalyticsDeletedTypeCount(teamId string, channelType string) (int64, *model.AppError) GetChannelUnread(channelId, userId string) (*model.ChannelUnread, *model.AppError) @@ -267,6 +268,7 @@ type UserStore interface { GetAllProfiles(options *model.UserGetOptions) StoreChannel GetProfiles(options *model.UserGetOptions) StoreChannel GetProfileByIds(userId []string, allowFromCache bool, viewRestrictions *model.ViewUsersRestrictions) StoreChannel + GetProfileByGroupChannelIdsForUser(userId string, channelIds []string) (map[string][]*model.User, *model.AppError) InvalidatProfileCacheForUser(userId string) GetByEmail(email string) (*model.User, *model.AppError) GetByAuth(authData *string, authService string) (*model.User, *model.AppError) diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 6ba2125a10..aca19d4eec 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -70,6 +70,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("SearchAllChannels", func(t *testing.T) { testChannelStoreSearchAllChannels(t, ss) }) t.Run("AutocompleteInTeamForSearch", func(t *testing.T) { testChannelStoreAutocompleteInTeamForSearch(t, ss, s) }) t.Run("GetMembersByIds", func(t *testing.T) { testChannelStoreGetMembersByIds(t, ss) }) + t.Run("SearchGroupChannels", func(t *testing.T) { testChannelStoreSearchGroupChannels(t, ss) }) t.Run("AnalyticsDeletedTypeCount", func(t *testing.T) { testChannelStoreAnalyticsDeletedTypeCount(t, ss) }) t.Run("GetPinnedPosts", func(t *testing.T) { testChannelStoreGetPinnedPosts(t, ss) }) t.Run("MaxChannelsPerTeam", func(t *testing.T) { testChannelStoreMaxChannelsPerTeam(t, ss) }) @@ -2716,6 +2717,153 @@ func testChannelStoreGetMembersByIds(t *testing.T, ss store.Store) { } } +func testChannelStoreSearchGroupChannels(t *testing.T, ss store.Store) { + // Users + u1 := &model.User{} + u1.Username = "user.one" + u1.Email = MakeEmail() + u1.Nickname = model.NewId() + store.Must(ss.User().Save(u1)) + + u2 := &model.User{} + u2.Username = "user.two" + u2.Email = MakeEmail() + u2.Nickname = model.NewId() + store.Must(ss.User().Save(u2)) + + u3 := &model.User{} + u3.Username = "user.three" + u3.Email = MakeEmail() + u3.Nickname = model.NewId() + store.Must(ss.User().Save(u3)) + + u4 := &model.User{} + u4.Username = "user.four" + u4.Email = MakeEmail() + u4.Nickname = model.NewId() + store.Must(ss.User().Save(u4)) + + // Group channels + userIds := []string{u1.Id, u2.Id, u3.Id} + gc1 := model.Channel{} + gc1.Name = model.GetGroupNameFromUserIds(userIds) + gc1.DisplayName = "GroupChannel" + model.NewId() + gc1.Type = model.CHANNEL_GROUP + _, err := ss.Channel().Save(&gc1, -1) + require.Nil(t, err) + + for _, userId := range userIds { + store.Must(ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: gc1.Id, + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + })) + } + + userIds = []string{u1.Id, u4.Id} + gc2 := model.Channel{} + gc2.Name = model.GetGroupNameFromUserIds(userIds) + gc2.DisplayName = "GroupChannel" + model.NewId() + gc2.Type = model.CHANNEL_GROUP + _, err = ss.Channel().Save(&gc2, -1) + require.Nil(t, err) + + for _, userId := range userIds { + store.Must(ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: gc2.Id, + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + })) + } + + userIds = []string{u1.Id, u2.Id, u3.Id, u4.Id} + gc3 := model.Channel{} + gc3.Name = model.GetGroupNameFromUserIds(userIds) + gc3.DisplayName = "GroupChannel" + model.NewId() + gc3.Type = model.CHANNEL_GROUP + _, err = ss.Channel().Save(&gc3, -1) + require.Nil(t, err) + + for _, userId := range userIds { + store.Must(ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: gc3.Id, + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + })) + } + + defer func() { + for _, gc := range []model.Channel{gc1, gc2, gc3} { + ss.Channel().PermanentDeleteMembersByChannel(gc3.Id) + <-ss.Channel().PermanentDelete(gc.Id) + } + }() + + testCases := []struct { + Name string + UserId string + Term string + ExpectedResult []string + }{ + { + Name: "Get all group channels for user1", + UserId: u1.Id, + Term: "", + ExpectedResult: []string{gc1.Id, gc2.Id, gc3.Id}, + }, + { + Name: "Get group channels for user1 and term 'three'", + UserId: u1.Id, + Term: "three", + ExpectedResult: []string{gc1.Id, gc3.Id}, + }, + { + Name: "Get group channels for user1 and term 'four two'", + UserId: u1.Id, + Term: "four two", + ExpectedResult: []string{gc3.Id}, + }, + { + Name: "Get all group channels for user2", + UserId: u2.Id, + Term: "", + ExpectedResult: []string{gc1.Id, gc3.Id}, + }, + { + Name: "Get group channels for user2 and term 'four'", + UserId: u2.Id, + Term: "four", + ExpectedResult: []string{gc3.Id}, + }, + { + Name: "Get all group channels for user4", + UserId: u4.Id, + Term: "", + ExpectedResult: []string{gc2.Id, gc3.Id}, + }, + { + Name: "Get group channels for user4 and term 'one five'", + UserId: u4.Id, + Term: "one five", + ExpectedResult: []string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + result, err := ss.Channel().SearchGroupChannels(tc.UserId, tc.Term) + require.Nil(t, err) + + resultIds := []string{} + for _, gc := range *result { + resultIds = append(resultIds, gc.Id) + } + + require.ElementsMatch(t, tc.ExpectedResult, resultIds) + }) + } +} + func testChannelStoreAnalyticsDeletedTypeCount(t *testing.T, ss store.Store) { o1 := model.Channel{} o1.TeamId = model.NewId() diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index d2203ff636..d0203d151c 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -1283,6 +1283,31 @@ func (_m *ChannelStore) SearchAllChannels(term string, opts store.ChannelSearchO return r0 } +// SearchGroupChannels provides a mock function with given fields: userId, term +func (_m *ChannelStore) SearchGroupChannels(userId string, term string) (*model.ChannelList, *model.AppError) { + ret := _m.Called(userId, term) + + var r0 *model.ChannelList + if rf, ok := ret.Get(0).(func(string, string) *model.ChannelList); ok { + r0 = rf(userId, term) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.ChannelList) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, string) *model.AppError); ok { + r1 = rf(userId, term) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // SearchInTeam provides a mock function with given fields: teamId, term, includeDeleted func (_m *ChannelStore) SearchInTeam(teamId string, term string, includeDeleted bool) (*model.ChannelList, *model.AppError) { ret := _m.Called(teamId, term, includeDeleted) diff --git a/store/storetest/mocks/UserStore.go b/store/storetest/mocks/UserStore.go index 0b4848e262..54f6f88606 100644 --- a/store/storetest/mocks/UserStore.go +++ b/store/storetest/mocks/UserStore.go @@ -381,6 +381,31 @@ func (_m *UserStore) GetNewUsersForTeam(teamId string, offset int, limit int, vi return r0 } +// GetProfileByGroupChannelIdsForUser provides a mock function with given fields: userId, channelIds +func (_m *UserStore) GetProfileByGroupChannelIdsForUser(userId string, channelIds []string) (map[string][]*model.User, *model.AppError) { + ret := _m.Called(userId, channelIds) + + var r0 map[string][]*model.User + if rf, ok := ret.Get(0).(func(string, []string) map[string][]*model.User); ok { + r0 = rf(userId, channelIds) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string][]*model.User) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, []string) *model.AppError); ok { + r1 = rf(userId, channelIds) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // GetProfileByIds provides a mock function with given fields: userId, allowFromCache, viewRestrictions func (_m *UserStore) GetProfileByIds(userId []string, allowFromCache bool, viewRestrictions *model.ViewUsersRestrictions) store.StoreChannel { ret := _m.Called(userId, allowFromCache, viewRestrictions) diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index a494161b5b..4613ed07f5 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -42,6 +42,7 @@ func TestUserStore(t *testing.T, ss store.Store) { t.Run("GetAllProfilesInChannel", func(t *testing.T) { testUserStoreGetAllProfilesInChannel(t, ss) }) t.Run("GetProfilesNotInChannel", func(t *testing.T) { testUserStoreGetProfilesNotInChannel(t, ss) }) t.Run("GetProfilesByIds", func(t *testing.T) { testUserStoreGetProfilesByIds(t, ss) }) + t.Run("GetProfileByGroupChannelIdsForUser", func(t *testing.T) { testUserStoreGetProfileByGroupChannelIdsForUser(t, ss) }) t.Run("GetProfilesByUsernames", func(t *testing.T) { testUserStoreGetProfilesByUsernames(t, ss) }) t.Run("GetSystemAdminProfiles", func(t *testing.T) { testUserStoreGetSystemAdminProfiles(t, ss) }) t.Run("GetByEmail", func(t *testing.T) { testUserStoreGetByEmail(t, ss) }) @@ -1217,6 +1218,122 @@ func testUserStoreGetProfilesByIds(t *testing.T, ss store.Store) { }) } +func testUserStoreGetProfileByGroupChannelIdsForUser(t *testing.T, ss store.Store) { + u1 := store.Must(ss.User().Save(&model.User{ + Email: MakeEmail(), + Username: "u1" + model.NewId(), + })).(*model.User) + defer func() { require.Nil(t, ss.User().PermanentDelete(u1.Id)) }() + + u2 := store.Must(ss.User().Save(&model.User{ + Email: MakeEmail(), + Username: "u2" + model.NewId(), + })).(*model.User) + defer func() { require.Nil(t, ss.User().PermanentDelete(u2.Id)) }() + + u3 := store.Must(ss.User().Save(&model.User{ + Email: MakeEmail(), + Username: "u3" + model.NewId(), + })).(*model.User) + defer func() { require.Nil(t, ss.User().PermanentDelete(u3.Id)) }() + + u4 := store.Must(ss.User().Save(&model.User{ + Email: MakeEmail(), + Username: "u4" + model.NewId(), + })).(*model.User) + defer func() { require.Nil(t, ss.User().PermanentDelete(u4.Id)) }() + + gc1, err := ss.Channel().Save(&model.Channel{ + DisplayName: "Profiles in private", + Name: "profiles-" + model.NewId(), + Type: model.CHANNEL_GROUP, + }, -1) + require.Nil(t, err) + + for _, uId := range []string{u1.Id, u2.Id, u3.Id} { + store.Must(ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: gc1.Id, + UserId: uId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + })) + } + + gc2, err := ss.Channel().Save(&model.Channel{ + DisplayName: "Profiles in private", + Name: "profiles-" + model.NewId(), + Type: model.CHANNEL_GROUP, + }, -1) + require.Nil(t, err) + + for _, uId := range []string{u1.Id, u3.Id, u4.Id} { + store.Must(ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: gc2.Id, + UserId: uId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + })) + } + + testCases := []struct { + Name string + UserId string + ChannelIds []string + ExpectedUserIdsByChannel map[string][]string + EnsureChannelsNotInResults []string + }{ + { + Name: "Get group 1 as user 1", + UserId: u1.Id, + ChannelIds: []string{gc1.Id}, + ExpectedUserIdsByChannel: map[string][]string{ + gc1.Id: {u2.Id, u3.Id}, + }, + EnsureChannelsNotInResults: []string{}, + }, + { + Name: "Get groups 1 and 2 as user 1", + UserId: u1.Id, + ChannelIds: []string{gc1.Id, gc2.Id}, + ExpectedUserIdsByChannel: map[string][]string{ + gc1.Id: {u2.Id, u3.Id}, + gc2.Id: {u3.Id, u4.Id}, + }, + EnsureChannelsNotInResults: []string{}, + }, + { + Name: "Get groups 1 and 2 as user 2", + UserId: u2.Id, + ChannelIds: []string{gc1.Id, gc2.Id}, + ExpectedUserIdsByChannel: map[string][]string{ + gc1.Id: {u1.Id, u3.Id}, + }, + EnsureChannelsNotInResults: []string{gc2.Id}, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + res, err := ss.User().GetProfileByGroupChannelIdsForUser(tc.UserId, tc.ChannelIds) + require.Nil(t, err) + + for channelId, expectedUsers := range tc.ExpectedUserIdsByChannel { + users, ok := res[channelId] + require.True(t, ok) + + userIds := []string{} + for _, user := range users { + userIds = append(userIds, user.Id) + } + require.ElementsMatch(t, expectedUsers, userIds) + } + + for _, channelId := range tc.EnsureChannelsNotInResults { + _, ok := res[channelId] + require.False(t, ok) + } + }) + } +} + func testUserStoreGetProfilesByUsernames(t *testing.T, ss store.Store) { teamId := model.NewId() team2Id := model.NewId()