From 715097cc76510a3d78ba83e8544ee7c956ed26e9 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Wed, 17 Oct 2018 11:24:12 -0400 Subject: MM-12234: configurable limit to user autocomplete and search matches (#9499) * unit test cleanup * allow limiting user search results * clean up test users before starting * model UserSearchOptions to simplify parameters --- api4/apitestlib.go | 7 +- api4/user.go | 59 +- api4/user_test.go | 44 +- app/user.go | 58 +- model/client4.go | 28 +- model/user_search.go | 29 +- model/user_search_test.go | 4 +- store/sqlstore/user_store.go | 102 ++- store/store.go | 10 +- store/storetest/mocks/ChannelStore.go | 31 +- store/storetest/mocks/UserStore.go | 20 +- store/storetest/user_store.go | 1434 +++++++++++++++++++++------------ 12 files changed, 1150 insertions(+), 676 deletions(-) diff --git a/api4/apitestlib.go b/api4/apitestlib.go index 37dbcad25..9976932a9 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -184,8 +184,11 @@ func (me *TestHelper) TearDown() { go func() { defer wg.Done() - options := map[string]bool{} - options[store.USER_SEARCH_OPTION_NAMES_ONLY_NO_FULL_NAME] = true + options := &model.UserSearchOptions{ + AllowEmails: false, + AllowFullNames: false, + Limit: model.USER_SEARCH_MAX_LIMIT, + } if result := <-me.App.Srv.Store.User().Search("", "fakeuser", options); result.Err != nil { mlog.Error("Error tearing down test users") } else { diff --git a/api4/user.go b/api4/user.go index c97e90e89..b827857a7 100644 --- a/api4/user.go +++ b/api4/user.go @@ -14,7 +14,6 @@ import ( "github.com/mattermost/mattermost-server/app" "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/store" ) func (api *API) InitUser() { @@ -547,23 +546,26 @@ func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) { return } - searchOptions := map[string]bool{} - searchOptions[store.USER_SEARCH_OPTION_ALLOW_INACTIVE] = props.AllowInactive + if props.Limit <= 0 || props.Limit > model.USER_SEARCH_MAX_LIMIT { + c.SetInvalidParam("limit") + return + } - if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { - hideFullName := !c.App.Config().PrivacySettings.ShowFullName - hideEmail := !c.App.Config().PrivacySettings.ShowEmailAddress - - if hideFullName && hideEmail { - searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY_NO_FULL_NAME] = true - } else if hideFullName { - searchOptions[store.USER_SEARCH_OPTION_ALL_NO_FULL_NAME] = true - } else if hideEmail { - searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true - } + options := &model.UserSearchOptions{ + IsAdmin: c.IsSystemAdmin(), + AllowInactive: props.AllowInactive, + Limit: props.Limit, } - profiles, err := c.App.SearchUsers(props, searchOptions, c.IsSystemAdmin()) + if c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { + options.AllowEmails = true + options.AllowFullNames = true + } else { + options.AllowEmails = c.App.Config().PrivacySettings.ShowEmailAddress + options.AllowFullNames = c.App.Config().PrivacySettings.ShowFullName + } + + profiles, err := c.App.SearchUsers(props, options) if err != nil { c.Err = err return @@ -576,17 +578,26 @@ func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) { channelId := r.URL.Query().Get("in_channel") teamId := r.URL.Query().Get("in_team") name := r.URL.Query().Get("name") + limitStr := r.URL.Query().Get("limit") + limit, _ := strconv.Atoi(limitStr) + if limitStr == "" { + limit = model.USER_SEARCH_DEFAULT_LIMIT + } autocomplete := new(model.UserAutocomplete) var err *model.AppError - searchOptions := map[string]bool{} + options := &model.UserSearchOptions{ + IsAdmin: c.IsSystemAdmin(), + // Never autocomplete on emails. + AllowEmails: false, + Limit: limit, + } - hideFullName := !c.App.Config().PrivacySettings.ShowFullName - if hideFullName && !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { - searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY_NO_FULL_NAME] = true + if c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { + options.AllowFullNames = true } else { - searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true + options.AllowFullNames = c.App.Config().PrivacySettings.ShowFullName } if len(channelId) > 0 { @@ -606,8 +617,8 @@ func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) { if len(channelId) > 0 { // Applying the provided teamId here is useful for DMs and GMs which don't belong // to a team. Applying it when the channel does belong to a team makes less sense, - //t but the permissions are checked above regardless. - result, err := c.App.AutocompleteUsersInChannel(teamId, channelId, name, searchOptions, c.IsSystemAdmin()) + // but the permissions are checked above regardless. + result, err := c.App.AutocompleteUsersInChannel(teamId, channelId, name, options) if err != nil { c.Err = err return @@ -616,7 +627,7 @@ func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) { autocomplete.Users = result.InChannel autocomplete.OutOfChannel = result.OutOfChannel } else if len(teamId) > 0 { - result, err := c.App.AutocompleteUsersInTeam(teamId, name, searchOptions, c.IsSystemAdmin()) + result, err := c.App.AutocompleteUsersInTeam(teamId, name, options) if err != nil { c.Err = err return @@ -625,7 +636,7 @@ func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) { autocomplete.Users = result.InTeam } else { // No permission check required - result, err := c.App.SearchUsersInTeam("", name, searchOptions, c.IsSystemAdmin()) + result, err := c.App.SearchUsersInTeam("", name, options) if err != nil { c.Err = err return diff --git a/api4/user_test.go b/api4/user_test.go index b99011aeb..9df062a2a 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -759,92 +759,92 @@ func TestAutocompleteUsers(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { cfg.PrivacySettings.ShowFullName = showFullName }) }() - rusers, resp := Client.AutocompleteUsersInChannel(teamId, channelId, username, "") + rusers, resp := Client.AutocompleteUsersInChannel(teamId, channelId, username, model.USER_SEARCH_DEFAULT_LIMIT, "") CheckNoError(t, resp) if len(rusers.Users) != 1 { t.Fatal("should have returned 1 user") } - rusers, resp = Client.AutocompleteUsersInChannel(teamId, channelId, "amazonses", "") + rusers, resp = Client.AutocompleteUsersInChannel(teamId, channelId, "amazonses", model.USER_SEARCH_DEFAULT_LIMIT, "") CheckNoError(t, resp) if len(rusers.Users) != 0 { t.Fatal("should have returned 0 users") } - rusers, resp = Client.AutocompleteUsersInChannel(teamId, channelId, "", "") + rusers, resp = Client.AutocompleteUsersInChannel(teamId, channelId, "", model.USER_SEARCH_DEFAULT_LIMIT, "") CheckNoError(t, resp) if len(rusers.Users) < 2 { t.Fatal("should have many users") } - rusers, resp = Client.AutocompleteUsersInChannel("", channelId, "", "") + rusers, resp = Client.AutocompleteUsersInChannel("", channelId, "", model.USER_SEARCH_DEFAULT_LIMIT, "") CheckNoError(t, resp) if len(rusers.Users) < 2 { t.Fatal("should have many users") } - rusers, resp = Client.AutocompleteUsersInTeam(teamId, username, "") + rusers, resp = Client.AutocompleteUsersInTeam(teamId, username, model.USER_SEARCH_DEFAULT_LIMIT, "") CheckNoError(t, resp) if len(rusers.Users) != 1 { t.Fatal("should have returned 1 user") } - rusers, resp = Client.AutocompleteUsers(username, "") + rusers, resp = Client.AutocompleteUsers(username, model.USER_SEARCH_DEFAULT_LIMIT, "") CheckNoError(t, resp) if len(rusers.Users) != 1 { t.Fatal("should have returned 1 users") } - rusers, resp = Client.AutocompleteUsers("", "") + rusers, resp = Client.AutocompleteUsers("", model.USER_SEARCH_DEFAULT_LIMIT, "") CheckNoError(t, resp) if len(rusers.Users) < 2 { t.Fatal("should have returned many users") } - rusers, resp = Client.AutocompleteUsersInTeam(teamId, "amazonses", "") + rusers, resp = Client.AutocompleteUsersInTeam(teamId, "amazonses", model.USER_SEARCH_DEFAULT_LIMIT, "") CheckNoError(t, resp) if len(rusers.Users) != 0 { t.Fatal("should have returned 0 users") } - rusers, resp = Client.AutocompleteUsersInTeam(teamId, "", "") + rusers, resp = Client.AutocompleteUsersInTeam(teamId, "", model.USER_SEARCH_DEFAULT_LIMIT, "") CheckNoError(t, resp) if len(rusers.Users) < 2 { t.Fatal("should have many users") } Client.Logout() - _, resp = Client.AutocompleteUsersInChannel(teamId, channelId, username, "") + _, resp = Client.AutocompleteUsersInChannel(teamId, channelId, username, model.USER_SEARCH_DEFAULT_LIMIT, "") CheckUnauthorizedStatus(t, resp) - _, resp = Client.AutocompleteUsersInTeam(teamId, username, "") + _, resp = Client.AutocompleteUsersInTeam(teamId, username, model.USER_SEARCH_DEFAULT_LIMIT, "") CheckUnauthorizedStatus(t, resp) - _, resp = Client.AutocompleteUsers(username, "") + _, resp = Client.AutocompleteUsers(username, model.USER_SEARCH_DEFAULT_LIMIT, "") CheckUnauthorizedStatus(t, resp) user := th.CreateUser() Client.Login(user.Email, user.Password) - _, resp = Client.AutocompleteUsersInChannel(teamId, channelId, username, "") + _, resp = Client.AutocompleteUsersInChannel(teamId, channelId, username, model.USER_SEARCH_DEFAULT_LIMIT, "") CheckForbiddenStatus(t, resp) - _, resp = Client.AutocompleteUsersInTeam(teamId, username, "") + _, resp = Client.AutocompleteUsersInTeam(teamId, username, model.USER_SEARCH_DEFAULT_LIMIT, "") CheckForbiddenStatus(t, resp) - _, resp = Client.AutocompleteUsers(username, "") + _, resp = Client.AutocompleteUsers(username, model.USER_SEARCH_DEFAULT_LIMIT, "") CheckNoError(t, resp) - _, resp = th.SystemAdminClient.AutocompleteUsersInChannel(teamId, channelId, username, "") + _, resp = th.SystemAdminClient.AutocompleteUsersInChannel(teamId, channelId, username, model.USER_SEARCH_DEFAULT_LIMIT, "") CheckNoError(t, resp) - _, resp = th.SystemAdminClient.AutocompleteUsersInTeam(teamId, username, "") + _, resp = th.SystemAdminClient.AutocompleteUsersInTeam(teamId, username, model.USER_SEARCH_DEFAULT_LIMIT, "") CheckNoError(t, resp) - _, resp = th.SystemAdminClient.AutocompleteUsers(username, "") + _, resp = th.SystemAdminClient.AutocompleteUsers(username, model.USER_SEARCH_DEFAULT_LIMIT, "") CheckNoError(t, resp) // Check against privacy config settings @@ -852,21 +852,21 @@ func TestAutocompleteUsers(t *testing.T) { th.LoginBasic() - rusers, resp = Client.AutocompleteUsers(username, "") + rusers, resp = Client.AutocompleteUsers(username, model.USER_SEARCH_DEFAULT_LIMIT, "") CheckNoError(t, resp) if rusers.Users[0].FirstName != "" || rusers.Users[0].LastName != "" { t.Fatal("should not show first/last name") } - rusers, resp = Client.AutocompleteUsersInChannel(teamId, channelId, username, "") + rusers, resp = Client.AutocompleteUsersInChannel(teamId, channelId, username, model.USER_SEARCH_DEFAULT_LIMIT, "") CheckNoError(t, resp) if rusers.Users[0].FirstName != "" || rusers.Users[0].LastName != "" { t.Fatal("should not show first/last name") } - rusers, resp = Client.AutocompleteUsersInTeam(teamId, username, "") + rusers, resp = Client.AutocompleteUsersInTeam(teamId, username, model.USER_SEARCH_DEFAULT_LIMIT, "") CheckNoError(t, resp) if rusers.Users[0].FirstName != "" || rusers.Users[0].LastName != "" { @@ -874,7 +874,7 @@ func TestAutocompleteUsers(t *testing.T) { } t.Run("user must have access to team id, especially when it does not match channel's team id", func(t *testing.T) { - rusers, resp = Client.AutocompleteUsersInChannel("otherTeamId", channelId, username, "") + rusers, resp = Client.AutocompleteUsersInChannel("otherTeamId", channelId, username, model.USER_SEARCH_DEFAULT_LIMIT, "") CheckErrorMessage(t, resp, "api.context.permissions.app_error") }) } diff --git a/app/user.go b/app/user.go index e565fea76..b3fd3f5b9 100644 --- a/app/user.go +++ b/app/user.go @@ -1469,93 +1469,93 @@ func (a *App) VerifyUserEmail(userId string) *model.AppError { return (<-a.Srv.Store.User().VerifyEmail(userId)).Err } -func (a *App) SearchUsers(props *model.UserSearch, searchOptions map[string]bool, asAdmin bool) ([]*model.User, *model.AppError) { +func (a *App) SearchUsers(props *model.UserSearch, options *model.UserSearchOptions) ([]*model.User, *model.AppError) { if props.WithoutTeam { - return a.SearchUsersWithoutTeam(props.Term, searchOptions, asAdmin) + return a.SearchUsersWithoutTeam(props.Term, options) } else if props.InChannelId != "" { - return a.SearchUsersInChannel(props.InChannelId, props.Term, searchOptions, asAdmin) + return a.SearchUsersInChannel(props.InChannelId, props.Term, options) } else if props.NotInChannelId != "" { - return a.SearchUsersNotInChannel(props.TeamId, props.NotInChannelId, props.Term, searchOptions, asAdmin) + return a.SearchUsersNotInChannel(props.TeamId, props.NotInChannelId, props.Term, options) } else if props.NotInTeamId != "" { - return a.SearchUsersNotInTeam(props.NotInTeamId, props.Term, searchOptions, asAdmin) + return a.SearchUsersNotInTeam(props.NotInTeamId, props.Term, options) } else { - return a.SearchUsersInTeam(props.TeamId, props.Term, searchOptions, asAdmin) + return a.SearchUsersInTeam(props.TeamId, props.Term, options) } } -func (a *App) SearchUsersInChannel(channelId string, term string, searchOptions map[string]bool, asAdmin bool) ([]*model.User, *model.AppError) { - if result := <-a.Srv.Store.User().SearchInChannel(channelId, term, searchOptions); result.Err != nil { +func (a *App) SearchUsersInChannel(channelId string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) { + if result := <-a.Srv.Store.User().SearchInChannel(channelId, term, options); result.Err != nil { return nil, result.Err } else { users := result.Data.([]*model.User) for _, user := range users { - a.SanitizeProfile(user, asAdmin) + a.SanitizeProfile(user, options.IsAdmin) } return users, nil } } -func (a *App) SearchUsersNotInChannel(teamId string, channelId string, term string, searchOptions map[string]bool, asAdmin bool) ([]*model.User, *model.AppError) { - if result := <-a.Srv.Store.User().SearchNotInChannel(teamId, channelId, term, searchOptions); result.Err != nil { +func (a *App) SearchUsersNotInChannel(teamId string, channelId string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) { + if result := <-a.Srv.Store.User().SearchNotInChannel(teamId, channelId, term, options); result.Err != nil { return nil, result.Err } else { users := result.Data.([]*model.User) for _, user := range users { - a.SanitizeProfile(user, asAdmin) + a.SanitizeProfile(user, options.IsAdmin) } return users, nil } } -func (a *App) SearchUsersInTeam(teamId string, term string, searchOptions map[string]bool, asAdmin bool) ([]*model.User, *model.AppError) { - if result := <-a.Srv.Store.User().Search(teamId, term, searchOptions); result.Err != nil { +func (a *App) SearchUsersInTeam(teamId string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) { + if result := <-a.Srv.Store.User().Search(teamId, term, options); result.Err != nil { return nil, result.Err } else { users := result.Data.([]*model.User) for _, user := range users { - a.SanitizeProfile(user, asAdmin) + a.SanitizeProfile(user, options.IsAdmin) } return users, nil } } -func (a *App) SearchUsersNotInTeam(notInTeamId string, term string, searchOptions map[string]bool, asAdmin bool) ([]*model.User, *model.AppError) { - if result := <-a.Srv.Store.User().SearchNotInTeam(notInTeamId, term, searchOptions); result.Err != nil { +func (a *App) SearchUsersNotInTeam(notInTeamId string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) { + if result := <-a.Srv.Store.User().SearchNotInTeam(notInTeamId, term, options); result.Err != nil { return nil, result.Err } else { users := result.Data.([]*model.User) for _, user := range users { - a.SanitizeProfile(user, asAdmin) + a.SanitizeProfile(user, options.IsAdmin) } return users, nil } } -func (a *App) SearchUsersWithoutTeam(term string, searchOptions map[string]bool, asAdmin bool) ([]*model.User, *model.AppError) { - if result := <-a.Srv.Store.User().SearchWithoutTeam(term, searchOptions); result.Err != nil { +func (a *App) SearchUsersWithoutTeam(term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) { + if result := <-a.Srv.Store.User().SearchWithoutTeam(term, options); result.Err != nil { return nil, result.Err } else { users := result.Data.([]*model.User) for _, user := range users { - a.SanitizeProfile(user, asAdmin) + a.SanitizeProfile(user, options.IsAdmin) } return users, nil } } -func (a *App) AutocompleteUsersInChannel(teamId string, channelId string, term string, searchOptions map[string]bool, asAdmin bool) (*model.UserAutocompleteInChannel, *model.AppError) { - uchan := a.Srv.Store.User().SearchInChannel(channelId, term, searchOptions) - nuchan := a.Srv.Store.User().SearchNotInChannel(teamId, channelId, term, searchOptions) +func (a *App) AutocompleteUsersInChannel(teamId string, channelId string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, *model.AppError) { + uchan := a.Srv.Store.User().SearchInChannel(channelId, term, options) + nuchan := a.Srv.Store.User().SearchNotInChannel(teamId, channelId, term, options) autocomplete := &model.UserAutocompleteInChannel{} @@ -1565,7 +1565,7 @@ func (a *App) AutocompleteUsersInChannel(teamId string, channelId string, term s users := result.Data.([]*model.User) for _, user := range users { - a.SanitizeProfile(user, asAdmin) + a.SanitizeProfile(user, options.IsAdmin) } autocomplete.InChannel = users @@ -1577,7 +1577,7 @@ func (a *App) AutocompleteUsersInChannel(teamId string, channelId string, term s users := result.Data.([]*model.User) for _, user := range users { - a.SanitizeProfile(user, asAdmin) + a.SanitizeProfile(user, options.IsAdmin) } autocomplete.OutOfChannel = users @@ -1586,16 +1586,16 @@ func (a *App) AutocompleteUsersInChannel(teamId string, channelId string, term s return autocomplete, nil } -func (a *App) AutocompleteUsersInTeam(teamId string, term string, searchOptions map[string]bool, asAdmin bool) (*model.UserAutocompleteInTeam, *model.AppError) { +func (a *App) AutocompleteUsersInTeam(teamId string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInTeam, *model.AppError) { autocomplete := &model.UserAutocompleteInTeam{} - if result := <-a.Srv.Store.User().Search(teamId, term, searchOptions); result.Err != nil { + if result := <-a.Srv.Store.User().Search(teamId, term, options); result.Err != nil { return nil, result.Err } else { users := result.Data.([]*model.User) for _, user := range users { - a.SanitizeProfile(user, asAdmin) + a.SanitizeProfile(user, options.IsAdmin) } autocomplete.InTeam = users diff --git a/model/client4.go b/model/client4.go index c35dab904..c5001c048 100644 --- a/model/client4.go +++ b/model/client4.go @@ -417,6 +417,10 @@ func (c *Client4) DoApiPost(url string, data string) (*http.Response, *AppError) return c.DoApiRequest(http.MethodPost, c.ApiUrl+url, data, "") } +func (c *Client4) doApiPostBytes(url string, data []byte) (*http.Response, *AppError) { + return c.doApiRequestBytes(http.MethodPost, c.ApiUrl+url, data, "") +} + func (c *Client4) DoApiPut(url string, data string) (*http.Response, *AppError) { return c.DoApiRequest(http.MethodPut, c.ApiUrl+url, data, "") } @@ -426,7 +430,15 @@ func (c *Client4) DoApiDelete(url string) (*http.Response, *AppError) { } func (c *Client4) DoApiRequest(method, url, data, etag string) (*http.Response, *AppError) { - rq, _ := http.NewRequest(method, url, strings.NewReader(data)) + return c.doApiRequestReader(method, url, strings.NewReader(data), etag) +} + +func (c *Client4) doApiRequestBytes(method, url string, data []byte, etag string) (*http.Response, *AppError) { + return c.doApiRequestReader(method, url, bytes.NewReader(data), etag) +} + +func (c *Client4) doApiRequestReader(method, url string, data io.Reader, etag string) (*http.Response, *AppError) { + rq, _ := http.NewRequest(method, url, data) if len(etag) > 0 { rq.Header.Set(HEADER_ETAG_CLIENT, etag) @@ -691,8 +703,8 @@ func (c *Client4) GetUserByEmail(email, etag string) (*User, *Response) { } // AutocompleteUsersInTeam returns the users on a team based on search term. -func (c *Client4) AutocompleteUsersInTeam(teamId string, username string, etag string) (*UserAutocomplete, *Response) { - query := fmt.Sprintf("?in_team=%v&name=%v", teamId, username) +func (c *Client4) AutocompleteUsersInTeam(teamId string, username string, limit int, etag string) (*UserAutocomplete, *Response) { + query := fmt.Sprintf("?in_team=%v&name=%v&limit=%d", teamId, username, limit) if r, err := c.DoApiGet(c.GetUsersRoute()+"/autocomplete"+query, etag); err != nil { return nil, BuildErrorResponse(r, err) } else { @@ -702,8 +714,8 @@ func (c *Client4) AutocompleteUsersInTeam(teamId string, username string, etag s } // AutocompleteUsersInChannel returns the users in a channel based on search term. -func (c *Client4) AutocompleteUsersInChannel(teamId string, channelId string, username string, etag string) (*UserAutocomplete, *Response) { - query := fmt.Sprintf("?in_team=%v&in_channel=%v&name=%v", teamId, channelId, username) +func (c *Client4) AutocompleteUsersInChannel(teamId string, channelId string, username string, limit int, etag string) (*UserAutocomplete, *Response) { + query := fmt.Sprintf("?in_team=%v&in_channel=%v&name=%v&limit=%d", teamId, channelId, username, limit) if r, err := c.DoApiGet(c.GetUsersRoute()+"/autocomplete"+query, etag); err != nil { return nil, BuildErrorResponse(r, err) } else { @@ -713,8 +725,8 @@ func (c *Client4) AutocompleteUsersInChannel(teamId string, channelId string, us } // AutocompleteUsers returns the users in the system based on search term. -func (c *Client4) AutocompleteUsers(username string, etag string) (*UserAutocomplete, *Response) { - query := fmt.Sprintf("?name=%v", username) +func (c *Client4) AutocompleteUsers(username string, limit int, etag string) (*UserAutocomplete, *Response) { + query := fmt.Sprintf("?name=%v&limit=%d", username, limit) if r, err := c.DoApiGet(c.GetUsersRoute()+"/autocomplete"+query, etag); err != nil { return nil, BuildErrorResponse(r, err) } else { @@ -875,7 +887,7 @@ func (c *Client4) GetUsersByUsernames(usernames []string) ([]*User, *Response) { // SearchUsers returns a list of users based on some search criteria. func (c *Client4) SearchUsers(search *UserSearch) ([]*User, *Response) { - if r, err := c.DoApiPost(c.GetUsersRoute()+"/search", search.ToJson()); err != nil { + if r, err := c.doApiPostBytes(c.GetUsersRoute()+"/search", search.ToJson()); err != nil { return nil, BuildErrorResponse(r, err) } else { defer closeBody(r) diff --git a/model/user_search.go b/model/user_search.go index 94596bdcd..68749fbe5 100644 --- a/model/user_search.go +++ b/model/user_search.go @@ -8,6 +8,10 @@ import ( "io" ) +const USER_SEARCH_MAX_LIMIT = 1000 +const USER_SEARCH_DEFAULT_LIMIT = 100 + +// UserSearch captures the parameters provided by a client for initiating a user search. type UserSearch struct { Term string `json:"term"` TeamId string `json:"team_id"` @@ -16,17 +20,38 @@ type UserSearch struct { NotInChannelId string `json:"not_in_channel_id"` AllowInactive bool `json:"allow_inactive"` WithoutTeam bool `json:"without_team"` + Limit int `json:"limit"` } // ToJson convert a User to a json string -func (u *UserSearch) ToJson() string { +func (u *UserSearch) ToJson() []byte { b, _ := json.Marshal(u) - return string(b) + return b } // UserSearchFromJson will decode the input and return a User func UserSearchFromJson(data io.Reader) *UserSearch { var us *UserSearch json.NewDecoder(data).Decode(&us) + + if us.Limit == 0 { + us.Limit = USER_SEARCH_DEFAULT_LIMIT + } + return us } + +// UserSearchOptions captures internal parameters derived from the user's permissions and a +// UserSearch request. +type UserSearchOptions struct { + // IsAdmin tracks whether or not the search is being conducted by an administrator. + IsAdmin bool + // AllowEmails allows search to examine the emails of users. + AllowEmails bool + // AllowFullNames allows search to examine the full names of users, vs. just usernames and nicknames. + AllowFullNames bool + // AllowInactive configures whether or not to return inactive users in the search results. + AllowInactive bool + // Limit limits the total number of results returned. + Limit int +} diff --git a/model/user_search_test.go b/model/user_search_test.go index 0fada7781..89c8b68f7 100644 --- a/model/user_search_test.go +++ b/model/user_search_test.go @@ -4,14 +4,14 @@ package model import ( - "strings" + "bytes" "testing" ) func TestUserSearchJson(t *testing.T) { userSearch := UserSearch{Term: NewId(), TeamId: NewId()} json := userSearch.ToJson() - ruserSearch := UserSearchFromJson(strings.NewReader(json)) + ruserSearch := UserSearchFromJson(bytes.NewReader(json)) if userSearch.Term != ruserSearch.Term { t.Fatal("Terms do not match") diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index f0839c3b7..0e70a87d9 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -970,12 +970,11 @@ func (us SqlUserStore) GetAnyUnreadPostCountForChannel(userId string, channelId }) } -func (us SqlUserStore) Search(teamId string, term string, options map[string]bool) store.StoreChannel { +func (us SqlUserStore) Search(teamId string, term string, options *model.UserSearchOptions) store.StoreChannel { return store.Do(func(result *store.StoreResult) { searchQuery := "" if teamId == "" { - // Id != '' is added because both SEARCH_CLAUSE and INACTIVE_CLAUSE start with an AND searchQuery = ` SELECT @@ -986,8 +985,8 @@ func (us SqlUserStore) Search(teamId string, term string, options map[string]boo Id != '' SEARCH_CLAUSE INACTIVE_CLAUSE - ORDER BY Username ASC - LIMIT 100` + ORDER BY Username ASC + LIMIT :Limit` } else { searchQuery = ` SELECT @@ -1000,16 +999,19 @@ func (us SqlUserStore) Search(teamId string, term string, options map[string]boo AND TeamMembers.DeleteAt = 0 SEARCH_CLAUSE INACTIVE_CLAUSE - ORDER BY Users.Username ASC - LIMIT 100` + ORDER BY Users.Username ASC + LIMIT :Limit` } - *result = us.performSearch(searchQuery, term, options, map[string]interface{}{"TeamId": teamId}) + *result = us.performSearch(searchQuery, term, options, map[string]interface{}{ + "TeamId": teamId, + "Limit": options.Limit, + }) }) } -func (us SqlUserStore) SearchWithoutTeam(term string, options map[string]bool) store.StoreChannel { +func (us SqlUserStore) SearchWithoutTeam(term string, options *model.UserSearchOptions) store.StoreChannel { return store.Do(func(result *store.StoreResult) { searchQuery := ` SELECT @@ -1027,14 +1029,16 @@ func (us SqlUserStore) SearchWithoutTeam(term string, options map[string]bool) s SEARCH_CLAUSE INACTIVE_CLAUSE ORDER BY Username ASC - LIMIT 100` + LIMIT :Limit` - *result = us.performSearch(searchQuery, term, options, map[string]interface{}{}) + *result = us.performSearch(searchQuery, term, options, map[string]interface{}{ + "Limit": options.Limit, + }) }) } -func (us SqlUserStore) SearchNotInTeam(notInTeamId string, term string, options map[string]bool) store.StoreChannel { +func (us SqlUserStore) SearchNotInTeam(notInTeamId string, term string, options *model.UserSearchOptions) store.StoreChannel { return store.Do(func(result *store.StoreResult) { searchQuery := ` SELECT @@ -1048,14 +1052,17 @@ func (us SqlUserStore) SearchNotInTeam(notInTeamId string, term string, options SEARCH_CLAUSE INACTIVE_CLAUSE ORDER BY Users.Username ASC - LIMIT 100` + LIMIT :Limit` - *result = us.performSearch(searchQuery, term, options, map[string]interface{}{"NotInTeamId": notInTeamId}) + *result = us.performSearch(searchQuery, term, options, map[string]interface{}{ + "NotInTeamId": notInTeamId, + "Limit": options.Limit, + }) }) } -func (us SqlUserStore) SearchNotInChannel(teamId string, channelId string, term string, options map[string]bool) store.StoreChannel { +func (us SqlUserStore) SearchNotInChannel(teamId string, channelId string, term string, options *model.UserSearchOptions) store.StoreChannel { return store.Do(func(result *store.StoreResult) { searchQuery := "" if teamId == "" { @@ -1071,7 +1078,7 @@ func (us SqlUserStore) SearchNotInChannel(teamId string, channelId string, term SEARCH_CLAUSE INACTIVE_CLAUSE ORDER BY Users.Username ASC - LIMIT 100` + LIMIT :Limit` } else { searchQuery = ` SELECT @@ -1089,30 +1096,37 @@ func (us SqlUserStore) SearchNotInChannel(teamId string, channelId string, term SEARCH_CLAUSE INACTIVE_CLAUSE ORDER BY Users.Username ASC - LIMIT 100` + LIMIT :Limit` } - *result = us.performSearch(searchQuery, term, options, map[string]interface{}{"TeamId": teamId, "ChannelId": channelId}) - + *result = us.performSearch(searchQuery, term, options, map[string]interface{}{ + "TeamId": teamId, + "ChannelId": channelId, + "Limit": options.Limit, + }) }) } -func (us SqlUserStore) SearchInChannel(channelId string, term string, options map[string]bool) store.StoreChannel { +func (us SqlUserStore) SearchInChannel(channelId string, term string, options *model.UserSearchOptions) store.StoreChannel { return store.Do(func(result *store.StoreResult) { searchQuery := ` - SELECT - Users.* - FROM - Users, ChannelMembers - WHERE - ChannelMembers.ChannelId = :ChannelId - AND ChannelMembers.UserId = Users.Id - SEARCH_CLAUSE - INACTIVE_CLAUSE - ORDER BY Users.Username ASC - LIMIT 100` - - *result = us.performSearch(searchQuery, term, options, map[string]interface{}{"ChannelId": channelId}) + SELECT + Users.* + FROM + Users, ChannelMembers + WHERE + ChannelMembers.ChannelId = :ChannelId + AND ChannelMembers.UserId = Users.Id + SEARCH_CLAUSE + INACTIVE_CLAUSE + ORDER BY Users.Username ASC + LIMIT :Limit + ` + + *result = us.performSearch(searchQuery, term, options, map[string]interface{}{ + "ChannelId": channelId, + "Limit": options.Limit, + }) }) } @@ -1160,7 +1174,7 @@ func generateSearchQuery(searchQuery string, terms []string, fields []string, pa return strings.Replace(searchQuery, "SEARCH_CLAUSE", fmt.Sprintf(" AND %s ", searchClause), 1) } -func (us SqlUserStore) performSearch(searchQuery string, term string, options map[string]bool, parameters map[string]interface{}) store.StoreResult { +func (us SqlUserStore) performSearch(searchQuery string, term string, options *model.UserSearchOptions, parameters map[string]interface{}) store.StoreResult { result := store.StoreResult{} // These chars must be removed from the like query. @@ -1173,16 +1187,22 @@ func (us SqlUserStore) performSearch(searchQuery string, term string, options ma term = strings.Replace(term, c, "*"+c, -1) } - searchType := USER_SEARCH_TYPE_ALL - if ok := options[store.USER_SEARCH_OPTION_NAMES_ONLY]; ok { - searchType = USER_SEARCH_TYPE_NAMES - } else if ok = options[store.USER_SEARCH_OPTION_NAMES_ONLY_NO_FULL_NAME]; ok { - searchType = USER_SEARCH_TYPE_NAMES_NO_FULL_NAME - } else if ok = options[store.USER_SEARCH_OPTION_ALL_NO_FULL_NAME]; ok { - searchType = USER_SEARCH_TYPE_ALL_NO_FULL_NAME + searchType := USER_SEARCH_TYPE_NAMES_NO_FULL_NAME + if options.AllowEmails { + if options.AllowFullNames { + searchType = USER_SEARCH_TYPE_ALL + } else { + searchType = USER_SEARCH_TYPE_ALL_NO_FULL_NAME + } + } else { + if options.AllowFullNames { + searchType = USER_SEARCH_TYPE_NAMES + } else { + searchType = USER_SEARCH_TYPE_NAMES_NO_FULL_NAME + } } - if ok := options[store.USER_SEARCH_OPTION_ALLOW_INACTIVE]; ok { + if ok := options.AllowInactive; ok { searchQuery = strings.Replace(searchQuery, "INACTIVE_CLAUSE", "", 1) } else { searchQuery = strings.Replace(searchQuery, "INACTIVE_CLAUSE", "AND Users.DeleteAt = 0", 1) diff --git a/store/store.go b/store/store.go index 1e153b422..e554f1486 100644 --- a/store/store.go +++ b/store/store.go @@ -272,11 +272,11 @@ type UserStore interface { GetAnyUnreadPostCountForChannel(userId string, channelId string) StoreChannel GetRecentlyActiveUsersForTeam(teamId string, offset, limit int) StoreChannel GetNewUsersForTeam(teamId string, offset, limit int) StoreChannel - Search(teamId string, term string, options map[string]bool) StoreChannel - SearchNotInTeam(notInTeamId string, term string, options map[string]bool) StoreChannel - SearchInChannel(channelId string, term string, options map[string]bool) StoreChannel - SearchNotInChannel(teamId string, channelId string, term string, options map[string]bool) StoreChannel - SearchWithoutTeam(term string, options map[string]bool) StoreChannel + Search(teamId string, term string, options *model.UserSearchOptions) StoreChannel + SearchNotInTeam(notInTeamId string, term string, options *model.UserSearchOptions) StoreChannel + SearchInChannel(channelId string, term string, options *model.UserSearchOptions) StoreChannel + SearchNotInChannel(teamId string, channelId string, term string, options *model.UserSearchOptions) StoreChannel + SearchWithoutTeam(term string, options *model.UserSearchOptions) StoreChannel AnalyticsGetInactiveUsersCount() StoreChannel AnalyticsGetSystemAdminCount() StoreChannel GetProfilesNotInTeam(teamId string, offset int, limit int) StoreChannel diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 9d42d0b65..7ad8f100d 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -314,6 +314,22 @@ func (_m *ChannelStore) GetChannelMembersForExport(userId string, teamId string) return r0 } +// GetChannelMembersTimezones provides a mock function with given fields: channelId +func (_m *ChannelStore) GetChannelMembersTimezones(channelId string) store.StoreChannel { + ret := _m.Called(channelId) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok { + r0 = rf(channelId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + // GetChannelUnread provides a mock function with given fields: channelId, userId func (_m *ChannelStore) GetChannelUnread(channelId string, userId string) store.StoreChannel { ret := _m.Called(channelId, userId) @@ -442,21 +458,6 @@ func (_m *ChannelStore) GetMember(channelId string, userId string) store.StoreCh return r0 } -func (_m *ChannelStore) GetChannelMembersTimezones(channelId string) store.StoreChannel { - ret := _m.Called(channelId) - - var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok { - r0 = rf(channelId) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(store.StoreChannel) - } - } - - return r0 -} - // GetMemberCount provides a mock function with given fields: channelId, allowFromCache func (_m *ChannelStore) GetMemberCount(channelId string, allowFromCache bool) store.StoreChannel { ret := _m.Called(channelId, allowFromCache) diff --git a/store/storetest/mocks/UserStore.go b/store/storetest/mocks/UserStore.go index 1e0ce8818..de836e95d 100644 --- a/store/storetest/mocks/UserStore.go +++ b/store/storetest/mocks/UserStore.go @@ -626,11 +626,11 @@ func (_m *UserStore) Save(user *model.User) store.StoreChannel { } // Search provides a mock function with given fields: teamId, term, options -func (_m *UserStore) Search(teamId string, term string, options map[string]bool) store.StoreChannel { +func (_m *UserStore) Search(teamId string, term string, options *model.UserSearchOptions) store.StoreChannel { ret := _m.Called(teamId, term, options) var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func(string, string, map[string]bool) store.StoreChannel); ok { + if rf, ok := ret.Get(0).(func(string, string, *model.UserSearchOptions) store.StoreChannel); ok { r0 = rf(teamId, term, options) } else { if ret.Get(0) != nil { @@ -642,11 +642,11 @@ func (_m *UserStore) Search(teamId string, term string, options map[string]bool) } // SearchInChannel provides a mock function with given fields: channelId, term, options -func (_m *UserStore) SearchInChannel(channelId string, term string, options map[string]bool) store.StoreChannel { +func (_m *UserStore) SearchInChannel(channelId string, term string, options *model.UserSearchOptions) store.StoreChannel { ret := _m.Called(channelId, term, options) var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func(string, string, map[string]bool) store.StoreChannel); ok { + if rf, ok := ret.Get(0).(func(string, string, *model.UserSearchOptions) store.StoreChannel); ok { r0 = rf(channelId, term, options) } else { if ret.Get(0) != nil { @@ -658,11 +658,11 @@ func (_m *UserStore) SearchInChannel(channelId string, term string, options map[ } // SearchNotInChannel provides a mock function with given fields: teamId, channelId, term, options -func (_m *UserStore) SearchNotInChannel(teamId string, channelId string, term string, options map[string]bool) store.StoreChannel { +func (_m *UserStore) SearchNotInChannel(teamId string, channelId string, term string, options *model.UserSearchOptions) store.StoreChannel { ret := _m.Called(teamId, channelId, term, options) var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func(string, string, string, map[string]bool) store.StoreChannel); ok { + if rf, ok := ret.Get(0).(func(string, string, string, *model.UserSearchOptions) store.StoreChannel); ok { r0 = rf(teamId, channelId, term, options) } else { if ret.Get(0) != nil { @@ -674,11 +674,11 @@ func (_m *UserStore) SearchNotInChannel(teamId string, channelId string, term st } // SearchNotInTeam provides a mock function with given fields: notInTeamId, term, options -func (_m *UserStore) SearchNotInTeam(notInTeamId string, term string, options map[string]bool) store.StoreChannel { +func (_m *UserStore) SearchNotInTeam(notInTeamId string, term string, options *model.UserSearchOptions) store.StoreChannel { ret := _m.Called(notInTeamId, term, options) var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func(string, string, map[string]bool) store.StoreChannel); ok { + if rf, ok := ret.Get(0).(func(string, string, *model.UserSearchOptions) store.StoreChannel); ok { r0 = rf(notInTeamId, term, options) } else { if ret.Get(0) != nil { @@ -690,11 +690,11 @@ func (_m *UserStore) SearchNotInTeam(notInTeamId string, term string, options ma } // SearchWithoutTeam provides a mock function with given fields: term, options -func (_m *UserStore) SearchWithoutTeam(term string, options map[string]bool) store.StoreChannel { +func (_m *UserStore) SearchWithoutTeam(term string, options *model.UserSearchOptions) store.StoreChannel { ret := _m.Called(term, options) var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func(string, map[string]bool) store.StoreChannel); ok { + if rf, ok := ret.Get(0).(func(string, *model.UserSearchOptions) store.StoreChannel); ok { r0 = rf(term, options) } else { if ret.Get(0) != nil { diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index 533e376b2..a3c1af5a3 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -16,6 +16,15 @@ import ( ) func TestUserStore(t *testing.T, ss store.Store) { + result := <-ss.User().GetAll() + require.Nil(t, result.Err, "failed cleaning up test users") + users := result.Data.([]*model.User) + + for _, u := range users { + result := <-ss.User().PermanentDelete(u.Id) + require.Nil(t, result.Err, "failed cleaning up test user %s", u.Username) + } + t.Run("Save", func(t *testing.T) { testUserStoreSave(t, ss) }) t.Run("Update", func(t *testing.T) { testUserStoreUpdate(t, ss) }) t.Run("UpdateUpdateAt", func(t *testing.T) { testUserStoreUpdateUpdateAt(t, ss) }) @@ -46,6 +55,9 @@ func TestUserStore(t *testing.T, ss store.Store) { t.Run("GetRecentlyActiveUsersForTeam", func(t *testing.T) { testUserStoreGetRecentlyActiveUsersForTeam(t, ss) }) t.Run("GetNewUsersForTeam", func(t *testing.T) { testUserStoreGetNewUsersForTeam(t, ss) }) t.Run("Search", func(t *testing.T) { testUserStoreSearch(t, ss) }) + t.Run("SearchNotInChannel", func(t *testing.T) { testUserStoreSearchNotInChannel(t, ss) }) + t.Run("SearchInChannel", func(t *testing.T) { testUserStoreSearchInChannel(t, ss) }) + t.Run("SearchNotInTeam", func(t *testing.T) { testUserStoreSearchNotInTeam(t, ss) }) t.Run("SearchWithoutTeam", func(t *testing.T) { testUserStoreSearchWithoutTeam(t, ss) }) t.Run("AnalyticsGetInactiveUsersCount", func(t *testing.T) { testUserStoreAnalyticsGetInactiveUsersCount(t, ss) }) t.Run("AnalyticsGetSystemAdminCount", func(t *testing.T) { testUserStoreAnalyticsGetSystemAdminCount(t, ss) }) @@ -65,6 +77,7 @@ func testUserStoreSave(t *testing.T, ss store.Store) { if err := (<-ss.User().Save(&u1)).Err; err != nil { t.Fatal("couldn't save user", err) } + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, maxUsersPerTeam)) @@ -95,6 +108,7 @@ func testUserStoreSave(t *testing.T, ss store.Store) { if err := (<-ss.User().Save(&u1)).Err; err != nil { t.Fatal("couldn't save item", err) } + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, maxUsersPerTeam)) } @@ -105,6 +119,7 @@ func testUserStoreSave(t *testing.T, ss store.Store) { if err := (<-ss.User().Save(&u1)).Err; err != nil { t.Fatal("couldn't save item", err) } + defer ss.User().PermanentDelete(u1.Id) if err := (<-ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, maxUsersPerTeam)).Err; err == nil { t.Fatal("should be the limit") @@ -115,12 +130,14 @@ func testUserStoreUpdate(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)) u2 := &model.User{} u2.Email = MakeEmail() u2.AuthService = "ldap" store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)) time.Sleep(100 * time.Millisecond) @@ -149,6 +166,7 @@ func testUserStoreUpdate(t *testing.T, ss store.Store) { oldEmail := u3.Email u3.AuthService = "gitlab" store.Must(ss.User().Save(u3)) + defer ss.User().PermanentDelete(u3.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u3.Id}, -1)) u3.Email = MakeEmail() @@ -180,6 +198,7 @@ func testUserStoreUpdateUpdateAt(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)) time.Sleep(10 * time.Millisecond) @@ -202,6 +221,7 @@ func testUserStoreUpdateFailedPasswordAttempts(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)) if err := (<-ss.User().UpdateFailedPasswordAttempts(u1.Id, 3)).Err; err != nil { @@ -222,6 +242,7 @@ func testUserStoreGet(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)) if r1 := <-ss.User().Get(u1.Id); r1.Err != nil { @@ -241,6 +262,7 @@ func testUserCount(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)) if result := <-ss.User().GetTotalUsersCount(); result.Err != nil { @@ -256,11 +278,13 @@ func testGetAllUsingAuthService(t *testing.T, ss store.Store) { u1.Email = MakeEmail() u1.AuthService = "someservice" store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) u2 := &model.User{} u2.Email = MakeEmail() u2.AuthService = "someservice" store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) if r1 := <-ss.User().GetAllUsingAuthService(u1.AuthService); r1.Err != nil { t.Fatal(r1.Err) @@ -276,10 +300,12 @@ func testUserStoreGetAllProfiles(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) u2 := &model.User{} u2.Email = MakeEmail() store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) if r1 := <-ss.User().GetAllProfiles(0, 100); r1.Err != nil { t.Fatal(r1.Err) @@ -318,6 +344,7 @@ func testUserStoreGetAllProfiles(t *testing.T, ss store.Store) { u3 := &model.User{} u3.Email = MakeEmail() store.Must(ss.User().Save(u3)) + defer ss.User().PermanentDelete(u3.Id) if r2 := <-ss.User().GetEtagForAllProfiles(); r2.Err != nil { t.Fatal(r2.Err) @@ -334,11 +361,13 @@ func testUserStoreGetProfiles(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)) u2 := &model.User{} u2.Email = MakeEmail() store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)) if r1 := <-ss.User().GetProfiles(teamId, 0, 100); r1.Err != nil { @@ -379,6 +408,7 @@ func testUserStoreGetProfiles(t *testing.T, ss store.Store) { u3 := &model.User{} u3.Email = MakeEmail() store.Must(ss.User().Save(u3)) + defer ss.User().PermanentDelete(u3.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)) if r2 := <-ss.User().GetEtagForProfiles(teamId); r2.Err != nil { @@ -396,11 +426,13 @@ func testUserStoreGetProfilesInChannel(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)) u2 := &model.User{} u2.Email = MakeEmail() store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)) c1 := model.Channel{} @@ -472,11 +504,13 @@ func testUserStoreGetProfilesInChannelByStatus(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)) u2 := &model.User{} u2.Email = MakeEmail() store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)) c1 := model.Channel{} @@ -589,11 +623,13 @@ func testUserStoreGetAllProfilesInChannel(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)) u2 := &model.User{} u2.Email = MakeEmail() store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)) c1 := model.Channel{} @@ -677,11 +713,13 @@ func testUserStoreGetProfilesNotInChannel(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)) u2 := &model.User{} u2.Email = MakeEmail() store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)) c1 := model.Channel{} @@ -770,11 +808,13 @@ func testUserStoreGetProfilesByIds(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)) u2 := &model.User{} u2.Email = MakeEmail() store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)) if r1 := <-ss.User().GetProfileByIds([]string{u1.Id}, false); r1.Err != nil { @@ -913,12 +953,14 @@ func testUserStoreGetProfilesByUsernames(t *testing.T, ss store.Store) { u1.Email = MakeEmail() u1.Username = "username1" + model.NewId() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)) u2 := &model.User{} u2.Email = MakeEmail() u2.Username = "username2" + model.NewId() store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)) if r1 := <-ss.User().GetProfilesByUsernames([]string{u1.Username, u2.Username}, teamId); r1.Err != nil { @@ -957,6 +999,7 @@ func testUserStoreGetProfilesByUsernames(t *testing.T, ss store.Store) { u3.Email = MakeEmail() u3.Username = "username3" + model.NewId() store.Must(ss.User().Save(u3)) + defer ss.User().PermanentDelete(u3.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: team2Id, UserId: u3.Id}, -1)) if r1 := <-ss.User().GetProfilesByUsernames([]string{u1.Username, u3.Username}, ""); r1.Err != nil { @@ -997,11 +1040,13 @@ func testUserStoreGetSystemAdminProfiles(t *testing.T, ss store.Store) { u1.Email = MakeEmail() u1.Roles = model.SYSTEM_USER_ROLE_ID + " " + model.SYSTEM_ADMIN_ROLE_ID store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)) u2 := &model.User{} u2.Email = MakeEmail() store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)) if r1 := <-ss.User().GetSystemAdminProfiles(); r1.Err != nil { @@ -1020,6 +1065,7 @@ func testUserStoreGetByEmail(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamid, UserId: u1.Id}, -1)) if err := (<-ss.User().GetByEmail(u1.Email)).Err; err != nil { @@ -1041,6 +1087,7 @@ func testUserStoreGetByAuthData(t *testing.T, ss store.Store) { u1.AuthData = &auth u1.AuthService = "service" store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)) if err := (<-ss.User().GetByAuth(u1.AuthData, u1.AuthService)).Err; err != nil { @@ -1060,6 +1107,7 @@ func testUserStoreGetByUsername(t *testing.T, ss store.Store) { u1.Email = MakeEmail() u1.Username = model.NewId() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)) if err := (<-ss.User().GetByUsername(u1.Username)).Err; err != nil { @@ -1081,6 +1129,7 @@ func testUserStoreGetForLogin(t *testing.T, ss store.Store) { AuthData: &auth, } store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) auth2 := model.NewId() @@ -1091,6 +1140,7 @@ func testUserStoreGetForLogin(t *testing.T, ss store.Store) { AuthData: &auth2, } store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) if result := <-ss.User().GetForLogin(u1.Username, true, true); result.Err != nil { t.Fatal("Should have gotten user by username", result.Err) @@ -1120,6 +1170,7 @@ func testUserStoreUpdatePassword(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)) hashedPassword := model.HashPassword("newpwd") @@ -1142,6 +1193,7 @@ func testUserStoreDelete(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)) if err := (<-ss.User().PermanentDelete(u1.Id)).Err; err != nil { @@ -1155,6 +1207,7 @@ func testUserStoreUpdateAuthData(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)) service := "someservice" @@ -1199,12 +1252,14 @@ func testUserUnreadCount(t *testing.T, ss store.Store) { u1.Username = "user1" + model.NewId() u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)) u2 := &model.User{} u2.Email = MakeEmail() u2.Username = "user2" + model.NewId() store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}, -1)) if err := (<-ss.Channel().Save(&c1, -1)).Err; err != nil { @@ -1275,6 +1330,7 @@ func testUserStoreUpdateMfaSecret(t *testing.T, ss store.Store) { u1 := model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(&u1)) + defer ss.User().PermanentDelete(u1.Id) time.Sleep(100 * time.Millisecond) @@ -1292,6 +1348,7 @@ func testUserStoreUpdateMfaActive(t *testing.T, ss store.Store) { u1 := model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(&u1)) + defer ss.User().PermanentDelete(u1.Id) time.Sleep(100 * time.Millisecond) @@ -1313,6 +1370,7 @@ func testUserStoreGetRecentlyActiveUsersForTeam(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Status().SaveOrUpdate(&model.Status{UserId: u1.Id, Status: model.STATUS_ONLINE, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: ""})) tid := model.NewId() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u1.Id}, -1)) @@ -1326,6 +1384,7 @@ func testUserStoreGetNewUsersForTeam(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Status().SaveOrUpdate(&model.Status{UserId: u1.Id, Status: model.STATUS_ONLINE, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: ""})) tid := model.NewId() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u1.Id}, -1)) @@ -1335,41 +1394,67 @@ func testUserStoreGetNewUsersForTeam(t *testing.T, ss store.Store) { } } +func assertUsers(t *testing.T, expected, actual []*model.User) { + expectedUsernames := make([]string, 0, len(expected)) + for _, user := range expected { + expectedUsernames = append(expectedUsernames, user.Username) + } + + actualUsernames := make([]string, 0, len(actual)) + for _, user := range actual { + actualUsernames = append(actualUsernames, user.Username) + } + + if assert.Equal(t, expectedUsernames, actualUsernames) { + assert.Equal(t, expected, actual) + } +} + func testUserStoreSearch(t *testing.T, ss store.Store) { - u1 := &model.User{} - u1.Username = "jimbo" + model.NewId() - u1.FirstName = "Tim" - u1.LastName = "Bill" - u1.Nickname = "Rob" - u1.Email = "harold" + model.NewId() + "@simulator.amazonses.com" + u1 := &model.User{ + Username: "jimbo1" + model.NewId(), + FirstName: "Tim", + LastName: "Bill", + Nickname: "Rob", + Email: "harold" + model.NewId() + "@simulator.amazonses.com", + } store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) - u2 := &model.User{} - u2.Username = "jim-bobby" + model.NewId() - u2.Email = MakeEmail() + u2 := &model.User{ + Username: "jim-bobby" + model.NewId(), + Email: MakeEmail(), + } store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) - u3 := &model.User{} - u3.Username = "jimbo" + model.NewId() - u3.Email = MakeEmail() - u3.DeleteAt = 1 + u3 := &model.User{ + Username: "jimbo3" + model.NewId(), + Email: MakeEmail(), + DeleteAt: 1, + } store.Must(ss.User().Save(u3)) + defer ss.User().PermanentDelete(u3.Id) - u5 := &model.User{} - u5.Username = "yu" + model.NewId() - u5.FirstName = "En" - u5.LastName = "Yu" - u5.Nickname = "enyu" - u5.Email = MakeEmail() + u5 := &model.User{ + Username: "yu" + model.NewId(), + FirstName: "En", + LastName: "Yu", + Nickname: "enyu", + Email: MakeEmail(), + } store.Must(ss.User().Save(u5)) + defer ss.User().PermanentDelete(u5.Id) - u6 := &model.User{} - u6.Username = "underscore" + model.NewId() - u6.FirstName = "Du_" - u6.LastName = "_DE" - u6.Nickname = "lodash" - u6.Email = MakeEmail() + u6 := &model.User{ + Username: "underscore" + model.NewId(), + FirstName: "Du_", + LastName: "_DE", + Nickname: "lodash", + Email: MakeEmail(), + } store.Must(ss.User().Save(u6)) + defer ss.User().PermanentDelete(u6.Id) tid := model.NewId() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u1.Id}, -1)) @@ -1378,550 +1463,855 @@ func testUserStoreSearch(t *testing.T, ss store.Store) { store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u5.Id}, -1)) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u6.Id}, -1)) - searchOptions := map[string]bool{} - searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true - - if r1 := <-ss.User().Search(tid, "jimb", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found1 := false - found2 := false - for _, profile := range profiles { - if profile.Id == u1.Id { - found1 = true - } - - if profile.Id == u3.Id { - found2 = true - } - } - - if !found1 { - t.Fatal("should have found user") - } - - if found2 { - t.Fatal("should not have found inactive user") - } - } - - if r1 := <-ss.User().Search(tid, "en", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found1 := false - for _, profile := range profiles { - if profile.Id == u5.Id { - found1 = true - } - } + // The users returned from the database will have AuthData as an empty string. + nilAuthData := new(string) + *nilAuthData = "" + + u1.AuthData = nilAuthData + u2.AuthData = nilAuthData + u3.AuthData = nilAuthData + u5.AuthData = nilAuthData + u6.AuthData = nilAuthData + + testCases := []struct { + Description string + TeamId string + Term string + Options *model.UserSearchOptions + Expected []*model.User + }{ + { + "search jimb", + tid, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "search en", + tid, + "en", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u5}, + }, + { + "search email", + tid, + u1.Email, + &model.UserSearchOptions{ + AllowEmails: true, + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "search maps * to space", + tid, + "jimb*", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "should not return spurious matches", + tid, + "harol", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{}, + }, + { + "% should be escaped", + tid, + "h%", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{}, + }, + { + "_ should be escaped", + tid, + "h_", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{}, + }, + { + "_ should be escaped (2)", + tid, + "Du_", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u6}, + }, + { + "_ should be escaped (2)", + tid, + "_dE", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u6}, + }, + { + "search jimb, allowing inactive", + tid, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowInactive: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1, u3}, + }, + { + "search jimb, no team id", + "", + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "search jim-bobb, no team id", + "", + "jim-bobb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u2}, + }, + + { + "search harol, search all fields", + tid, + "harol", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowEmails: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "search Tim, search all fields", + tid, + "Tim", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowEmails: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "search Tim, don't search full names", + tid, + "Tim", + &model.UserSearchOptions{ + AllowFullNames: false, + AllowEmails: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{}, + }, + { + "search Bill, search all fields", + tid, + "Bill", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowEmails: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "search Rob, search all fields", + tid, + "Rob", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowEmails: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + result := <-ss.User().Search(testCase.TeamId, testCase.Term, testCase.Options) + require.Nil(t, result.Err) + assertUsers(t, testCase.Expected, result.Data.([]*model.User)) + }) + } + + t.Run("search empty string", func(t *testing.T) { + searchOptions := &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + } + + r1 := <-ss.User().Search(tid, "", searchOptions) + require.Nil(t, r1.Err) + assert.Len(t, r1.Data.([]*model.User), 4) + // Don't assert contents, since Postgres' default collation order is left up to + // the operating system, and jimbo1 might sort before or after jim-bo. + // assertUsers(t, []*model.User{u2, u1, u6, u5}, r1.Data.([]*model.User)) + }) + + t.Run("search empty string, limit 2", func(t *testing.T) { + searchOptions := &model.UserSearchOptions{ + AllowFullNames: true, + Limit: 2, + } + + r1 := <-ss.User().Search(tid, "", searchOptions) + require.Nil(t, r1.Err) + assert.Len(t, r1.Data.([]*model.User), 2) + // Don't assert contents, since Postgres' default collation order is left up to + // the operating system, and jimbo1 might sort before or after jim-bo. + // assertUsers(t, []*model.User{u2, u1, u6, u5}, r1.Data.([]*model.User)) + }) +} - if !found1 { - t.Fatal("should have found user") - } +func testUserStoreSearchNotInChannel(t *testing.T, ss store.Store) { + u1 := &model.User{ + Username: "jimbo1" + model.NewId(), + FirstName: "Tim", + LastName: "Bill", + Nickname: "Rob", + Email: "harold" + model.NewId() + "@simulator.amazonses.com", } + store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) - searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = false - - if r1 := <-ss.User().Search(tid, u1.Email, searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found1 := false - for _, profile := range profiles { - if profile.Id == u1.Id { - found1 = true - } - } - - if !found1 { - t.Fatal("should have found user") - } + u2 := &model.User{ + Username: "jim2-bobby" + model.NewId(), + Email: MakeEmail(), } + store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) - searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true - - // * should be treated as a space - if r1 := <-ss.User().Search(tid, "jimb*", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found1 := false - found2 := false - for _, profile := range profiles { - if profile.Id == u1.Id { - found1 = true - } - - if profile.Id == u3.Id { - found2 = true - } - } - - if !found1 { - t.Fatal("should have found user") - } - - if found2 { - t.Fatal("should not have found inactive user") - } + u3 := &model.User{ + Username: "jimbo3" + model.NewId(), + Email: MakeEmail(), + DeleteAt: 1, } + store.Must(ss.User().Save(u3)) + defer ss.User().PermanentDelete(u3.Id) - if r1 := <-ss.User().Search(tid, "harol", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found1 := false - for _, profile := range profiles { - if profile.Id == u1.Id { - found1 = true - } - } + tid := model.NewId() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u1.Id}, -1)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u2.Id}, -1)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u3.Id}, -1)) - if found1 { - t.Fatal("should not have found user") - } - } + // The users returned from the database will have AuthData as an empty string. + nilAuthData := new(string) + *nilAuthData = "" - // % should be escaped and searched for. - if r1 := <-ss.User().Search(tid, "h%", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - if len(profiles) != 0 { - t.Fatal("shouldn't have found anything") - } - } + u1.AuthData = nilAuthData + u2.AuthData = nilAuthData + u3.AuthData = nilAuthData - // "_" should be properly escaped and searched for. - if r1 := <-ss.User().Search(tid, "h_", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - if len(profiles) != 0 { - t.Fatal("shouldn't have found anything") - } + c1 := model.Channel{ + TeamId: tid, + DisplayName: "NameName", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, } - if r1 := <-ss.User().Search(tid, "Du_", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found6 := false - for _, profile := range profiles { - if profile.Id == u6.Id { - found6 = true - } - } + c1 = *store.Must(ss.Channel().Save(&c1, -1)).(*model.Channel) - if !found6 { - t.Fatal("should have found user") - } + c2 := model.Channel{ + TeamId: tid, + DisplayName: "NameName", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + } + c2 = *store.Must(ss.Channel().Save(&c2, -1)).(*model.Channel) + + store.Must(ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: c2.Id, + UserId: u1.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + })) + store.Must(ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: c1.Id, + UserId: u3.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + })) + store.Must(ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: c2.Id, + UserId: u2.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + })) + + testCases := []struct { + Description string + TeamId string + ChannelId string + Term string + Options *model.UserSearchOptions + Expected []*model.User + }{ + { + "search jimb, channel 1", + tid, + c1.Id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "search jimb, allow inactive, channel 1", + tid, + c1.Id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowInactive: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "search jimb, channel 1, no team id", + "", + c1.Id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "search jimb, channel 1, junk team id", + "junk", + c1.Id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{}, + }, + { + "search jimb, channel 2", + tid, + c2.Id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{}, + }, + { + "search jimb, allow inactive, channel 2", + tid, + c2.Id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowInactive: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u3}, + }, + { + "search jimb, channel 2, no team id", + "", + c2.Id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{}, + }, + { + "search jimb, channel 2, junk team id", + "junk", + c2.Id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{}, + }, + { + "search jim, channel 1", + tid, + c1.Id, + "jim", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u2, u1}, + }, + { + "search jim, channel 1, limit 1", + tid, + c1.Id, + "jim", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: 1, + }, + []*model.User{u2}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + result := <-ss.User().SearchNotInChannel( + testCase.TeamId, + testCase.ChannelId, + testCase.Term, + testCase.Options, + ) + require.Nil(t, result.Err) + assertUsers(t, testCase.Expected, result.Data.([]*model.User)) + }) } - if r1 := <-ss.User().Search(tid, "_dE", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found6 := false - for _, profile := range profiles { - if profile.Id == u6.Id { - found6 = true - } - } +} - if !found6 { - t.Fatal("should have found user") - } +func testUserStoreSearchInChannel(t *testing.T, ss store.Store) { + u1 := &model.User{ + Username: "jimbo1" + model.NewId(), + FirstName: "Tim", + LastName: "Bill", + Nickname: "Rob", + Email: "harold" + model.NewId() + "@simulator.amazonses.com", } + store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) - searchOptions[store.USER_SEARCH_OPTION_ALLOW_INACTIVE] = true - - if r1 := <-ss.User().Search(tid, "jimb", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found1 := false - found2 := false - for _, profile := range profiles { - if profile.Id == u1.Id { - found1 = true - } - - if profile.Id == u3.Id { - found2 = true - } - } - - if !found1 { - t.Fatal("should have found user") - } - - if !found2 { - t.Fatal("should have found inactive user") - } + u2 := &model.User{ + Username: "jim-bobby" + model.NewId(), + Email: MakeEmail(), } + store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) - searchOptions[store.USER_SEARCH_OPTION_ALLOW_INACTIVE] = false - - if r1 := <-ss.User().Search(tid, "jimb", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found := false - for _, profile := range profiles { - if profile.Id == u1.Id { - found = true - break - } - } - - if !found { - t.Fatal("should have found user") - } + u3 := &model.User{ + Username: "jimbo3" + model.NewId(), + Email: MakeEmail(), + DeleteAt: 1, } + store.Must(ss.User().Save(u3)) + defer ss.User().PermanentDelete(u3.Id) - if r1 := <-ss.User().Search("", "jimb", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found := false - for _, profile := range profiles { - if profile.Id == u1.Id { - found = true - break - } - } - - if !found { - t.Fatal("should have found user") - } - } + tid := model.NewId() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u1.Id}, -1)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u2.Id}, -1)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u3.Id}, -1)) - if r1 := <-ss.User().Search("", "jim-bobb", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found := false - for _, profile := range profiles { - t.Log(profile.Username) - if profile.Id == u2.Id { - found = true - break - } - } + // The users returned from the database will have AuthData as an empty string. + nilAuthData := new(string) + *nilAuthData = "" - if !found { - t.Fatal("should have found user") - } - } + u1.AuthData = nilAuthData + u2.AuthData = nilAuthData + u3.AuthData = nilAuthData - if r1 := <-ss.User().Search(tid, "", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) + c1 := model.Channel{ + TeamId: tid, + DisplayName: "NameName", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, } - - c1 := model.Channel{} - c1.TeamId = tid - c1.DisplayName = "NameName" - c1.Name = "zz" + model.NewId() + "b" - c1.Type = model.CHANNEL_OPEN c1 = *store.Must(ss.Channel().Save(&c1, -1)).(*model.Channel) - if r1 := <-ss.User().SearchNotInChannel(tid, c1.Id, "jimb", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found := false - for _, profile := range profiles { - if profile.Id == u1.Id { - found = true - break - } - } - - if !found { - t.Fatal("should have found user") - } - } - - if r1 := <-ss.User().SearchNotInChannel("", c1.Id, "jimb", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found := false - for _, profile := range profiles { - if profile.Id == u1.Id { - found = true - break - } - } - - if !found { - t.Fatal("should have found user") - } - } - - if r1 := <-ss.User().SearchNotInChannel("junk", c1.Id, "jimb", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found := false - for _, profile := range profiles { - if profile.Id == u1.Id { - found = true - break - } - } - - if found { - t.Fatal("should not have found user") - } - } - - if r1 := <-ss.User().SearchInChannel(c1.Id, "jimb", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found := false - for _, profile := range profiles { - if profile.Id == u1.Id { - found = true - break - } - } - - if found { - t.Fatal("should not have found user") - } + c2 := model.Channel{ + TeamId: tid, + DisplayName: "NameName", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + } + c2 = *store.Must(ss.Channel().Save(&c2, -1)).(*model.Channel) + + store.Must(ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: c1.Id, + UserId: u1.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + })) + store.Must(ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: c2.Id, + UserId: u2.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + })) + store.Must(ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: c1.Id, + UserId: u3.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + })) + + testCases := []struct { + Description string + ChannelId string + Term string + Options *model.UserSearchOptions + Expected []*model.User + }{ + { + "search jimb, channel 1", + c1.Id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "search jimb, allow inactive, channel 1", + c1.Id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowInactive: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1, u3}, + }, + { + "search jimb, allow inactive, channel 1, limit 1", + c1.Id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowInactive: true, + Limit: 1, + }, + []*model.User{u1}, + }, + { + "search jimb, channel 2", + c2.Id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{}, + }, + { + "search jimb, allow inactive, channel 2", + c2.Id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowInactive: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + result := <-ss.User().SearchInChannel( + testCase.ChannelId, + testCase.Term, + testCase.Options, + ) + require.Nil(t, result.Err) + assertUsers(t, testCase.Expected, result.Data.([]*model.User)) + }) } +} - store.Must(ss.Channel().SaveMember(&model.ChannelMember{ChannelId: c1.Id, UserId: u1.Id, NotifyProps: model.GetDefaultChannelNotifyProps()})) - - if r1 := <-ss.User().SearchInChannel(c1.Id, "jimb", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found := false - for _, profile := range profiles { - if profile.Id == u1.Id { - found = true - break - } - } - - if !found { - t.Fatal("should have found user") - } - } - - searchOptions = map[string]bool{} - - if r1 := <-ss.User().Search(tid, "harol", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found1 := false - for _, profile := range profiles { - if profile.Id == u1.Id { - found1 = true - } - } - - if !found1 { - t.Fatal("should have found user") - } +func testUserStoreSearchNotInTeam(t *testing.T, ss store.Store) { + u1 := &model.User{ + Username: "jimbo1" + model.NewId(), + FirstName: "Tim", + LastName: "Bill", + Nickname: "Rob", + Email: "harold" + model.NewId() + "@simulator.amazonses.com", } + store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) - if r1 := <-ss.User().Search(tid, "Tim", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found := false - for _, profile := range profiles { - if profile.Id == u1.Id { - found = true - break - } - } - - if !found { - t.Fatal("should have found user") - } + u2 := &model.User{ + Username: "jim-bobby" + model.NewId(), + Email: MakeEmail(), } + store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) - if r1 := <-ss.User().Search(tid, "Bill", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found := false - for _, profile := range profiles { - if profile.Id == u1.Id { - found = true - break - } - } - - if !found { - t.Fatal("should have found user") - } + u3 := &model.User{ + Username: "jimbo3" + model.NewId(), + Email: MakeEmail(), + DeleteAt: 1, } + store.Must(ss.User().Save(u3)) + defer ss.User().PermanentDelete(u3.Id) - if r1 := <-ss.User().Search(tid, "Rob", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found := false - for _, profile := range profiles { - if profile.Id == u1.Id { - found = true - break - } - } - - if !found { - t.Fatal("should have found user") - } + u4 := &model.User{ + Username: "simon" + model.NewId(), + Email: MakeEmail(), + DeleteAt: 0, } - - // Search Users not in Team. - u4 := &model.User{} - u4.Username = "simon" + model.NewId() - u4.Email = MakeEmail() - u4.DeleteAt = 0 store.Must(ss.User().Save(u4)) + defer ss.User().PermanentDelete(u4.Id) - if r1 := <-ss.User().SearchNotInTeam(tid, "simo", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found := false - for _, profile := range profiles { - if profile.Id == u4.Id { - found = true - break - } - } - - if !found { - t.Fatal("should have found user") - } - } - - if r1 := <-ss.User().SearchNotInTeam(tid, "jimb", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found := false - for _, profile := range profiles { - if profile.Id == u1.Id { - found = true - break - } - } - - if found { - t.Fatal("should not have found user") - } + u5 := &model.User{ + Username: "yu" + model.NewId(), + FirstName: "En", + LastName: "Yu", + Nickname: "enyu", + Email: MakeEmail(), } + store.Must(ss.User().Save(u5)) + defer ss.User().PermanentDelete(u5.Id) - // Check SearchNotInTeam finds previously deleted team members. - store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u4.Id}, -1)) - - if r1 := <-ss.User().SearchNotInTeam(tid, "simo", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found := false - for _, profile := range profiles { - if profile.Id == u4.Id { - found = true - break - } - } - - if found { - t.Fatal("should not have found user") - } + u6 := &model.User{ + Username: "underscore" + model.NewId(), + FirstName: "Du_", + LastName: "_DE", + Nickname: "lodash", + Email: MakeEmail(), } - - store.Must(ss.Team().UpdateMember(&model.TeamMember{TeamId: tid, UserId: u4.Id, DeleteAt: model.GetMillis() - 1000})) - if r1 := <-ss.User().SearchNotInTeam(tid, "simo", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - found := false - for _, profile := range profiles { - if profile.Id == u4.Id { - found = true - break - } - } - - if !found { - t.Fatal("should have found user") - } + store.Must(ss.User().Save(u6)) + defer ss.User().PermanentDelete(u6.Id) + + teamId1 := model.NewId() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: u1.Id}, -1)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: u2.Id}, -1)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: u3.Id}, -1)) + // u4 is not in team 1 + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: u5.Id}, -1)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId1, UserId: u6.Id}, -1)) + + teamId2 := model.NewId() + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId2, UserId: u4.Id}, -1)) + + // The users returned from the database will have AuthData as an empty string. + nilAuthData := new(string) + *nilAuthData = "" + + u1.AuthData = nilAuthData + u2.AuthData = nilAuthData + u3.AuthData = nilAuthData + u4.AuthData = nilAuthData + u5.AuthData = nilAuthData + u6.AuthData = nilAuthData + + testCases := []struct { + Description string + TeamId string + Term string + Options *model.UserSearchOptions + Expected []*model.User + }{ + { + "search simo, team 1", + teamId1, + "simo", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u4}, + }, + + { + "search jimb, team 1", + teamId1, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{}, + }, + { + "search jimb, allow inactive, team 1", + teamId1, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowInactive: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{}, + }, + { + "search simo, team 2", + teamId2, + "simo", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{}, + }, + { + "search jimb, team1", + teamId2, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "search jimb, allow inactive, team 2", + teamId2, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowInactive: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1, u3}, + }, + { + "search jimb, allow inactive, team 2, limit 1", + teamId2, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowInactive: true, + Limit: 1, + }, + []*model.User{u1}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + result := <-ss.User().SearchNotInTeam( + testCase.TeamId, + testCase.Term, + testCase.Options, + ) + require.Nil(t, result.Err) + assertUsers(t, testCase.Expected, result.Data.([]*model.User)) + }) } - - // Check PLT-8354 - search that ends up with just space for terms doesn't error. - r1 := <-ss.User().SearchWithoutTeam("* ", searchOptions) - assert.Nil(t, r1.Err) } func testUserStoreSearchWithoutTeam(t *testing.T, ss store.Store) { - u1 := &model.User{} - u1.Username = "jimbo" + model.NewId() - u1.FirstName = "Tim" - u1.LastName = "Bill" - u1.Nickname = "Rob" - u1.Email = "harold" + model.NewId() + "@simulator.amazonses.com" + u1 := &model.User{ + Username: "jimbo1" + model.NewId(), + FirstName: "Tim", + LastName: "Bill", + Nickname: "Rob", + Email: "harold" + model.NewId() + "@simulator.amazonses.com", + } store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) - u2 := &model.User{} - u2.Username = "jim-bobby" + model.NewId() - u2.Email = MakeEmail() + u2 := &model.User{ + Username: "jim2-bobby" + model.NewId(), + Email: MakeEmail(), + } store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) - u3 := &model.User{} - u3.Username = "jimbo" + model.NewId() - u3.Email = MakeEmail() - u3.DeleteAt = 1 + u3 := &model.User{ + Username: "jimbo3" + model.NewId(), + Email: MakeEmail(), + DeleteAt: 1, + } store.Must(ss.User().Save(u3)) + defer ss.User().PermanentDelete(u3.Id) tid := model.NewId() store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u3.Id}, -1)) - searchOptions := map[string]bool{} - searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true - - if r1 := <-ss.User().SearchWithoutTeam("", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } - - if r1 := <-ss.User().SearchWithoutTeam("jim", searchOptions); r1.Err != nil { - t.Fatal(r1.Err) - } else { - profiles := r1.Data.([]*model.User) - - found1 := false - found2 := false - found3 := false - - for _, profile := range profiles { - if profile.Id == u1.Id { - found1 = true - } else if profile.Id == u2.Id { - found2 = true - } else if profile.Id == u3.Id { - found3 = true - } - } - - if !found1 { - t.Fatal("should have found user1") - } else if !found2 { - t.Fatal("should have found user2") - } else if found3 { - t.Fatal("should not have found user3") - } + // The users returned from the database will have AuthData as an empty string. + nilAuthData := new(string) + *nilAuthData = "" + + u1.AuthData = nilAuthData + u2.AuthData = nilAuthData + u3.AuthData = nilAuthData + + testCases := []struct { + Description string + Term string + Options *model.UserSearchOptions + Expected []*model.User + }{ + { + "empty string", + "", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u2, u1}, + }, + { + "jim", + "jim", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u2, u1}, + }, + { + "PLT-8354", + "* ", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u2, u1}, + }, + { + "jim, limit 1", + "jim", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: 1, + }, + []*model.User{u2}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + result := <-ss.User().SearchWithoutTeam( + testCase.Term, + testCase.Options, + ) + require.Nil(t, result.Err) + assertUsers(t, testCase.Expected, result.Data.([]*model.User)) + }) } } @@ -1929,6 +2319,7 @@ func testUserStoreAnalyticsGetInactiveUsersCount(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) var count int64 @@ -1942,6 +2333,7 @@ func testUserStoreAnalyticsGetInactiveUsersCount(t *testing.T, ss store.Store) { u2.Email = MakeEmail() u2.DeleteAt = model.GetMillis() store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) if result := <-ss.User().AnalyticsGetInactiveUsersCount(); result.Err != nil { t.Fatal(result.Err) @@ -1973,10 +2365,12 @@ func testUserStoreAnalyticsGetSystemAdminCount(t *testing.T, ss store.Store) { if err := (<-ss.User().Save(&u1)).Err; err != nil { t.Fatal("couldn't save user", err) } + defer ss.User().PermanentDelete(u1.Id) if err := (<-ss.User().Save(&u2)).Err; err != nil { t.Fatal("couldn't save user", err) } + defer ss.User().PermanentDelete(u2.Id) if result := <-ss.User().AnalyticsGetSystemAdminCount(); result.Err != nil { t.Fatal(result.Err) @@ -1994,12 +2388,14 @@ func testUserStoreGetProfilesNotInTeam(t *testing.T, ss store.Store) { u1 := &model.User{} u1.Email = MakeEmail() store.Must(ss.User().Save(u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}, -1)) store.Must(ss.User().UpdateUpdateAt(u1.Id)) u2 := &model.User{} u2.Email = MakeEmail() store.Must(ss.User().Save(u2)) + defer ss.User().PermanentDelete(u2.Id) store.Must(ss.User().UpdateUpdateAt(u2.Id)) var initialUsersNotInTeam int @@ -2106,6 +2502,7 @@ func testUserStoreGetProfilesNotInTeam(t *testing.T, ss store.Store) { u3 := &model.User{} u3.Email = MakeEmail() store.Must(ss.User().Save(u3)) + defer ss.User().PermanentDelete(u3.Id) store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u3.Id}, -1)) store.Must(ss.User().UpdateUpdateAt(u3.Id)) @@ -2143,9 +2540,13 @@ func testUserStoreClearAllCustomRoleAssignments(t *testing.T, ss store.Store) { } store.Must(ss.User().Save(&u1)) + defer ss.User().PermanentDelete(u1.Id) store.Must(ss.User().Save(&u2)) + defer ss.User().PermanentDelete(u2.Id) store.Must(ss.User().Save(&u3)) + defer ss.User().PermanentDelete(u3.Id) store.Must(ss.User().Save(&u4)) + defer ss.User().PermanentDelete(u4.Id) require.Nil(t, (<-ss.User().ClearAllCustomRoleAssignments()).Err) @@ -2173,6 +2574,7 @@ func testUserStoreGetAllAfter(t *testing.T, ss store.Store) { Roles: "system_user system_admin system_post_all", } store.Must(ss.User().Save(&u1)) + defer ss.User().PermanentDelete(u1.Id) r1 := <-ss.User().GetAllAfter(10000, strings.Repeat("0", 26)) require.Nil(t, r1.Err) -- cgit v1.2.3-1-g7c22