diff --git a/api4/apitestlib.go b/api4/apitestlib.go index 6b4aedb35a..01ad3b4f51 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -19,6 +19,7 @@ import ( "github.com/mattermost/mattermost-server/v5/config" "github.com/mattermost/mattermost-server/v5/mlog" "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/services/searchengine" "github.com/mattermost/mattermost-server/v5/store" "github.com/mattermost/mattermost-server/v5/store/localcachelayer" "github.com/mattermost/mattermost-server/v5/store/storetest/mocks" @@ -63,7 +64,7 @@ func SetMainHelper(mh *testlib.MainHelper) { mainHelper = mh } -func setupTestHelper(dbStore store.Store, enterprise bool, includeCache bool, updateConfig func(*model.Config)) *TestHelper { +func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, enterprise bool, includeCache bool, updateConfig func(*model.Config)) *TestHelper { tempWorkspace, err := ioutil.TempDir("", "apptest") if err != nil { panic(err) @@ -102,6 +103,10 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCache bool, up IncludeCacheLayer: includeCache, } + if searchEngine != nil { + th.App.SetSearchEngine(searchEngine) + } + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.MaxUsersPerTeam = 50 *cfg.RateLimitSettings.Enable = false @@ -163,7 +168,8 @@ func SetupEnterprise(tb testing.TB) *TestHelper { dbStore := mainHelper.GetStore() dbStore.DropAllTables() dbStore.MarkSystemRanUnitTests() - return setupTestHelper(dbStore, true, true, nil) + searchEngine := mainHelper.GetSearchEngine() + return setupTestHelper(dbStore, searchEngine, true, true, nil) } func Setup(tb testing.TB) *TestHelper { @@ -178,7 +184,8 @@ func Setup(tb testing.TB) *TestHelper { dbStore := mainHelper.GetStore() dbStore.DropAllTables() dbStore.MarkSystemRanUnitTests() - return setupTestHelper(dbStore, false, true, nil) + searchEngine := mainHelper.GetSearchEngine() + return setupTestHelper(dbStore, searchEngine, false, true, nil) } func SetupConfig(tb testing.TB, updateConfig func(cfg *model.Config)) *TestHelper { @@ -193,11 +200,12 @@ func SetupConfig(tb testing.TB, updateConfig func(cfg *model.Config)) *TestHelpe dbStore := mainHelper.GetStore() dbStore.DropAllTables() dbStore.MarkSystemRanUnitTests() - return setupTestHelper(dbStore, false, true, updateConfig) + searchEngine := mainHelper.GetSearchEngine() + return setupTestHelper(dbStore, searchEngine, false, true, updateConfig) } func SetupConfigWithStoreMock(tb testing.TB, updateConfig func(cfg *model.Config)) *TestHelper { - th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), false, false, updateConfig) + th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), nil, false, false, updateConfig) emptyMockStore := mocks.Store{} emptyMockStore.On("Close").Return(nil) th.App.Srv().Store = &emptyMockStore @@ -205,7 +213,7 @@ func SetupConfigWithStoreMock(tb testing.TB, updateConfig func(cfg *model.Config } func SetupWithStoreMock(tb testing.TB) *TestHelper { - th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), false, false, nil) + th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), nil, false, false, nil) emptyMockStore := mocks.Store{} emptyMockStore.On("Close").Return(nil) th.App.Srv().Store = &emptyMockStore @@ -213,7 +221,7 @@ func SetupWithStoreMock(tb testing.TB) *TestHelper { } func SetupEnterpriseWithStoreMock(tb testing.TB) *TestHelper { - th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), true, false, nil) + th := setupTestHelper(testlib.GetMockStoreForSetupFunctions(), nil, true, false, nil) emptyMockStore := mocks.Store{} emptyMockStore.On("Close").Return(nil) th.App.Srv().Store = &emptyMockStore diff --git a/app/searchengine.go b/app/searchengine.go index d1eae5123c..81b0634fa9 100644 --- a/app/searchengine.go +++ b/app/searchengine.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/services/searchengine" ) func (a *App) TestElasticsearch(cfg *model.Config) *model.AppError { @@ -43,3 +44,7 @@ func (a *App) PurgeElasticsearchIndexes() *model.AppError { return nil } + +func (a *App) SetSearchEngine(se *searchengine.Broker) { + a.searchEngine = se +} diff --git a/i18n/en.json b/i18n/en.json index 7e89b8853d..9bb5801a0d 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3970,6 +3970,10 @@ "id": "ent.elasticsearch.purge_indexes.delete_failed", "translation": "Failed to delete Elasticsearch index" }, + { + "id": "ent.elasticsearch.refresh_indexes.refresh_failed", + "translation": "Failed to refresh Elasticsearch indexes" + }, { "id": "ent.elasticsearch.search_channels.disabled", "translation": "Elasticsearch searching is disabled on this server" @@ -5682,6 +5686,10 @@ "id": "store.insert_error", "translation": "insert error" }, + { + "id": "store.search_user_store.empty_team_id", + "translation": "Failed to get list of allowed channels for team: empty teamId" + }, { "id": "store.select_error", "translation": "select error" diff --git a/services/searchengine/interface.go b/services/searchengine/interface.go index 3f15c58de6..54c76861ec 100644 --- a/services/searchengine/interface.go +++ b/services/searchengine/interface.go @@ -19,6 +19,7 @@ type SearchEngineInterface interface { IsIndexingEnabled() bool IsSearchEnabled() bool IsAutocompletionEnabled() bool + IsIndexingSync() bool IndexPost(post *model.Post, teamId string) *model.AppError SearchPosts(channels *model.ChannelList, searchParams []*model.SearchParams, page, perPage int) ([]string, model.PostSearchMatches, *model.AppError) DeletePost(post *model.Post) *model.AppError @@ -31,5 +32,6 @@ type SearchEngineInterface interface { DeleteUser(user *model.User) *model.AppError TestConfig(cfg *model.Config) *model.AppError PurgeIndexes() *model.AppError + RefreshIndexes() *model.AppError DataRetentionDeleteIndexes(cutoff time.Time) *model.AppError } diff --git a/services/searchengine/mocks/SearchEngineInterface.go b/services/searchengine/mocks/SearchEngineInterface.go index a04edd64a1..6e98562025 100644 --- a/services/searchengine/mocks/SearchEngineInterface.go +++ b/services/searchengine/mocks/SearchEngineInterface.go @@ -5,10 +5,10 @@ package mocks import ( - time "time" - model "github.com/mattermost/mattermost-server/v5/model" mock "github.com/stretchr/testify/mock" + + time "time" ) // SearchEngineInterface is an autogenerated mock type for the SearchEngineInterface type @@ -198,6 +198,20 @@ func (_m *SearchEngineInterface) IsIndexingEnabled() bool { return r0 } +// IsIndexingSync provides a mock function with given fields: +func (_m *SearchEngineInterface) IsIndexingSync() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + // IsSearchEnabled provides a mock function with given fields: func (_m *SearchEngineInterface) IsSearchEnabled() bool { ret := _m.Called() @@ -228,6 +242,22 @@ func (_m *SearchEngineInterface) PurgeIndexes() *model.AppError { return r0 } +// RefreshIndexes provides a mock function with given fields: +func (_m *SearchEngineInterface) RefreshIndexes() *model.AppError { + ret := _m.Called() + + var r0 *model.AppError + if rf, ok := ret.Get(0).(func() *model.AppError); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) + } + } + + return r0 +} + // SearchChannels provides a mock function with given fields: teamId, term func (_m *SearchEngineInterface) SearchChannels(teamId string, term string) ([]string, *model.AppError) { ret := _m.Called(teamId, term) diff --git a/store/searchlayer/channel_layer.go b/store/searchlayer/channel_layer.go index d5214a3929..a9c8279bc0 100644 --- a/store/searchlayer/channel_layer.go +++ b/store/searchlayer/channel_layer.go @@ -19,12 +19,12 @@ func (c *SearchChannelStore) deleteChannelIndex(channel *model.Channel) { if channel.Type == model.CHANNEL_OPEN { for _, engine := range c.rootStore.searchEngine.GetActiveEngines() { if engine.IsIndexingEnabled() { - go (func(engineCopy searchengine.SearchEngineInterface) { + runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) { if err := engineCopy.DeleteChannel(channel); err != nil { mlog.Error("Encountered error deleting channel", mlog.String("channel_id", channel.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err)) } mlog.Debug("Removed channel from index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("channel_id", channel.Id)) - })(engine) + }) } } } @@ -34,12 +34,12 @@ func (c *SearchChannelStore) indexChannel(channel *model.Channel) { if channel.Type == model.CHANNEL_OPEN { for _, engine := range c.rootStore.searchEngine.GetActiveEngines() { if engine.IsIndexingEnabled() { - go (func(engineCopy searchengine.SearchEngineInterface) { + runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) { if err := engineCopy.IndexChannel(channel); err != nil { mlog.Error("Encountered error indexing channel", mlog.String("channel_id", channel.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err)) } mlog.Debug("Indexed channel in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("channel_id", channel.Id)) - })(engine) + }) } } } diff --git a/store/searchlayer/layer.go b/store/searchlayer/layer.go index b843f5c1a8..1239122e85 100644 --- a/store/searchlayer/layer.go +++ b/store/searchlayer/layer.go @@ -59,7 +59,7 @@ func (s SearchStore) indexUserFromID(userId string) { func (s SearchStore) indexUser(user *model.User) { for _, engine := range s.searchEngine.GetActiveEngines() { if engine.IsIndexingEnabled() { - go (func(engineCopy searchengine.SearchEngineInterface) { + runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) { userTeams, err := s.Team().GetTeamsByUserId(user.Id) if err != nil { mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err)) @@ -87,7 +87,21 @@ func (s SearchStore) indexUser(user *model.User) { return } mlog.Debug("Indexed user in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("user_id", user.Id)) - })(engine) + }) } } } + +// Runs an indexing function synchronously or asynchronously depending on the engine +func runIndexFn(engine searchengine.SearchEngineInterface, indexFn func(searchengine.SearchEngineInterface)) { + if engine.IsIndexingSync() { + indexFn(engine) + if err := engine.RefreshIndexes(); err != nil { + mlog.Error("Encountered error refresh the indexes", mlog.Err(err)) + } + } else { + go (func(engineCopy searchengine.SearchEngineInterface) { + indexFn(engineCopy) + })(engine) + } +} diff --git a/store/searchlayer/post_layer.go b/store/searchlayer/post_layer.go index 74723f4585..2be7ad978b 100644 --- a/store/searchlayer/post_layer.go +++ b/store/searchlayer/post_layer.go @@ -18,7 +18,7 @@ type SearchPostStore struct { func (s SearchPostStore) indexPost(post *model.Post) { for _, engine := range s.rootStore.searchEngine.GetActiveEngines() { if engine.IsIndexingEnabled() { - go (func(engineCopy searchengine.SearchEngineInterface) { + runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) { channel, chanErr := s.rootStore.Channel().Get(post.ChannelId, true) if chanErr != nil { mlog.Error("Couldn't get channel for post for SearchEngine indexing.", mlog.String("channel_id", post.ChannelId), mlog.String("search_engine", engineCopy.GetName()), mlog.String("post_id", post.Id), mlog.Err(chanErr)) @@ -28,7 +28,7 @@ func (s SearchPostStore) indexPost(post *model.Post) { mlog.Error("Encountered error indexing post", mlog.String("post_id", post.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err)) } mlog.Debug("Indexed post in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("post_id", post.Id)) - })(engine) + }) } } } @@ -36,12 +36,12 @@ func (s SearchPostStore) indexPost(post *model.Post) { func (s SearchPostStore) deletePostIndex(post *model.Post) { for _, engine := range s.rootStore.searchEngine.GetActiveEngines() { if engine.IsIndexingEnabled() { - go (func(engineCopy searchengine.SearchEngineInterface) { + runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) { if err := engineCopy.DeletePost(post); err != nil { mlog.Error("Encountered error deleting post", mlog.String("post_id", post.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err)) } mlog.Debug("Removed post from the index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("post_id", post.Id)) - })(engine) + }) } } } diff --git a/store/searchlayer/user_layer.go b/store/searchlayer/user_layer.go index 95c4e903f8..a63700d958 100644 --- a/store/searchlayer/user_layer.go +++ b/store/searchlayer/user_layer.go @@ -4,6 +4,7 @@ package searchlayer import ( + "net/http" "strings" "github.com/mattermost/mattermost-server/v5/mlog" @@ -20,13 +21,13 @@ type SearchUserStore struct { func (s *SearchUserStore) deleteUserIndex(user *model.User) { for _, engine := range s.rootStore.searchEngine.GetActiveEngines() { if engine.IsIndexingEnabled() { - go (func(engineCopy searchengine.SearchEngineInterface) { + runIndexFn(engine, func(engineCopy searchengine.SearchEngineInterface) { if err := engineCopy.DeleteUser(user); err != nil { mlog.Error("Encountered error deleting user", mlog.String("user_id", user.Id), mlog.String("search_engine", engineCopy.GetName()), mlog.Err(err)) return } mlog.Debug("Removed user from the index in search engine", mlog.String("search_engine", engineCopy.GetName()), mlog.String("user_id", user.Id)) - })(engine) + }) } } } @@ -45,11 +46,13 @@ func (s *SearchUserStore) Search(teamId, term string, options *model.UserSearchO usersIds, err := engine.SearchUsersInTeam(teamId, listOfAllowedChannels, term, options) if err != nil { + mlog.Error("Encountered error on Search", mlog.String("search_engine", engine.GetName()), mlog.Err(err)) continue } users, err := s.UserStore.GetProfileByIds(usersIds, nil, false) if err != nil { + mlog.Error("Encountered error on Search", mlog.String("search_engine", engine.GetName()), mlog.Err(err)) continue } @@ -58,6 +61,7 @@ func (s *SearchUserStore) Search(teamId, term string, options *model.UserSearchO } } mlog.Debug("Using database search because no other search engine is available") + return s.UserStore.Search(teamId, term, options) } @@ -138,6 +142,10 @@ func (s *SearchUserStore) autocompleteUsersInChannelByEngine(engine searchengine } func (s *SearchUserStore) getListOfAllowedChannelsForTeam(teamId string, viewRestrictions *model.ViewUsersRestrictions) ([]string, *model.AppError) { + if len(teamId) == 0 { + return nil, model.NewAppError("SearchUserStore", "store.search_user_store.empty_team_id", nil, "", http.StatusInternalServerError) + } + var listOfAllowedChannels []string if viewRestrictions == nil || strings.Contains(strings.Join(viewRestrictions.Teams, "."), teamId) { channels, err := s.rootStore.Channel().GetTeamChannels(teamId) @@ -152,7 +160,12 @@ func (s *SearchUserStore) getListOfAllowedChannelsForTeam(teamId string, viewRes return channelIds, nil } + if len(viewRestrictions.Channels) == 0 { + return []string{}, nil + } + channels, err := s.rootStore.Channel().GetChannelsByIds(viewRestrictions.Channels, false) + if err != nil { return nil, err } diff --git a/store/searchtest/channel_layer.go b/store/searchtest/channel_layer.go new file mode 100644 index 0000000000..6f4d817e5c --- /dev/null +++ b/store/searchtest/channel_layer.go @@ -0,0 +1,211 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package searchtest + +import ( + "testing" + + "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/store" + + "github.com/stretchr/testify/require" +) + +var searchChannelStoreTests = []searchTest{ + {"Database Channel AutocompleteInTeamForSearch tests", testSearchDatabaseChannelAutocompleteInTeamForSearch, []string{ENGINE_MYSQL, ENGINE_POSTGRES}}, + {"Elasticsearch search channels", testSearchESSearchChannels, []string{ENGINE_ELASTICSEARCH}}, +} + +func TestSearchChannelStore(t *testing.T, s store.Store, testEngine *SearchTestEngine) { + runTestSearch(t, s, testEngine, searchChannelStoreTests) +} + +func testSearchDatabaseChannelAutocompleteInTeamForSearch(t *testing.T, s store.Store) { + u1 := &model.User{} + u1.Email = makeEmail() + u1.Username = "user1" + model.NewId() + u1.Nickname = model.NewId() + _, err := s.User().Save(u1) + require.Nil(t, err) + + u2 := &model.User{} + u2.Email = makeEmail() + u2.Username = "user2" + model.NewId() + u2.Nickname = model.NewId() + _, err = s.User().Save(u2) + require.Nil(t, err) + + u3 := &model.User{} + u3.Email = makeEmail() + u3.Username = "user3" + model.NewId() + u3.Nickname = model.NewId() + _, err = s.User().Save(u3) + require.Nil(t, err) + + u4 := &model.User{} + u4.Email = makeEmail() + u4.Username = "user4" + model.NewId() + u4.Nickname = model.NewId() + _, err = s.User().Save(u4) + require.Nil(t, err) + + o1 := model.Channel{} + o1.TeamId = model.NewId() + o1.DisplayName = "ChannelA" + o1.Name = "zz" + model.NewId() + "b" + o1.Type = model.CHANNEL_OPEN + _, err = s.Channel().Save(&o1, -1) + require.Nil(t, err) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = u1.Id + m1.NotifyProps = model.GetDefaultChannelNotifyProps() + _, err = s.Channel().SaveMember(&m1) + require.Nil(t, err) + + o2 := model.Channel{} + o2.TeamId = model.NewId() + o2.DisplayName = "Channel2" + o2.Name = "zz" + model.NewId() + "b" + o2.Type = model.CHANNEL_OPEN + _, err = s.Channel().Save(&o2, -1) + require.Nil(t, err) + + m2 := model.ChannelMember{} + m2.ChannelId = o2.Id + m2.UserId = m1.UserId + m2.NotifyProps = model.GetDefaultChannelNotifyProps() + _, err = s.Channel().SaveMember(&m2) + require.Nil(t, err) + + o3 := model.Channel{} + o3.TeamId = o1.TeamId + o3.DisplayName = "ChannelA" + o3.Name = "zz" + model.NewId() + "b" + o3.Type = model.CHANNEL_OPEN + _, err = s.Channel().Save(&o3, -1) + require.Nil(t, err) + + m3 := model.ChannelMember{} + m3.ChannelId = o3.Id + m3.UserId = m1.UserId + m3.NotifyProps = model.GetDefaultChannelNotifyProps() + _, err = s.Channel().SaveMember(&m3) + require.Nil(t, err) + + err = s.Channel().SetDeleteAt(o3.Id, 100, 100) + require.Nil(t, err, "channel should have been deleted") + + o4 := model.Channel{} + o4.TeamId = o1.TeamId + o4.DisplayName = "ChannelA" + o4.Name = "zz" + model.NewId() + "b" + o4.Type = model.CHANNEL_PRIVATE + _, err = s.Channel().Save(&o4, -1) + require.Nil(t, err) + + m4 := model.ChannelMember{} + m4.ChannelId = o4.Id + m4.UserId = m1.UserId + m4.NotifyProps = model.GetDefaultChannelNotifyProps() + _, err = s.Channel().SaveMember(&m4) + require.Nil(t, err) + + o5 := model.Channel{} + o5.TeamId = o1.TeamId + o5.DisplayName = "ChannelC" + o5.Name = "zz" + model.NewId() + "b" + o5.Type = model.CHANNEL_PRIVATE + _, err = s.Channel().Save(&o5, -1) + require.Nil(t, err) + + _, err = s.Channel().CreateDirectChannel(u1, u2) + require.Nil(t, err) + _, err = s.Channel().CreateDirectChannel(u2, u3) + require.Nil(t, err) + + tt := []struct { + name string + term string + includeDeleted bool + expectedMatches int + }{ + {"Empty search (list all)", "", false, 4}, + {"Narrow search", "ChannelA", false, 2}, + {"Wide search", "Cha", false, 3}, + {"Direct messages", "user", false, 1}, + {"Wide search with archived channels", "Cha", true, 4}, + {"Narrow with archived channels", "ChannelA", true, 3}, + {"Direct messages with archived channels", "user", true, 1}, + {"Search without results", "blarg", true, 0}, + } + + for _, tc := range tt { + tc := tc + t.Run(tc.name, func(t *testing.T) { + channels, err := s.Channel().AutocompleteInTeamForSearch(o1.TeamId, m1.UserId, "ChannelA", false) + require.Nil(t, err) + require.Len(t, *channels, 2) + }) + } +} + +func testSearchESSearchChannels(t *testing.T, s store.Store) { + team, err := s.Team().Save(&model.Team{Name: "team", DisplayName: "team", Type: model.TEAM_OPEN}) + require.Nil(t, err) + channel1, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "channel", DisplayName: "Test One", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + channel2, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "channel-second", DisplayName: "Test Two", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + channel3, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "channel_third", DisplayName: "Test Three", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + + testCases := []struct { + Name string + Term string + Expected []string + }{ + { + Name: "autocomplete search for all channels by name", + Term: "cha", + Expected: []string{channel1.Id, channel2.Id, channel3.Id}, + }, + { + Name: "autocomplete search for one channel by display name", + Term: "one", + Expected: []string{channel1.Id}, + }, + { + Name: "autocomplete search for one channel split by -", + Term: "seco", + Expected: []string{channel2.Id}, + }, + { + Name: "autocomplete search for one channel split by _", + Term: "thir", + Expected: []string{channel3.Id}, + }, + { + Name: "autocomplete search that won't match anything", + Term: "nothing", + Expected: []string{}, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + res, err := s.Channel().AutocompleteInTeam(team.Id, tc.Term, false) + require.Nil(t, err) + require.Len(t, tc.Expected, len(*res)) + + resIds := make([]string, len(*res)) + for i, channel := range *res { + resIds[i] = channel.Id + } + require.ElementsMatch(t, tc.Expected, resIds) + }) + } +} diff --git a/store/searchtest/post_layer.go b/store/searchtest/post_layer.go new file mode 100644 index 0000000000..1cbaaca189 --- /dev/null +++ b/store/searchtest/post_layer.go @@ -0,0 +1,1971 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package searchtest + +import ( + "testing" + + "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/store" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var searchPostStoreTests = []searchTest{ + {"Database Post tests", testSearchDatabasePostSearch, []string{ENGINE_ALL}}, + {"Elasticsearch search posts: Markdown Underscores", testSearchESSearchPosts_MarkdownUnderscores, []string{ENGINE_ELASTICSEARCH}}, + {"Elasticsearch search posts: Email Addresses", testSearchESSearchPosts_EmailAddresses, []string{ENGINE_ELASTICSEARCH}}, + {"Elasticsearch search posts: Chinese Words", testSearchESSearchPosts_ChineseWords, []string{ENGINE_ELASTICSEARCH}}, + {"Elasticsearch search posts: Alternative Spellings", testSearchESSearchPosts_AlternativeSpellings, []string{ENGINE_ELASTICSEARCH}}, + {"Elasticsearch search posts: And Or Terms", testSearchESSearchPosts_AndOrTerms, []string{ENGINE_ELASTICSEARCH}}, + {"Elasticsearch search posts: Paging", testSearchESSearchPosts_Paging, []string{ENGINE_ELASTICSEARCH}}, + {"Elasticsearch search posts: Quoted Terms", testSearchESSearchPosts_QuotedTerms, []string{ENGINE_ELASTICSEARCH}}, + {"Elasticsearch search posts: Stop Words", testSearchESSearchPosts_StopWords, []string{ENGINE_ELASTICSEARCH}}, + {"Elasticsearch search posts: Stemming", testSearchESSearchPosts_Stemming, []string{ENGINE_ELASTICSEARCH}}, + {"Elasticsearch search posts: Wildcard", testSearchESSearchPosts_Wildcard, []string{ENGINE_ELASTICSEARCH}}, + {"Elasticsearch search posts: Alternative Unicode Forms", testSearchESSearchPosts_AlternativeUnicodeForms, []string{ENGINE_ELASTICSEARCH}}, + {"Elasticsearch search posts: From and In", testSearchESSearchPosts_FromAndIn, []string{ENGINE_ELASTICSEARCH}}, + {"Elasticsearch search posts: After Before On", testSearchESSearchPosts_AfterBeforeOn, []string{ENGINE_ELASTICSEARCH}}, + {"Elasticsearch search posts: Hashtags", testSearchESSearchPosts_Hashtags, []string{ENGINE_ELASTICSEARCH}}, + {"Elasticsearch search posts: Hashtags and Or words", testSearchESSearchPosts_HashtagsAndOrWords, []string{ENGINE_ELASTICSEARCH}}, + {"Elasticsearch search posts: Hashtags case insensitive", testSearchESSearchPosts_HashtagsCaseInsensitive, []string{ENGINE_ELASTICSEARCH}}, +} + +func TestSearchPostStore(t *testing.T, s store.Store, testEngine *SearchTestEngine) { + runTestSearch(t, s, testEngine, searchPostStoreTests) +} + +func testSearchDatabasePostSearch(t *testing.T, s store.Store) { + teamId := model.NewId() + userId := model.NewId() + + u1 := &model.User{} + u1.Username = "usera1" + u1.Email = makeEmail() + u1, err := s.User().Save(u1) + require.Nil(t, err) + + t1 := &model.TeamMember{} + t1.TeamId = teamId + t1.UserId = u1.Id + _, err = s.Team().SaveMember(t1, 1000) + require.Nil(t, err) + + u2 := &model.User{} + u2.Username = "userb2" + u2.Email = makeEmail() + u2, err = s.User().Save(u2) + require.Nil(t, err) + + t2 := &model.TeamMember{} + t2.TeamId = teamId + t2.UserId = u2.Id + _, err = s.Team().SaveMember(t2, 1000) + require.Nil(t, err) + + u3 := &model.User{} + u3.Username = "userc3" + u3.Email = makeEmail() + u3, err = s.User().Save(u3) + require.Nil(t, err) + + t3 := &model.TeamMember{} + t3.TeamId = teamId + t3.UserId = u3.Id + _, err = s.Team().SaveMember(t3, 1000) + require.Nil(t, err) + + c1 := &model.Channel{} + c1.TeamId = teamId + c1.DisplayName = "Channel1" + c1.Name = "channel-x" + c1.Type = model.CHANNEL_OPEN + c1, _ = s.Channel().Save(c1, -1) + + m1 := model.ChannelMember{} + m1.ChannelId = c1.Id + m1.UserId = userId + m1.NotifyProps = model.GetDefaultChannelNotifyProps() + _, err = s.Channel().SaveMember(&m1) + require.Nil(t, err) + + c2 := &model.Channel{} + c2.TeamId = teamId + c2.DisplayName = "Channel2" + c2.Name = "channel-y" + c2.Type = model.CHANNEL_OPEN + c2, _ = s.Channel().Save(c2, -1) + + c3 := &model.Channel{} + c3.TeamId = teamId + c3.DisplayName = "Channel3" + c3.Name = "channel-z" + c3.Type = model.CHANNEL_OPEN + c3, _ = s.Channel().Save(c3, -1) + + s.Channel().Delete(c3.Id, model.GetMillis()) + + m3 := model.ChannelMember{} + m3.ChannelId = c3.Id + m3.UserId = userId + m3.NotifyProps = model.GetDefaultChannelNotifyProps() + _, err = s.Channel().SaveMember(&m3) + require.Nil(t, err) + + o1 := &model.Post{} + o1.ChannelId = c1.Id + o1.UserId = u1.Id + o1.Message = "corey mattermost new york United States" + o1, err = s.Post().Save(o1) + require.Nil(t, err) + + o1a := &model.Post{} + o1a.ChannelId = c1.Id + o1a.UserId = model.NewId() + o1a.Message = "corey mattermost new york United States" + o1a.Type = model.POST_JOIN_CHANNEL + _, err = s.Post().Save(o1a) + require.Nil(t, err) + + o2 := &model.Post{} + o2.ChannelId = c1.Id + o2.UserId = u2.Id + o2.Message = "New Jersey United States is where John is from" + o2, err = s.Post().Save(o2) + require.Nil(t, err) + + o3 := &model.Post{} + o3.ChannelId = c2.Id + o3.UserId = model.NewId() + o3.Message = "New Jersey United States is where John is from corey new york" + _, err = s.Post().Save(o3) + require.Nil(t, err) + + o4 := &model.Post{} + o4.ChannelId = c1.Id + o4.UserId = model.NewId() + o4.Hashtags = "#hashtag #tagme" + o4.Message = "(message)blargh" + o4, err = s.Post().Save(o4) + require.Nil(t, err) + + o5 := &model.Post{} + o5.ChannelId = c1.Id + o5.UserId = model.NewId() + o5.Hashtags = "#secret #howdy #tagme" + o5, err = s.Post().Save(o5) + require.Nil(t, err) + + o6 := &model.Post{} + o6.ChannelId = c3.Id + o6.UserId = model.NewId() + o6.Hashtags = "#hashtag" + o6, err = s.Post().Save(o6) + require.Nil(t, err) + + o7 := &model.Post{} + o7.ChannelId = c3.Id + o7.UserId = u3.Id + o7.Message = "New Jersey United States is where John is from corey new york" + o7, err = s.Post().Save(o7) + require.Nil(t, err) + + o8 := &model.Post{} + o8.ChannelId = c3.Id + o8.UserId = model.NewId() + o8.Message = "Deleted" + o8, err = s.Post().Save(o8) + require.Nil(t, err) + + tt := []struct { + name string + searchParams *model.SearchParams + expectedResultsCount int + expectedMessageResultIds []string + }{ + { + "normal-search-1", + &model.SearchParams{Terms: "corey"}, + 1, + []string{o1.Id}, + }, + { + "normal-search-2", + &model.SearchParams{Terms: "new"}, + 2, + []string{o1.Id, o2.Id}, + }, + { + "normal-search-3", + &model.SearchParams{Terms: "john"}, + 1, + []string{o2.Id}, + }, + { + "wildcard-search", + &model.SearchParams{Terms: "matter*"}, + 1, + []string{o1.Id}, + }, + { + "hashtag-search", + &model.SearchParams{Terms: "#hashtag", IsHashtag: true}, + 1, + []string{o4.Id}, + }, + { + "hashtag-search-2", + &model.SearchParams{Terms: "#secret", IsHashtag: true}, + 1, + []string{o5.Id}, + }, + { + "hashtag-search-with-exclusion", + &model.SearchParams{Terms: "#tagme", ExcludedTerms: "#hashtag", IsHashtag: true}, + 1, + []string{o5.Id}, + }, + { + "no-match-mention", + &model.SearchParams{Terms: "@thisshouldmatchnothing", IsHashtag: true}, + 0, + []string{}, + }, + { + "no-results-search", + &model.SearchParams{Terms: "mattermost jersey"}, + 0, + []string{}, + }, + { + "exclude-search", + &model.SearchParams{Terms: "united", ExcludedTerms: "jersey"}, + 1, + []string{o1.Id}, + }, + { + "multiple-words-search", + &model.SearchParams{Terms: "corey new york"}, + 1, + []string{o1.Id}, + }, + { + "multiple-words-with-exclusion-search", + &model.SearchParams{Terms: "united states", ExcludedTerms: "jersey"}, + 1, + []string{o1.Id}, + }, + { + "multiple-excluded-words-search", + &model.SearchParams{Terms: "united", ExcludedTerms: "corey john"}, + 0, + []string{}, + }, + { + "multiple-wildcard-search", + &model.SearchParams{Terms: "matter* jer*"}, + 0, + []string{}, + }, + { + "multiple-wildcard-with-exclusion-search", + &model.SearchParams{Terms: "unite* state*", ExcludedTerms: "jers*"}, + 1, + []string{o1.Id}, + }, + { + "multiple-wildcard-excluded-words-search", + &model.SearchParams{Terms: "united states", ExcludedTerms: "jers* yor*"}, + 0, + []string{}, + }, + { + "search-with-work-next-to-a-symbol", + &model.SearchParams{Terms: "message blargh"}, + 1, + []string{o4.Id}, + }, + { + "search-with-or", + &model.SearchParams{Terms: "Jersey corey", OrTerms: true}, + 2, + []string{o1.Id, o2.Id}, + }, + { + "exclude-search-with-or", + &model.SearchParams{Terms: "york jersey", ExcludedTerms: "john", OrTerms: true}, + 1, + []string{o1.Id}, + }, + { + "search-with-from-user", + &model.SearchParams{Terms: "united states", FromUsers: []string{"usera1"}, IncludeDeletedChannels: true}, + 1, + []string{o1.Id}, + }, + { + "search-with-multiple-from-user", + &model.SearchParams{Terms: "united states", FromUsers: []string{"usera1", "userc3"}, IncludeDeletedChannels: true}, + 2, + []string{o1.Id, o7.Id}, + }, + { + "search-with-excluded-user", + &model.SearchParams{Terms: "united states", ExcludedUsers: []string{"usera1"}, IncludeDeletedChannels: true}, + 2, + []string{o2.Id, o7.Id}, + }, + { + "search-with-multiple-excluded-user", + &model.SearchParams{Terms: "united states", ExcludedUsers: []string{"usera1", "userb2"}, IncludeDeletedChannels: true}, + 1, + []string{o7.Id}, + }, + { + "search-with-deleted-and-channel-filter", + &model.SearchParams{Terms: "Jersey corey", InChannels: []string{"channel-x"}, IncludeDeletedChannels: true, OrTerms: true}, + 2, + []string{o1.Id, o2.Id}, + }, + { + "search-with-deleted-and-multiple-channel-filter", + &model.SearchParams{Terms: "Jersey corey", InChannels: []string{"channel-x", "channel-z"}, IncludeDeletedChannels: true, OrTerms: true}, + 3, + []string{o1.Id, o2.Id, o7.Id}, + }, + { + "search-with-deleted-and-excluded-channel-filter", + &model.SearchParams{Terms: "Jersey corey", ExcludedChannels: []string{"channel-x"}, IncludeDeletedChannels: true, OrTerms: true}, + 1, + []string{o7.Id}, + }, + { + "search-with-deleted-and-multiple-excluded-channel-filter", + &model.SearchParams{Terms: "Jersey corey", ExcludedChannels: []string{"channel-x", "channel-z"}, IncludeDeletedChannels: true, OrTerms: true}, + 0, + []string{}, + }, + { + "search-with-or-and-deleted", + &model.SearchParams{Terms: "Jersey corey", OrTerms: true, IncludeDeletedChannels: true}, + 3, + []string{o1.Id, o2.Id, o7.Id}, + }, + { + "search-hashtag-deleted", + &model.SearchParams{Terms: "#hashtag", IsHashtag: true, IncludeDeletedChannels: true}, + 2, + []string{o4.Id, o6.Id}, + }, + { + "search-deleted-only", + &model.SearchParams{Terms: "Deleted", IncludeDeletedChannels: true}, + 1, + []string{o8.Id}, + }, + } + for _, tc := range tt { + tc := tc + t.Run(tc.name, func(t *testing.T) { + result, err := s.Post().Search(teamId, userId, tc.searchParams) + require.Nil(t, err) + require.Len(t, result.Order, tc.expectedResultsCount) + for _, expectedMessageResultId := range tc.expectedMessageResultIds { + assert.Contains(t, result.Order, expectedMessageResultId) + } + }) + } +} + +func testSearchESSearchPosts_MarkdownUnderscores(t *testing.T, s store.Store) { + team, err := s.Team().Save(&model.Team{Name: "t1", DisplayName: "t1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer func() { require.Nil(t, s.Team().PermanentDelete(team.Id)) }() + channel, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "c1", DisplayName: "c1", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + user, err := s.User().Save(createUser("testuser", "testuser", "Test", "User")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user, []string{team.Id}, []string{channel.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user.Id)) }() + + // Test searching for posts with Markdown underscores in them. + post, err := s.Post().Save(createPost(user.Id, channel.Id, "_start middle end_ _both_")) + require.Nil(t, err) + + searchParams := []*model.SearchParams{ + { + IsHashtag: false, + OrTerms: false, + }, + } + + testCases := []struct { + Name string + Terms string + }{ + { + Name: "It should match the post by start", + Terms: "start", + }, + { + Name: "It should match the post by middle", + Terms: "middle", + }, + { + Name: "It should match the post by end", + Terms: "end", + }, + { + Name: "It should match the post by both", + Terms: "both", + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + searchParams[0].Terms = tc.Terms + + res, err := s.Post().SearchPostsInTeamForUser(searchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + checkPostInSearchResults(t, post.Id, ids) + checkMatchesEqual(t, map[string][]string{ + post.Id: {tc.Terms}, + }, res.Matches) + }) + } +} + +func testSearchESSearchPosts_EmailAddresses(t *testing.T, s store.Store) { + team, err := s.Team().Save(&model.Team{Name: "t1", DisplayName: "t1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer func() { require.Nil(t, s.Team().PermanentDelete(team.Id)) }() + channel, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "c1", DisplayName: "c1", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + user, err := s.User().Save(createUser("testuser", "testuser", "Test", "User")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user, []string{team.Id}, []string{channel.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user.Id)) }() + + // Test searching for posts with Markdown underscores in them. + post1, err := s.Post().Save(createPost(user.Id, channel.Id, "something test something example.com something")) + require.Nil(t, err) + post2, err := s.Post().Save(createPost(user.Id, channel.Id, "test@example.com")) + require.Nil(t, err) + + searchParams := []*model.SearchParams{ + { + Terms: "test@example.com", + IsHashtag: false, + OrTerms: false, + }, + } + + res, err := s.Post().SearchPostsInTeamForUser(searchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + checkPostNotInSearchResults(t, post1.Id, ids) + checkPostInSearchResults(t, post2.Id, ids) + checkMatchesEqual(t, map[string][]string{ + post2.Id: {"test", "example.com"}, + }, res.Matches) +} + +func testSearchESSearchPosts_ChineseWords(t *testing.T, s store.Store) { + team, err := s.Team().Save(&model.Team{Name: "t1", DisplayName: "t1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer func() { require.Nil(t, s.Team().PermanentDelete(team.Id)) }() + channel, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "c1", DisplayName: "c1", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + user, err := s.User().Save(createUser("testuser", "testuser", "Test", "User")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user, []string{team.Id}, []string{channel.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user.Id)) }() + + post1, err := s.Post().Save(createPost(user.Id, channel.Id, "你好")) + require.Nil(t, err) + post2, err := s.Post().Save(createPost(user.Id, channel.Id, "你")) + require.Nil(t, err) + + searchParams := []*model.SearchParams{ + { + IsHashtag: false, + OrTerms: false, + }, + } + + testCases := []struct { + Name string + Terms string + Matches map[string][]string + }{ + { + Name: "Test cjk 2 character word match", + Terms: "你好", + Matches: map[string][]string{ + post1.Id: {"你好"}, + }, + }, + { + Name: "Test cjk 1 character word match", + Terms: "你", + Matches: map[string][]string{ + post2.Id: {"你"}, + }, + }, + { + Name: "Test cjk wildcard word match", + Terms: "你*", + Matches: map[string][]string{ + post1.Id: {"你好"}, + post2.Id: {"你"}, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + searchParams[0].Terms = tc.Terms + + res, err := s.Post().SearchPostsInTeamForUser(searchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + + expectedIds := []string{} + for id := range tc.Matches { + expectedIds = append(expectedIds, id) + } + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + require.ElementsMatch(t, expectedIds, ids) + checkMatchesEqual(t, tc.Matches, res.Matches) + }) + } +} + +func testSearchESSearchPosts_AlternativeSpellings(t *testing.T, s store.Store) { + team, err := s.Team().Save(&model.Team{Name: "t1", DisplayName: "t1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer func() { require.Nil(t, s.Team().PermanentDelete(team.Id)) }() + channel, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "c1", DisplayName: "c1", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + user, err := s.User().Save(createUser("testuser", "testuser", "Test", "User")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user, []string{team.Id}, []string{channel.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user.Id)) }() + + post1, err := s.Post().Save(createPost(user.Id, channel.Id, "Straße")) + require.Nil(t, err) + post2, err := s.Post().Save(createPost(user.Id, channel.Id, "Strasse")) + require.Nil(t, err) + + searchParams := []*model.SearchParams{ + { + IsHashtag: false, + OrTerms: false, + }, + } + + testCases := []struct { + Name string + Terms string + Matches map[string][]string + }{ + { + Name: "Search for Straße", + Terms: "Straße", + Matches: map[string][]string{ + post1.Id: {"Straße"}, + post2.Id: {"Strasse"}, + }, + }, + { + Name: "Search for Strasse", + Terms: "Strasse", + Matches: map[string][]string{ + post1.Id: {"Straße"}, + post2.Id: {"Strasse"}, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + searchParams[0].Terms = tc.Terms + + res, err := s.Post().SearchPostsInTeamForUser(searchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + + expectedIds := []string{} + for id := range tc.Matches { + expectedIds = append(expectedIds, id) + } + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + require.ElementsMatch(t, expectedIds, ids) + checkMatchesEqual(t, tc.Matches, res.Matches) + }) + } +} + +func testSearchESSearchPosts_AndOrTerms(t *testing.T, s store.Store) { + team, err := s.Team().Save(&model.Team{Name: "t1", DisplayName: "t1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer func() { require.Nil(t, s.Team().PermanentDelete(team.Id)) }() + channel, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "c1", DisplayName: "c1", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + user, err := s.User().Save(createUser("testuser", "testuser", "Test", "User")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user, []string{team.Id}, []string{channel.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user.Id)) }() + + post1, err := s.Post().Save(createPost(user.Id, channel.Id, "things and stuff")) + require.Nil(t, err) + post2, err := s.Post().Save(createPost(user.Id, channel.Id, "things")) + require.Nil(t, err) + post3, err := s.Post().Save(createPost(user.Id, channel.Id, "stuff")) + require.Nil(t, err) + + t.Run("Searches for things stuff", func(t *testing.T) { + searchParams := []*model.SearchParams{ + { + Terms: "things stuff", + IsHashtag: false, + OrTerms: false, + }, + } + + res, err := s.Post().SearchPostsInTeamForUser(searchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + require.Len(t, res.Posts, 1) + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + checkPostInSearchResults(t, post1.Id, ids) + checkPostNotInSearchResults(t, post2.Id, ids) + checkPostNotInSearchResults(t, post3.Id, ids) + checkMatchesEqual(t, map[string][]string{ + post1.Id: {"things", "stuff"}, + }, res.Matches) + }) + + t.Run("Searches for things", func(t *testing.T) { + searchParams := []*model.SearchParams{ + { + Terms: "things", + IsHashtag: false, + OrTerms: false, + }, + } + + res, err := s.Post().SearchPostsInTeamForUser(searchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + require.Len(t, res.Posts, 2) + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + checkPostInSearchResults(t, post1.Id, ids) + checkPostInSearchResults(t, post2.Id, ids) + checkPostNotInSearchResults(t, post3.Id, ids) + checkMatchesEqual(t, map[string][]string{ + post1.Id: {"things"}, + post2.Id: {"things"}, + }, res.Matches) + }) + + t.Run("Searches for stuff", func(t *testing.T) { + searchParams := []*model.SearchParams{ + { + Terms: "stuff", + IsHashtag: false, + OrTerms: false, + }, + } + + res, err := s.Post().SearchPostsInTeamForUser(searchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + require.Len(t, res.Posts, 2) + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + checkPostInSearchResults(t, post1.Id, ids) + checkPostNotInSearchResults(t, post2.Id, ids) + checkPostInSearchResults(t, post3.Id, ids) + checkMatchesEqual(t, map[string][]string{ + post1.Id: {"stuff"}, + post3.Id: {"stuff"}, + }, res.Matches) + }) + + t.Run("Searches for things and not stuff", func(t *testing.T) { + searchParams := []*model.SearchParams{ + { + Terms: "things", + ExcludedTerms: "stuff", + IsHashtag: false, + OrTerms: false, + }, + } + + res, err := s.Post().SearchPostsInTeamForUser(searchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + require.Len(t, res.Posts, 1) + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + checkPostInSearchResults(t, post2.Id, ids) + checkMatchesEqual(t, map[string][]string{ + post2.Id: {"things"}, + }, res.Matches) + }) + + t.Run("Searches for not things", func(t *testing.T) { + searchParams := []*model.SearchParams{ + { + ExcludedTerms: "things", + IsHashtag: false, + OrTerms: false, + }, + } + + res, err := s.Post().SearchPostsInTeamForUser(searchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + require.Len(t, res.Posts, 1) + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + checkPostInSearchResults(t, post3.Id, ids) + checkMatchesEqual(t, map[string][]string{ + post3.Id: {}, + }, res.Matches) + }) + + t.Run("Searches for things stuff with an or search", func(t *testing.T) { + searchParams := []*model.SearchParams{ + { + Terms: "things stuff", + IsHashtag: false, + OrTerms: true, + }, + } + + res, err := s.Post().SearchPostsInTeamForUser(searchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + require.Len(t, res.Posts, 3) + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + checkPostInSearchResults(t, post1.Id, ids) + checkPostInSearchResults(t, post2.Id, ids) + checkPostInSearchResults(t, post3.Id, ids) + checkMatchesEqual(t, map[string][]string{ + post1.Id: {"things", "stuff"}, + post2.Id: {"things"}, + post3.Id: {"stuff"}, + }, res.Matches) + }) +} + +func testSearchESSearchPosts_Paging(t *testing.T, s store.Store) { + team, err := s.Team().Save(&model.Team{Name: "t1", DisplayName: "t1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer func() { require.Nil(t, s.Team().PermanentDelete(team.Id)) }() + channel, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "c1", DisplayName: "c1", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + user, err := s.User().Save(createUser("testuser", "testuser", "Test", "User")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user, []string{team.Id}, []string{channel.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user.Id)) }() + + post1, err := s.Post().Save(createPost(user.Id, channel.Id, "things and stuff")) + require.Nil(t, err) + post2, err := s.Post().Save(createPost(user.Id, channel.Id, "things")) + require.Nil(t, err) + post3, err := s.Post().Save(createPost(user.Id, channel.Id, "stuff")) + require.Nil(t, err) + + searchParams := []*model.SearchParams{ + { + Terms: "things stuff", + OrTerms: true, + IsHashtag: false, + }, + } + + testCases := []struct { + Name string + Page int + PerPage int + Matches map[string][]string + }{ + { + Name: "The full page", + Page: 0, + PerPage: 20, + Matches: map[string][]string{ + post1.Id: {"things", "stuff"}, + post2.Id: {"things"}, + post3.Id: {"stuff"}, + }, + }, + { + Name: "The first page with two results", + Page: 0, + PerPage: 2, + Matches: map[string][]string{ + post1.Id: {"things", "stuff"}, + post2.Id: {"things"}, + }, + }, + { + Name: "The second page with two results", + Page: 1, + PerPage: 2, + Matches: map[string][]string{ + post3.Id: {"stuff"}, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + res, err := s.Post().SearchPostsInTeamForUser(searchParams, user.Id, team.Id, false, false, tc.Page, tc.PerPage) + require.Nil(t, err) + + expectedIds := []string{} + for id := range tc.Matches { + expectedIds = append(expectedIds, id) + } + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + require.ElementsMatch(t, expectedIds, ids) + checkMatchesEqual(t, tc.Matches, res.Matches) + }) + } +} + +func testSearchESSearchPosts_QuotedTerms(t *testing.T, s store.Store) { + team, err := s.Team().Save(&model.Team{Name: "t1", DisplayName: "t1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer func() { require.Nil(t, s.Team().PermanentDelete(team.Id)) }() + channel, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "c1", DisplayName: "c1", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + user, err := s.User().Save(createUser("testuser", "testuser", "Test", "User")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user, []string{team.Id}, []string{channel.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user.Id)) }() + + post1, err := s.Post().Save(createPost(user.Id, channel.Id, "large dog")) + require.Nil(t, err) + post2, err := s.Post().Save(createPost(user.Id, channel.Id, "large scary dog")) + require.Nil(t, err) + + searchParams := []*model.SearchParams{ + { + IsHashtag: false, + OrTerms: false, + }, + } + + testCases := []struct { + Name string + Terms string + ExcludedTerms string + Matches map[string][]string + }{ + { + Name: "Search for large dog", + Terms: "large dog", + Matches: map[string][]string{ + post1.Id: {"large", "dog"}, + post2.Id: {"large", "dog"}, + }, + }, + { + Name: "Search for \"large dog\"", + Terms: "\"large dog\"", + Matches: map[string][]string{ + post1.Id: {"large", "dog"}, // Note that we don't return the whole string highlighted + }, + }, + { + Name: "Search for dog -\"large scary\"", + Terms: "dog", + ExcludedTerms: "\"large scary\"", + Matches: map[string][]string{ + post1.Id: {"dog"}, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + searchParams[0].Terms = tc.Terms + searchParams[0].ExcludedTerms = tc.ExcludedTerms + + res, err := s.Post().SearchPostsInTeamForUser(searchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + + expectedIds := []string{} + for id := range tc.Matches { + expectedIds = append(expectedIds, id) + } + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + require.ElementsMatch(t, expectedIds, ids) + checkMatchesEqual(t, tc.Matches, res.Matches) + }) + } +} + +func testSearchESSearchPosts_StopWords(t *testing.T, s store.Store) { + team, err := s.Team().Save(&model.Team{Name: "t1", DisplayName: "t1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer func() { require.Nil(t, s.Team().PermanentDelete(team.Id)) }() + channel, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "c1", DisplayName: "c1", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + user, err := s.User().Save(createUser("testuser", "testuser", "Test", "User")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user, []string{team.Id}, []string{channel.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user.Id)) }() + + post1, err := s.Post().Save(createPost(user.Id, channel.Id, "I enjoyed eating the house")) + require.Nil(t, err) + post2, err := s.Post().Save(createPost(user.Id, channel.Id, "I enjoyed eating house")) + require.Nil(t, err) + post3, err := s.Post().Save(createPost(user.Id, channel.Id, "I enjoyed eating a house")) + require.Nil(t, err) + + searchParams := []*model.SearchParams{ + { + IsHashtag: false, + OrTerms: false, + }, + } + + testCases := []struct { + Name string + Terms string + Matches map[string][]string + }{ + { + Name: "Search for the house", + Terms: "the house", + Matches: map[string][]string{ + post1.Id: {"house"}, + post2.Id: {"house"}, + post3.Id: {"house"}, + }, + }, + { + Name: "Search for eating the house", + Terms: "eating the house", + Matches: map[string][]string{ + post1.Id: {"eating", "house"}, + post2.Id: {"eating", "house"}, + post3.Id: {"eating", "house"}, + }, + }, + { + Name: "Search for quoted \"eating the house\"", + Terms: "\"eating the house\"", + Matches: map[string][]string{ + post1.Id: {"eating", "house"}, + post3.Id: {"eating", "house"}, + }, + }, + { + Name: "Search for quoted \"eating house\"", + Terms: "\"eating house\"", + Matches: map[string][]string{ + post2.Id: {"eating", "house"}, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + searchParams[0].Terms = tc.Terms + + res, err := s.Post().SearchPostsInTeamForUser(searchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + + expectedIds := []string{} + for id := range tc.Matches { + expectedIds = append(expectedIds, id) + } + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + require.ElementsMatch(t, expectedIds, ids) + checkMatchesEqual(t, tc.Matches, res.Matches) + }) + } +} + +func testSearchESSearchPosts_Stemming(t *testing.T, s store.Store) { + team, err := s.Team().Save(&model.Team{Name: "t1", DisplayName: "t1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer func() { require.Nil(t, s.Team().PermanentDelete(team.Id)) }() + channel, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "c1", DisplayName: "c1", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + user, err := s.User().Save(createUser("testuser", "testuser", "Test", "User")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user, []string{team.Id}, []string{channel.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user.Id)) }() + + post1, err := s.Post().Save(createPost(user.Id, channel.Id, "sail")) + require.Nil(t, err) + post2, err := s.Post().Save(createPost(user.Id, channel.Id, "sailing")) + require.Nil(t, err) + + searchParams := []*model.SearchParams{ + { + IsHashtag: false, + OrTerms: false, + }, + } + + testCases := []struct { + Name string + Terms string + Matches map[string][]string + }{ + { + Name: "Search for sail", + Terms: "sail", + Matches: map[string][]string{ + post1.Id: {"sail"}, + post2.Id: {"sailing"}, + }, + }, + { + Name: "Search for sailing", + Terms: "sailing", + Matches: map[string][]string{ + post1.Id: {"sail"}, + post2.Id: {"sailing"}, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + searchParams[0].Terms = tc.Terms + + res, err := s.Post().SearchPostsInTeamForUser(searchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + + expectedIds := []string{} + for id := range tc.Matches { + expectedIds = append(expectedIds, id) + } + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + require.ElementsMatch(t, expectedIds, ids) + checkMatchesEqual(t, tc.Matches, res.Matches) + }) + } +} + +func testSearchESSearchPosts_Wildcard(t *testing.T, s store.Store) { + team, err := s.Team().Save(&model.Team{Name: "t1", DisplayName: "t1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer func() { require.Nil(t, s.Team().PermanentDelete(team.Id)) }() + channel, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "c1", DisplayName: "c1", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + user, err := s.User().Save(createUser("testuser", "testuser", "Test", "User")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user, []string{team.Id}, []string{channel.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user.Id)) }() + + post1, err := s.Post().Save(createPost(user.Id, channel.Id, "insensitive")) + require.Nil(t, err) + post2, err := s.Post().Save(createPost(user.Id, channel.Id, "insensible")) + require.Nil(t, err) + + searchParams := []*model.SearchParams{ + { + IsHashtag: false, + OrTerms: false, + }, + } + + testCases := []struct { + Name string + Terms string + Matches map[string][]string + }{ + { + Name: "Search for inse*", + Terms: "inse*", + Matches: map[string][]string{ + post1.Id: {"insensitive"}, + post2.Id: {"insensible"}, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + searchParams[0].Terms = tc.Terms + + res, err := s.Post().SearchPostsInTeamForUser(searchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + + expectedIds := []string{} + for id := range tc.Matches { + expectedIds = append(expectedIds, id) + } + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + require.ElementsMatch(t, expectedIds, ids) + checkMatchesEqual(t, tc.Matches, res.Matches) + }) + } +} + +func testSearchESSearchPosts_AlternativeUnicodeForms(t *testing.T, s store.Store) { + team, err := s.Team().Save(&model.Team{Name: "t1", DisplayName: "t1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer func() { require.Nil(t, s.Team().PermanentDelete(team.Id)) }() + channel, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "c1", DisplayName: "c1", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + user, err := s.User().Save(createUser("testuser", "testuser", "Test", "User")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user, []string{team.Id}, []string{channel.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user.Id)) }() + + post1, err := s.Post().Save(createPost(user.Id, channel.Id, "café")) + require.Nil(t, err) + post2, err := s.Post().Save(createPost(user.Id, channel.Id, "café")) + require.Nil(t, err) + + searchParams := []*model.SearchParams{ + { + IsHashtag: false, + OrTerms: false, + }, + } + + testCases := []struct { + Name string + Terms string + Matches map[string][]string + }{ + { + Name: "Search for café", + Terms: "café", + Matches: map[string][]string{ + post1.Id: {"café"}, + post2.Id: {"café"}, + }, + }, + { + Name: "Search for café", + Terms: "café", + Matches: map[string][]string{ + post1.Id: {"café"}, + post2.Id: {"café"}, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + searchParams[0].Terms = tc.Terms + + res, err := s.Post().SearchPostsInTeamForUser(searchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + + expectedIds := []string{} + for id := range tc.Matches { + expectedIds = append(expectedIds, id) + } + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + require.ElementsMatch(t, expectedIds, ids) + checkMatchesEqual(t, tc.Matches, res.Matches) + }) + } +} + +func testSearchESSearchPosts_FromAndIn(t *testing.T, s store.Store) { + team, err := s.Team().Save(&model.Team{Name: "t1", DisplayName: "t1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer func() { require.Nil(t, s.Team().PermanentDelete(team.Id)) }() + channel1, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "c1", DisplayName: "c1", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + channel2, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "c2", DisplayName: "c2", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + user1, err := s.User().Save(createUser("testuserone", "testuserone", "Test", "One")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user1, []string{team.Id}, []string{channel1.Id, channel2.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user1.Id)) }() + user2, err := s.User().Save(createUser("testusertwo", "testusertwo", "Test", "Two")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user2, []string{team.Id}, []string{channel1.Id, channel2.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user2.Id)) }() + + post1, err := s.Post().Save(createPost(user1.Id, channel1.Id, "post in channel 1 by user 1")) + require.Nil(t, err) + post2, err := s.Post().Save(createPost(user1.Id, channel2.Id, "post in channel 2 by user 1")) + require.Nil(t, err) + post3, err := s.Post().Save(createPost(user2.Id, channel1.Id, "post in channel 1 by user 2")) + require.Nil(t, err) + post4, err := s.Post().Save(createPost(user2.Id, channel2.Id, "post in channel 2 by user 2")) + require.Nil(t, err) + + searchParams := []*model.SearchParams{ + { + Terms: "post", + IsHashtag: false, + OrTerms: false, + }, + } + + testCases := []struct { + Name string + User *model.User + FromUsers []string + InChannels []string + ExcludedUsers []string + ExcludedChannels []string + Matches map[string][]string + }{ + { + Name: "Search for post from the user1 in the channel1", + User: user1, + InChannels: []string{channel1.Id}, + FromUsers: []string{user1.Id}, + Matches: map[string][]string{ + post1.Id: {"post"}, + }, + }, + { + Name: "Search for post from the user1 in channel1 and channel2", + User: user1, + InChannels: []string{channel1.Id, channel2.Id}, + FromUsers: []string{user1.Id}, + Matches: map[string][]string{ + post1.Id: {"post"}, + post2.Id: {"post"}, + }, + }, + { + Name: "Search for post from user1 and user2 in channel1 and channel2", + User: user1, + InChannels: []string{channel1.Id, channel2.Id}, + FromUsers: []string{user1.Id, user2.Id}, + Matches: map[string][]string{ + post1.Id: {"post"}, + post2.Id: {"post"}, + post3.Id: {"post"}, + post4.Id: {"post"}, + }, + }, + { + Name: "Search for post not from the user1 in the channel1", + User: user1, + InChannels: []string{channel1.Id}, + ExcludedUsers: []string{user1.Id}, + Matches: map[string][]string{ + post3.Id: {"post"}, + }, + }, + { + Name: "Search for post not in the channel1", + User: user1, + ExcludedChannels: []string{channel1.Id}, + Matches: map[string][]string{ + post2.Id: {"post"}, + post4.Id: {"post"}, + }, + }, + { + Name: "Search for post not from user1 and not in the channel1", + User: user1, + ExcludedUsers: []string{user1.Id}, + ExcludedChannels: []string{channel1.Id}, + Matches: map[string][]string{ + post4.Id: {"post"}, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + searchParams[0].FromUsers = tc.FromUsers + searchParams[0].InChannels = tc.InChannels + searchParams[0].ExcludedUsers = tc.ExcludedUsers + searchParams[0].ExcludedChannels = tc.ExcludedChannels + + res, err := s.Post().SearchPostsInTeamForUser(searchParams, tc.User.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + + expectedIds := []string{} + for id := range tc.Matches { + expectedIds = append(expectedIds, id) + } + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + require.ElementsMatch(t, expectedIds, ids) + checkMatchesEqual(t, tc.Matches, res.Matches) + }) + } +} + +func testSearchESSearchPosts_AfterBeforeOn(t *testing.T, s store.Store) { + team, err := s.Team().Save(&model.Team{Name: "t1", DisplayName: "t1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer func() { require.Nil(t, s.Team().PermanentDelete(team.Id)) }() + channel, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "c1", DisplayName: "c1", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + user, err := s.User().Save(createUser("testuser", "testuser", "Test", "User")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user, []string{team.Id}, []string{channel.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user.Id)) }() + + post1, err := s.Post().Save(createPostAtTime(user.Id, channel.Id, "post on 2018-08-28 at 23:59 GMT", 1535500740000)) + require.Nil(t, err) + post2, err := s.Post().Save(createPostAtTime(user.Id, channel.Id, "post on 2018-08-29 at 00:01 GMT", 1535500860000)) + require.Nil(t, err) + post3, err := s.Post().Save(createPostAtTime(user.Id, channel.Id, "post on 2018-08-29 at 23:59 GMT", 1535587140000)) + require.Nil(t, err) + post4, err := s.Post().Save(createPostAtTime(user.Id, channel.Id, "post on 2018-08-30 at 00:01 GMT", 1535587260000)) + require.Nil(t, err) + + earlierTimeZoneOffset := 1 * 60 * 60 + laterTimeZoneOffset := -1 * 60 * 60 + + testCases := []struct { + Name string + SearchParams []*model.SearchParams + ExpectedIds []string + }{ + { + Name: "after: should return posts on 2018-08-30 or later", + SearchParams: []*model.SearchParams{ + { + AfterDate: "2018-08-28", + }, + }, + ExpectedIds: []string{post2.Id, post3.Id, post4.Id}, + }, + { + Name: "after: should return posts on 2018-09-01 or later", + SearchParams: []*model.SearchParams{ + { + AfterDate: "2018-08-30", + }, + }, + ExpectedIds: []string{}, + }, + { + Name: "not before: should return posts on 2018-08-28 or later", + SearchParams: []*model.SearchParams{ + { + ExcludedBeforeDate: "2018-08-28", + }, + }, + ExpectedIds: []string{post1.Id, post2.Id, post3.Id, post4.Id}, + }, + { + Name: "not before: should return posts on 2018-08-29 or later", + SearchParams: []*model.SearchParams{ + { + ExcludedBeforeDate: "2018-08-29", + }, + }, + ExpectedIds: []string{post2.Id, post3.Id, post4.Id}, + }, + { + Name: "not before: should return posts on 2018-08-30 or later", + SearchParams: []*model.SearchParams{ + { + ExcludedBeforeDate: "2018-08-30", + }, + }, + ExpectedIds: []string{post4.Id}, + }, + { + Name: "not before: should return posts on 2018-09-01 or later", + SearchParams: []*model.SearchParams{ + { + ExcludedBeforeDate: "2018-09-01", + }, + }, + ExpectedIds: []string{}, + }, + { + Name: "before: should return posts on 2018-08-29 or earlier", + SearchParams: []*model.SearchParams{ + { + BeforeDate: "2018-08-30", + }, + }, + ExpectedIds: []string{post1.Id, post2.Id, post3.Id}, + }, + { + Name: "before: should return posts on 2018-08-28 or earlier", + SearchParams: []*model.SearchParams{ + { + BeforeDate: "2018-08-29", + }, + }, + ExpectedIds: []string{post1.Id}, + }, + { + Name: "before: should return posts on 2018-08-27 or earlier", + SearchParams: []*model.SearchParams{ + { + BeforeDate: "2018-08-28", + }, + }, + ExpectedIds: []string{}, + }, + { + Name: "after and before: should return posts on 2018-08-29", + SearchParams: []*model.SearchParams{ + { + AfterDate: "2018-08-28", + BeforeDate: "2018-08-30", + }, + }, + ExpectedIds: []string{post2.Id, post3.Id}, + }, + { + Name: "not after: should return posts on 2018-08-30 or earlier", + SearchParams: []*model.SearchParams{ + { + ExcludedAfterDate: "2018-09-01", + }, + }, + ExpectedIds: []string{post1.Id, post2.Id, post3.Id, post4.Id}, + }, + { + Name: "not after: should return posts on 2018-08-29 or earlier", + SearchParams: []*model.SearchParams{ + { + ExcludedAfterDate: "2018-08-29", + }, + }, + ExpectedIds: []string{post1.Id, post2.Id, post3.Id}, + }, + + { + Name: "not after: should return posts on 2018-08-28 or earlier", + SearchParams: []*model.SearchParams{ + { + ExcludedAfterDate: "2018-08-28", + }, + }, + ExpectedIds: []string{post1.Id}, + }, + { + Name: "not after: should return posts on 2018-08-27 or earlier", + SearchParams: []*model.SearchParams{ + { + ExcludedAfterDate: "2018-08-27", + }, + }, + ExpectedIds: []string{}, + }, + { + Name: "not after and not before: should return posts on 2018-08-29", + SearchParams: []*model.SearchParams{ + { + ExcludedAfterDate: "2018-08-29", + ExcludedBeforeDate: "2018-08-29", + }, + }, + ExpectedIds: []string{post2.Id, post3.Id}, + }, + { + Name: "on: should return posts on 2018-08-28", + SearchParams: []*model.SearchParams{ + { + OnDate: "2018-08-28", + }, + }, + ExpectedIds: []string{post1.Id}, + }, + { + Name: "on: should return posts on 2018-08-29", + SearchParams: []*model.SearchParams{ + { + OnDate: "2018-08-29", + }, + }, + ExpectedIds: []string{post2.Id, post3.Id}, + }, + { + Name: "on: should return posts on 2018-08-30", + SearchParams: []*model.SearchParams{ + { + OnDate: "2018-08-30", + }, + }, + ExpectedIds: []string{post4.Id}, + }, + { + Name: "not on: should return all posts but the ones on 2018-08-28", + SearchParams: []*model.SearchParams{ + { + ExcludedDate: "2018-08-28", + }, + }, + ExpectedIds: []string{post2.Id, post3.Id, post4.Id}, + }, + { + Name: "not on: should return all posts but the ones on 2018-08-29", + SearchParams: []*model.SearchParams{ + { + ExcludedDate: "2018-08-29", + }, + }, + ExpectedIds: []string{post1.Id, post4.Id}, + }, + { + Name: "not on: should return all posts but the ones on 2018-08-30", + SearchParams: []*model.SearchParams{ + { + ExcludedDate: "2018-08-30", + }, + }, + ExpectedIds: []string{post1.Id, post2.Id, post3.Id}, + }, + { + Name: "not on and after: should return posts after 2018-08-30", + SearchParams: []*model.SearchParams{ + { + AfterDate: "2018-08-28", + ExcludedDate: "2018-08-29", + }, + }, + ExpectedIds: []string{post4.Id}, + }, + + { + Name: "after with different time zones: should return posts after 2018-08-27 23:00", + SearchParams: []*model.SearchParams{ + { + AfterDate: "2018-08-28", + TimeZoneOffset: earlierTimeZoneOffset, + }, + }, + ExpectedIds: []string{post1.Id, post2.Id, post3.Id, post4.Id}, + }, + { + Name: "after with different time zones: should return posts after 2018-08-28 01:00", + SearchParams: []*model.SearchParams{ + { + AfterDate: "2018-08-28", + TimeZoneOffset: laterTimeZoneOffset, + }, + }, + ExpectedIds: []string{post3.Id, post4.Id}, + }, + { + Name: "before with different time zones: should return posts before 2018-08-27 23:00", + SearchParams: []*model.SearchParams{ + { + BeforeDate: "2018-08-30", + TimeZoneOffset: earlierTimeZoneOffset, + }, + }, + ExpectedIds: []string{post1.Id, post2.Id}, + }, + { + Name: "before with different time zones: should return posts before 2018-08-28 01:00", + SearchParams: []*model.SearchParams{ + { + BeforeDate: "2018-08-30", + TimeZoneOffset: laterTimeZoneOffset, + }, + }, + ExpectedIds: []string{post1.Id, post2.Id, post3.Id, post4.Id}, + }, + { + Name: "on with different time zones: should return posts after 2018-08-28 23:00 and before 2018-08-29 23:00", + SearchParams: []*model.SearchParams{ + { + OnDate: "2018-08-29", + TimeZoneOffset: earlierTimeZoneOffset, + }, + }, + ExpectedIds: []string{post1.Id, post2.Id}, + }, + { + Name: "on with different time zones: should return posts after 2018-08-29 01:00 and before 2018-08-30 01:00", + SearchParams: []*model.SearchParams{ + { + OnDate: "2018-08-29", + TimeZoneOffset: laterTimeZoneOffset, + }, + }, + ExpectedIds: []string{post3.Id, post4.Id}, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + res, err := s.Post().SearchPostsInTeamForUser(tc.SearchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + require.ElementsMatch(t, tc.ExpectedIds, ids) + }) + } +} + +func testSearchESSearchPosts_Hashtags(t *testing.T, s store.Store) { + team, err := s.Team().Save(&model.Team{Name: "t1", DisplayName: "t1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer func() { require.Nil(t, s.Team().PermanentDelete(team.Id)) }() + channel, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "c1", DisplayName: "c1", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + user, err := s.User().Save(createUser("testuser", "testuser", "Test", "User")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user, []string{team.Id}, []string{channel.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user.Id)) }() + + post1, err := s.Post().Save(createPostWithHashtags(user.Id, channel.Id, "From bean to cup, you #hashtag up", "#hashtag")) + require.Nil(t, err) + post2, err := s.Post().Save(createPost(user.Id, channel.Id, "I liek hashtags")) + require.Nil(t, err) + post3, err := s.Post().Save(createPostWithHashtags(user.Id, channel.Id, "moar #hashtag please", "#hashtag")) + require.Nil(t, err) + + testCases := []struct { + Name string + SearchParams []*model.SearchParams + Matches map[string][]string + }{ + { + Name: "Search for plain term hashtag", + SearchParams: []*model.SearchParams{ + { + Terms: "hashtag", + IsHashtag: false, + OrTerms: false, + }, + }, + Matches: map[string][]string{ + post1.Id: {"hashtag", "#hashtag"}, + post2.Id: {"hashtags"}, + post3.Id: {"hashtag", "#hashtag"}, + }, + }, + { + Name: "Search for #hashtag", + SearchParams: []*model.SearchParams{ + { + Terms: "#hashtag", + IsHashtag: true, + OrTerms: false, + }, + }, + Matches: map[string][]string{ + post1.Id: {"#hashtag"}, + post3.Id: {"#hashtag"}, + }, + }, + { + Name: "Search for #hashtag and bean", + SearchParams: []*model.SearchParams{ + { + Terms: "#hashtag", + IsHashtag: true, + OrTerms: false, + }, + { + Terms: "bean", + IsHashtag: false, + OrTerms: false, + }, + }, + Matches: map[string][]string{ + post1.Id: {"bean", "#hashtag"}, + }, + }, + { + Name: "Search for not #hashtag", + SearchParams: []*model.SearchParams{ + { + ExcludedTerms: "#hashtag", + IsHashtag: true, + OrTerms: false, + }, + }, + Matches: map[string][]string{ + post2.Id: {}, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + res, err := s.Post().SearchPostsInTeamForUser(tc.SearchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + + expectedIds := []string{} + for id := range tc.Matches { + expectedIds = append(expectedIds, id) + } + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + require.ElementsMatch(t, expectedIds, ids) + checkMatchesEqual(t, tc.Matches, res.Matches) + }) + } +} + +func testSearchESSearchPosts_HashtagsAndOrWords(t *testing.T, s store.Store) { + team, err := s.Team().Save(&model.Team{Name: "t1", DisplayName: "t1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer func() { require.Nil(t, s.Team().PermanentDelete(team.Id)) }() + channel, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "c1", DisplayName: "c1", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + user, err := s.User().Save(createUser("testuser", "testuser", "Test", "User")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user, []string{team.Id}, []string{channel.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user.Id)) }() + + // Test searching for posts with Markdown underscores in them. + post1, err := s.Post().Save(createPostWithHashtags(user.Id, channel.Id, "From bean to cup, you #hashtag up", "#hashtag")) + require.Nil(t, err) + post2, err := s.Post().Save(createPostWithHashtags(user.Id, channel.Id, "#hashtag mashup city", "#hashtag")) + require.Nil(t, err) + post3, err := s.Post().Save(createPost(user.Id, channel.Id, "has bean")) + require.Nil(t, err) + + testCases := []struct { + Name string + SearchParams []*model.SearchParams + ExpectedIds []string + }{ + { + Name: "Search for bean or #hashtag", + SearchParams: []*model.SearchParams{ + { + Terms: "bean", + IsHashtag: false, + OrTerms: false, + }, + { + Terms: "#hashtag", + IsHashtag: true, + OrTerms: false, + }, + }, + ExpectedIds: []string{post1.Id}, + }, + { + Name: "Search for bean and #hashtag", + SearchParams: []*model.SearchParams{ + { + Terms: "bean", + IsHashtag: false, + OrTerms: true, + }, + { + Terms: "#hashtag", + IsHashtag: true, + OrTerms: true, + }, + }, + ExpectedIds: []string{post1.Id, post2.Id, post3.Id}, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + res, err := s.Post().SearchPostsInTeamForUser(tc.SearchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + require.ElementsMatch(t, tc.ExpectedIds, ids) + }) + } +} + +func testSearchESSearchPosts_HashtagsCaseInsensitive(t *testing.T, s store.Store) { + team, err := s.Team().Save(&model.Team{Name: "t1", DisplayName: "t1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer func() { require.Nil(t, s.Team().PermanentDelete(team.Id)) }() + channel, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "c1", DisplayName: "c1", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + user, err := s.User().Save(createUser("testuser", "testuser", "Test", "User")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user, []string{team.Id}, []string{channel.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user.Id)) }() + + // Test searching for posts with Markdown underscores in them. + post1, err := s.Post().Save(createPostWithHashtags(user.Id, channel.Id, "From bean to cup, you #hashtag up", "#hashtag")) + require.Nil(t, err) + post2, err := s.Post().Save(createPost(user.Id, channel.Id, "I liek hashtags")) + require.Nil(t, err) + post3, err := s.Post().Save(createPostWithHashtags(user.Id, channel.Id, "moar #Hashtag please", "#Hashtag")) + require.Nil(t, err) + + testCases := []struct { + Name string + SearchParams []*model.SearchParams + Matches map[string][]string + }{ + { + Name: "Search for hashtag", + SearchParams: []*model.SearchParams{ + { + Terms: "hashtag", + IsHashtag: false, + OrTerms: false, + }, + }, + Matches: map[string][]string{ + post1.Id: {"hashtag", "#hashtag"}, + post2.Id: {"hashtags"}, + post3.Id: {"Hashtag", "#hashtag"}, + }, + }, + { + Name: "Search for #hashtag", + SearchParams: []*model.SearchParams{ + { + Terms: "#hashtag", + IsHashtag: true, + OrTerms: false, + }, + }, + Matches: map[string][]string{ + post1.Id: {"#hashtag"}, + post3.Id: {"#hashtag"}, + }, + }, + { + Name: "Search for #HashTag", + SearchParams: []*model.SearchParams{ + { + Terms: "#HashTag", + IsHashtag: true, + OrTerms: false, + }, + }, + Matches: map[string][]string{ + post1.Id: {"#hashtag"}, + post3.Id: {"#hashtag"}, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + res, err := s.Post().SearchPostsInTeamForUser(tc.SearchParams, user.Id, team.Id, false, false, 0, 20) + require.Nil(t, err) + + expectedIds := []string{} + for id := range tc.Matches { + expectedIds = append(expectedIds, id) + } + + ids := []string{} + for id := range res.Posts { + ids = append(ids, id) + } + + require.ElementsMatch(t, expectedIds, ids) + checkMatchesEqual(t, tc.Matches, res.Matches) + }) + } +} diff --git a/store/searchtest/testlib.go b/store/searchtest/testlib.go new file mode 100644 index 0000000000..06e2fbd8d1 --- /dev/null +++ b/store/searchtest/testlib.go @@ -0,0 +1,180 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package searchtest + +import ( + "fmt" + "testing" + + "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/store" + "github.com/mattermost/mattermost-server/v5/utils" + + "github.com/stretchr/testify/assert" +) + +const ( + ENGINE_ALL = "all" + ENGINE_MYSQL = "mysql" + ENGINE_POSTGRES = "postgres" + ENGINE_ELASTICSEARCH = "elasticsearch" +) + +type SearchTestEngine struct { + Driver string + BeforeTest func(*testing.T, store.Store) + AfterTest func(*testing.T, store.Store) +} + +type searchTest struct { + Name string + Fn func(*testing.T, store.Store) + Tags []string +} + +func filterTestsByTag(tests []searchTest, tags ...string) []searchTest { + filteredTests := []searchTest{} + for _, test := range tests { + if utils.StringInSlice(ENGINE_ALL, test.Tags) { + filteredTests = append(filteredTests, test) + continue + } + for _, tag := range tags { + if utils.StringInSlice(tag, test.Tags) { + filteredTests = append(filteredTests, test) + break + } + } + } + + return filteredTests +} + +func runTestSearch(t *testing.T, s store.Store, testEngine *SearchTestEngine, tests []searchTest) { + filteredTests := filterTestsByTag(tests, testEngine.Driver) + + for _, test := range filteredTests { + test := test + if testing.Short() { + t.Skip("Skipping advanced search test") + continue + } + + if testEngine.BeforeTest != nil { + testEngine.BeforeTest(t, s) + } + t.Run(test.Name, func(t *testing.T) { test.Fn(t, s) }) + if testEngine.AfterTest != nil { + testEngine.AfterTest(t, s) + } + } +} + +func makeEmail() string { + return "success_" + model.NewId() + "@simulator.amazonses.com" +} + +func assertUsersMatchInAnyOrder(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.ElementsMatch(t, expectedUsernames, actualUsernames) { + assert.ElementsMatch(t, expected, actual) + } +} + +func createUser(username, nickname, firstName, lastName string) *model.User { + user := &model.User{ + Username: username, + Password: username, + Nickname: nickname, + FirstName: firstName, + LastName: lastName, + Email: makeEmail(), + } + + return user +} + +func createPost(userId string, channelId string, message string) *model.Post { + post := &model.Post{ + Message: message, + ChannelId: channelId, + PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), + UserId: userId, + CreateAt: 1000000, + } + + return post +} + +func addUserToTeamsAndChannels(s store.Store, user *model.User, teamIds []string, channelIds []string) error { + for _, teamId := range teamIds { + _, err := s.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user.Id}, -1) + if err != nil { + return err + } + } + + for _, channelId := range channelIds { + _, err := s.Channel().SaveMember(&model.ChannelMember{ChannelId: channelId, UserId: user.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}) + if err != nil { + return err + } + } + + return nil +} + +func checkPostInSearchResults(t *testing.T, postId string, searchResults []string) { + t.Helper() + assert.Contains(t, searchResults, postId, "Did not find expected post in search results.") +} + +func checkPostNotInSearchResults(t *testing.T, postId string, searchResults []string) { + t.Helper() + assert.NotContains(t, searchResults, postId, "Found post in search results that should not be there.") +} + +func checkMatchesEqual(t *testing.T, expected model.PostSearchMatches, actual map[string][]string) { + a := assert.New(t) + + a.Len(actual, len(expected), "Received matches for a different number of posts") + + for postId, expectedMatches := range expected { + a.ElementsMatch(expectedMatches, actual[postId], fmt.Sprintf("%v: expected %v, got %v", postId, expectedMatches, actual[postId])) + } +} + +func createPostWithHashtags(userId string, channelId string, message string, hashtags string) *model.Post { + post := &model.Post{ + Message: message, + ChannelId: channelId, + PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), + UserId: userId, + CreateAt: 1000000, + Hashtags: hashtags, + } + + return post +} + +func createPostAtTime(userId string, channelId string, message string, createAt int64) *model.Post { + post := &model.Post{ + Message: message, + ChannelId: channelId, + PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()), + UserId: userId, + CreateAt: createAt, + } + + return post +} diff --git a/store/searchtest/user_layer.go b/store/searchtest/user_layer.go new file mode 100644 index 0000000000..f98d3ec875 --- /dev/null +++ b/store/searchtest/user_layer.go @@ -0,0 +1,679 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package searchtest + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/store" +) + +var searchUserStoreTests = []searchTest{ + {"Database User Search tests", testSearchDatabaseUserSearch, []string{ENGINE_MYSQL, ENGINE_POSTGRES}}, + {"Elasticsearch search users in channel", testSearchESSearchUsersInChannel, []string{ENGINE_ELASTICSEARCH}}, + {"Elasticsearch search users in team", testSearchESSearchUsersInTeam, []string{ENGINE_ELASTICSEARCH}}, +} + +func TestSearchUserStore(t *testing.T, s store.Store, testEngine *SearchTestEngine) { + runTestSearch(t, s, testEngine, searchUserStoreTests) +} + +func testSearchDatabaseUserSearch(t *testing.T, s store.Store) { + u1 := &model.User{ + Username: "jimbo1" + model.NewId(), + FirstName: "Tim", + LastName: "Bill", + Nickname: "Rob", + Email: "harold" + model.NewId() + "@simulator.amazonses.com", + Roles: "system_user system_admin", + } + _, err := s.User().Save(u1) + require.Nil(t, err) + defer func() { require.Nil(t, s.User().PermanentDelete(u1.Id)) }() + + u2 := &model.User{ + Username: "jim-bobby" + model.NewId(), + Email: makeEmail(), + Roles: "system_user", + } + _, err = s.User().Save(u2) + require.Nil(t, err) + defer func() { require.Nil(t, s.User().PermanentDelete(u2.Id)) }() + + u3 := &model.User{ + Username: "jimbo3" + model.NewId(), + Email: makeEmail(), + DeleteAt: 1, + Roles: "system_admin", + } + _, err = s.User().Save(u3) + require.Nil(t, err) + defer func() { require.Nil(t, s.User().PermanentDelete(u3.Id)) }() + _, err = s.Bot().Save(&model.Bot{ + UserId: u3.Id, + Username: u3.Username, + OwnerId: u1.Id, + }) + require.Nil(t, err) + u3.IsBot = true + defer func() { require.Nil(t, s.Bot().PermanentDelete(u3.Id)) }() + + u5 := &model.User{ + Username: "yu" + model.NewId(), + FirstName: "En", + LastName: "Yu", + Nickname: "enyu", + Email: makeEmail(), + } + _, err = s.User().Save(u5) + require.Nil(t, err) + defer func() { require.Nil(t, s.User().PermanentDelete(u5.Id)) }() + + u6 := &model.User{ + Username: "underscore" + model.NewId(), + FirstName: "Du_", + LastName: "_DE", + Nickname: "lodash", + Email: makeEmail(), + } + _, err = s.User().Save(u6) + require.Nil(t, err) + defer func() { require.Nil(t, s.User().PermanentDelete(u6.Id)) }() + + team, err := s.Team().Save(&model.Team{Name: "t1", DisplayName: "t1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + channel, err := s.Channel().Save(&model.Channel{TeamId: team.Id, Name: "c1", DisplayName: "c1", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + + _, err = s.Team().SaveMember(&model.TeamMember{TeamId: team.Id, UserId: u1.Id}, -1) + require.Nil(t, err) + _, err = s.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: u1.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + _, err = s.Team().SaveMember(&model.TeamMember{TeamId: team.Id, UserId: u2.Id}, -1) + require.Nil(t, err) + _, err = s.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: u2.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + _, err = s.Team().SaveMember(&model.TeamMember{TeamId: team.Id, UserId: u3.Id}, -1) + require.Nil(t, err) + _, err = s.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: u3.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + _, err = s.Team().SaveMember(&model.TeamMember{TeamId: team.Id, UserId: u5.Id}, -1) + require.Nil(t, err) + _, err = s.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: u5.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + _, err = s.Team().SaveMember(&model.TeamMember{TeamId: team.Id, UserId: u6.Id}, -1) + require.Nil(t, err) + _, err = s.Channel().SaveMember(&model.ChannelMember{ChannelId: channel.Id, UserId: u6.Id, NotifyProps: model.GetDefaultChannelNotifyProps()}) + require.Nil(t, err) + + // 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", + team.Id, + "jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "search en", + team.Id, + "en", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u5}, + }, + { + "search email", + team.Id, + u1.Email, + &model.UserSearchOptions{ + AllowEmails: true, + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "search maps * to space", + team.Id, + "jimb*", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "should not return spurious matches", + team.Id, + "harol", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{}, + }, + { + "% should be escaped", + team.Id, + "h%", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{}, + }, + { + "_ should be escaped", + team.Id, + "h_", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{}, + }, + { + "_ should be escaped (2)", + team.Id, + "Du_", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u6}, + }, + { + "_ should be escaped (2)", + team.Id, + "_dE", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u6}, + }, + { + "search jimb, allowing inactive", + team.Id, + "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", + team.Id, + "harol", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowEmails: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "search Tim, search all fields", + team.Id, + "Tim", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowEmails: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "search Tim, don't search full names", + team.Id, + "Tim", + &model.UserSearchOptions{ + AllowFullNames: false, + AllowEmails: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{}, + }, + { + "search Bill, search all fields", + team.Id, + "Bill", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowEmails: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "search Rob, search all fields", + team.Id, + "Rob", + &model.UserSearchOptions{ + AllowFullNames: true, + AllowEmails: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "leading @ should be ignored", + team.Id, + "@jimb", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + }, + []*model.User{u1}, + }, + { + "search jim-bobby with system_user roles", + team.Id, + "jim-bobby", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + Role: "system_user", + }, + []*model.User{u2}, + }, + { + "search jim with system_admin roles", + team.Id, + "jim", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + Role: "system_admin", + }, + []*model.User{u1}, + }, + { + "search ji with system_user roles", + team.Id, + "ji", + &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + Role: "system_user", + }, + []*model.User{u1, u2}, + }, + } + + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.Description, func(t *testing.T) { + users, err := s.User().Search(testCase.TeamId, testCase.Term, testCase.Options) + require.Nil(t, err) + assertUsersMatchInAnyOrder(t, testCase.Expected, users) + }) + } + + t.Run("search empty string", func(t *testing.T) { + searchOptions := &model.UserSearchOptions{ + AllowFullNames: true, + Limit: model.USER_SEARCH_DEFAULT_LIMIT, + } + + users, err := s.User().Search(team.Id, "", searchOptions) + require.Nil(t, err) + assert.Len(t, users, 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, + } + + users, err := s.User().Search(team.Id, "", searchOptions) + require.Nil(t, err) + assert.Len(t, users, 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)) + }) +} + +func testSearchESSearchUsersInChannel(t *testing.T, s store.Store) { + // Create and index some users + // Channels for team 1 + team1, err := s.Team().Save(&model.Team{Name: "team1", DisplayName: "team1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer func() { require.Nil(t, s.Team().PermanentDelete(team1.Id)) }() + channel1, err := s.Channel().Save(&model.Channel{TeamId: team1.Id, Name: "channel", DisplayName: "Test One", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + channel2, err := s.Channel().Save(&model.Channel{TeamId: team1.Id, Name: "channel-second", DisplayName: "Test Two", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + + // Channels for team 2 + team2, err := s.Team().Save(&model.Team{Name: "team2", DisplayName: "team2", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer func() { require.Nil(t, s.Team().PermanentDelete(team2.Id)) }() + channel3, err := s.Channel().Save(&model.Channel{TeamId: team2.Id, Name: "channel_third", DisplayName: "Test Three", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + + // Users in team 1 + user1, err := s.User().Save(createUser("test.one", "userone", "User", "One")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user1, []string{team1.Id}, []string{channel1.Id, channel2.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user1.Id)) }() + + user2, err := s.User().Save(createUser("test.two", "usertwo", "User", "Special Two")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user2, []string{team1.Id}, []string{channel1.Id, channel2.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user2.Id)) }() + + user3, err := s.User().Save(createUser("test.three", "userthree", "User", "Special Three")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user3, []string{team1.Id}, []string{channel2.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user3.Id)) }() + + // Users in team 2 + user4, err := s.User().Save(createUser("test.four", "userfour", "User", "Four")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user4, []string{team2.Id}, []string{channel3.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user4.Id)) }() + + user5, err := s.User().Save(createUser("test.five_split", "userfive", "User", "Five")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user5, []string{team2.Id}, []string{channel3.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user5.Id)) }() + + // Given the default search options + options := &model.UserSearchOptions{ + AllowFullNames: true, + Limit: 100, + } + + testCases := []struct { + Name string + Team string + Channel string + ViewRestrictions *model.ViewUsersRestrictions + Term string + InChannel []string + OutOfChannel []string + }{ + { + Name: "All users in channel1", + Team: team1.Id, + Channel: channel1.Id, + ViewRestrictions: nil, + Term: "", + InChannel: []string{user1.Id, user2.Id}, + OutOfChannel: []string{user3.Id}, + }, + { + Name: "All users in channel1 with channel restrictions", + Team: team1.Id, + Channel: channel1.Id, + ViewRestrictions: &model.ViewUsersRestrictions{Channels: []string{channel1.Id}}, + Term: "", + InChannel: []string{user1.Id, user2.Id}, + OutOfChannel: []string{}, + }, + { + Name: "All users in channel1 with channel all channels restricted", + Team: team1.Id, + Channel: channel1.Id, + ViewRestrictions: &model.ViewUsersRestrictions{Channels: []string{}}, + Term: "", + InChannel: []string{}, + OutOfChannel: []string{}, + }, + { + Name: "All users in channel2", + Team: team1.Id, + Channel: channel2.Id, + ViewRestrictions: nil, + Term: "", + InChannel: []string{user1.Id, user2.Id, user3.Id}, + OutOfChannel: []string{}, + }, + { + Name: "All users in channel3", + Team: team2.Id, + Channel: channel3.Id, + ViewRestrictions: nil, + Term: "", + InChannel: []string{user4.Id, user5.Id}, + OutOfChannel: []string{}, + }, + { + Name: "All users in channel1 with term \"spe\"", + Team: team1.Id, + Channel: channel1.Id, + ViewRestrictions: nil, + Term: "spe", + InChannel: []string{user2.Id}, + OutOfChannel: []string{user3.Id}, + }, + { + Name: "All users in channel1 with term \"spe\" with channel restrictions", + Team: team1.Id, + Channel: channel1.Id, + ViewRestrictions: &model.ViewUsersRestrictions{Channels: []string{channel1.Id}}, + Term: "spe", + InChannel: []string{user2.Id}, + OutOfChannel: []string{}, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + options.ViewRestrictions = tc.ViewRestrictions + res, err := s.User().AutocompleteUsersInChannel(tc.Team, tc.Channel, tc.Term, options) + require.Nil(t, err) + + require.Len(t, res.InChannel, len(tc.InChannel)) + inChannelIds := make([]string, len(res.InChannel)) + for i, user := range res.InChannel { + inChannelIds[i] = user.Id + } + assert.ElementsMatch(t, tc.InChannel, inChannelIds) + + require.Len(t, res.OutOfChannel, len(tc.OutOfChannel)) + outOfChannelIds := make([]string, len(res.OutOfChannel)) + for i, user := range res.OutOfChannel { + outOfChannelIds[i] = user.Id + } + assert.ElementsMatch(t, tc.OutOfChannel, outOfChannelIds) + }) + } +} + +func testSearchESSearchUsersInTeam(t *testing.T, s store.Store) { + // Create and index some users + // Channels for team 1 + team1, err := s.Team().Save(&model.Team{Name: "team1", DisplayName: "team1", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer require.Nil(t, s.Team().PermanentDelete(team1.Id)) + channel1, err := s.Channel().Save(&model.Channel{TeamId: team1.Id, Name: "channel", DisplayName: "Test One", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + channel2, err := s.Channel().Save(&model.Channel{TeamId: team1.Id, Name: "channel-second", DisplayName: "Test Two", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + + // Channels for team 2 + team2, err := s.Team().Save(&model.Team{Name: "team2", DisplayName: "team2", Type: model.TEAM_OPEN}) + require.Nil(t, err) + defer require.Nil(t, s.Team().PermanentDelete(team2.Id)) + channel3, err := s.Channel().Save(&model.Channel{TeamId: team2.Id, Name: "channel_third", DisplayName: "Test Three", Type: model.CHANNEL_OPEN}, -1) + require.Nil(t, err) + + // Users in team 1 + user1, err := s.User().Save(createUser("test.one.split", "userone", "User", "One")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user1, []string{team1.Id}, []string{channel1.Id, channel2.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user1.Id)) }() + + user2, err := s.User().Save(createUser("test.two", "usertwo", "User", "Special Two")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user2, []string{team1.Id}, []string{channel1.Id, channel2.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user2.Id)) }() + + user3, err := s.User().Save(createUser("test.three", "userthree", "User", "Special Three")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user3, []string{team1.Id}, []string{channel2.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user3.Id)) }() + + // Users in team 2 + user4, err := s.User().Save(createUser("test.four-slash", "userfour", "User", "Four")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user4, []string{team2.Id}, []string{channel3.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user4.Id)) }() + + user5, err := s.User().Save(createUser("test.five.split", "userfive", "User", "Five")) + require.Nil(t, err) + require.Nil(t, addUserToTeamsAndChannels(s, user5, []string{team2.Id}, []string{channel3.Id})) + defer func() { require.Nil(t, s.User().PermanentDelete(user5.Id)) }() + + // Given the default search options + options := &model.UserSearchOptions{ + AllowFullNames: true, + Limit: 100, + } + + testCases := []struct { + Name string + Team string + ViewRestrictions *model.ViewUsersRestrictions + Term string + Result []string + }{ + { + Name: "All users in team1", + Team: team1.Id, + ViewRestrictions: nil, + Term: "", + Result: []string{user1.Id, user2.Id, user3.Id}, + }, + { + Name: "All users in team1 with term \"spe\"", + Team: team1.Id, + ViewRestrictions: nil, + Term: "spe", + Result: []string{user2.Id, user3.Id}, + }, + { + Name: "All users in team1 with term \"spe\" and channel restrictions", + Team: team1.Id, + ViewRestrictions: &model.ViewUsersRestrictions{Channels: []string{channel1.Id}}, + Term: "spe", + Result: []string{user2.Id}, + }, + { + Name: "All users in team1 with term \"spe\" and all channels restricted", + Team: team1.Id, + ViewRestrictions: &model.ViewUsersRestrictions{Channels: []string{}}, + Term: "spe", + Result: []string{}, + }, + { + Name: "All users in team2", + Team: team2.Id, + ViewRestrictions: nil, + Term: "", + Result: []string{user4.Id, user5.Id}, + }, + { + Name: "All users in team2 with term FiV", + Team: team2.Id, + ViewRestrictions: nil, + Term: "FiV", + Result: []string{user5.Id}, + }, + { + Name: "All users in team2 by split part of the username with a dot", + Team: team2.Id, + ViewRestrictions: &model.ViewUsersRestrictions{Channels: []string{channel3.Id}}, + Term: "split", + Result: []string{user5.Id}, + }, + { + Name: "All users in team2 by split part of the username with a slash", + Team: team2.Id, + ViewRestrictions: &model.ViewUsersRestrictions{Channels: []string{channel3.Id}}, + Term: "slash", + Result: []string{user4.Id}, + }, + { + Name: "All users in team2 by split part of the username with a -slash", + Team: team2.Id, + ViewRestrictions: &model.ViewUsersRestrictions{Channels: []string{channel3.Id}}, + Term: "-slash", + Result: []string{user4.Id}, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + options.ViewRestrictions = tc.ViewRestrictions + res, err := s.User().Search(tc.Team, tc.Term, options) + require.Nil(t, err) + require.Len(t, tc.Result, len(res)) + + userIds := make([]string, len(res)) + for i, user := range res { + userIds[i] = user.Id + } + assert.ElementsMatch(t, userIds, tc.Result) + }) + } +} diff --git a/store/sqlstore/channel_store_test.go b/store/sqlstore/channel_store_test.go index 7fe4c28240..73bdacba96 100644 --- a/store/sqlstore/channel_store_test.go +++ b/store/sqlstore/channel_store_test.go @@ -12,6 +12,7 @@ import ( "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/store" + "github.com/mattermost/mattermost-server/v5/store/searchtest" "github.com/mattermost/mattermost-server/v5/store/storetest" ) @@ -19,6 +20,10 @@ func TestChannelStore(t *testing.T) { StoreTestWithSqlSupplier(t, storetest.TestChannelStore) } +func TestSearchChannelStore(t *testing.T) { + StoreTestWithSearchTestEngine(t, searchtest.TestSearchChannelStore) +} + func TestChannelSearchQuerySQLInjection(t *testing.T) { for _, st := range storeTypes { t.Run(st.Name, func(t *testing.T) { diff --git a/store/sqlstore/post_store_test.go b/store/sqlstore/post_store_test.go index 6cbaf809c3..d280a24f1c 100644 --- a/store/sqlstore/post_store_test.go +++ b/store/sqlstore/post_store_test.go @@ -6,9 +6,14 @@ package sqlstore import ( "testing" + "github.com/mattermost/mattermost-server/v5/store/searchtest" "github.com/mattermost/mattermost-server/v5/store/storetest" ) func TestPostStore(t *testing.T) { StoreTestWithSqlSupplier(t, storetest.TestPostStore) } + +func TestSearchPostStore(t *testing.T) { + StoreTestWithSearchTestEngine(t, searchtest.TestSearchPostStore) +} diff --git a/store/sqlstore/store_test.go b/store/sqlstore/store_test.go index 3dff4549f0..fb46d2dcbe 100644 --- a/store/sqlstore/store_test.go +++ b/store/sqlstore/store_test.go @@ -9,6 +9,7 @@ import ( "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/store" + "github.com/mattermost/mattermost-server/v5/store/searchtest" "github.com/mattermost/mattermost-server/v5/store/storetest" ) @@ -39,6 +40,31 @@ func StoreTest(t *testing.T, f func(*testing.T, store.Store)) { } } +func StoreTestWithSearchTestEngine(t *testing.T, f func(*testing.T, store.Store, *searchtest.SearchTestEngine)) { + defer func() { + if err := recover(); err != nil { + tearDownStores() + panic(err) + } + }() + + for _, st := range storeTypes { + st := st + searchTestEngine := &searchtest.SearchTestEngine{ + Driver: *st.SqlSettings.DriverName, + AfterTest: func(t *testing.T, s store.Store) { + t.Helper() + + st.SqlSupplier.GetMaster().Exec("TRUNCATE Teams") + st.SqlSupplier.GetMaster().Exec("TRUNCATE Channels") + st.SqlSupplier.GetMaster().Exec("TRUNCATE Users") + }, + } + + t.Run(st.Name, func(t *testing.T) { f(t, st.Store, searchTestEngine) }) + } +} + func StoreTestWithSqlSupplier(t *testing.T, f func(*testing.T, store.Store, storetest.SqlSupplier)) { defer func() { if err := recover(); err != nil { diff --git a/store/sqlstore/user_store_test.go b/store/sqlstore/user_store_test.go index 5af2659f3b..9c9f758c94 100644 --- a/store/sqlstore/user_store_test.go +++ b/store/sqlstore/user_store_test.go @@ -6,9 +6,14 @@ package sqlstore import ( "testing" + "github.com/mattermost/mattermost-server/v5/store/searchtest" "github.com/mattermost/mattermost-server/v5/store/storetest" ) func TestUserStore(t *testing.T) { StoreTestWithSqlSupplier(t, storetest.TestUserStore) } + +func TestSearchUserStore(t *testing.T) { + StoreTestWithSearchTestEngine(t, searchtest.TestSearchUserStore) +} diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index afe1ab4979..0e6a6efcda 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -75,7 +75,6 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("SearchInTeam", func(t *testing.T) { testChannelStoreSearchInTeam(t, ss) }) t.Run("SearchForUserInTeam", func(t *testing.T) { testChannelStoreSearchForUserInTeam(t, ss) }) 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) }) @@ -89,7 +88,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("MaterializedPublicChannels", func(t *testing.T) { testMaterializedPublicChannels(t, ss, s) }) t.Run("GetAllChannelsForExportAfter", func(t *testing.T) { testChannelStoreGetAllChannelsForExportAfter(t, ss) }) t.Run("GetChannelMembersForExport", func(t *testing.T) { testChannelStoreGetChannelMembersForExport(t, ss) }) - t.Run("RemoveAllDeactivatedMembers", func(t *testing.T) { testChannelStoreRemoveAllDeactivatedMembers(t, ss) }) + t.Run("RemoveAllDeactivatedMembers", func(t *testing.T) { testChannelStoreRemoveAllDeactivatedMembers(t, ss, s) }) t.Run("ExportAllDirectChannels", func(t *testing.T) { testChannelStoreExportAllDirectChannels(t, ss, s) }) t.Run("ExportAllDirectChannelsExcludePrivateAndPublic", func(t *testing.T) { testChannelStoreExportAllDirectChannelsExcludePrivateAndPublic(t, ss, s) }) t.Run("ExportAllDirectChannelsDeletedChannel", func(t *testing.T) { testChannelStoreExportAllDirectChannelsDeletedChannel(t, ss, s) }) @@ -5074,139 +5073,6 @@ func testChannelStoreSearchAllChannels(t *testing.T, ss store.Store) { } } -func testChannelStoreAutocompleteInTeamForSearch(t *testing.T, ss store.Store, s SqlSupplier) { - u1 := &model.User{} - u1.Email = MakeEmail() - u1.Username = "user1" + model.NewId() - u1.Nickname = model.NewId() - _, err := ss.User().Save(u1) - require.Nil(t, err) - - u2 := &model.User{} - u2.Email = MakeEmail() - u2.Username = "user2" + model.NewId() - u2.Nickname = model.NewId() - _, err = ss.User().Save(u2) - require.Nil(t, err) - - u3 := &model.User{} - u3.Email = MakeEmail() - u3.Username = "user3" + model.NewId() - u3.Nickname = model.NewId() - _, err = ss.User().Save(u3) - require.Nil(t, err) - - u4 := &model.User{} - u4.Email = MakeEmail() - u4.Username = "user4" + model.NewId() - u4.Nickname = model.NewId() - _, err = ss.User().Save(u4) - require.Nil(t, err) - - o1 := model.Channel{} - o1.TeamId = model.NewId() - o1.DisplayName = "ChannelA" - o1.Name = "zz" + model.NewId() + "b" - o1.Type = model.CHANNEL_OPEN - _, err = ss.Channel().Save(&o1, -1) - require.Nil(t, err) - - m1 := model.ChannelMember{} - m1.ChannelId = o1.Id - m1.UserId = u1.Id - m1.NotifyProps = model.GetDefaultChannelNotifyProps() - _, err = ss.Channel().SaveMember(&m1) - require.Nil(t, err) - - o2 := model.Channel{} - o2.TeamId = model.NewId() - o2.DisplayName = "Channel2" - o2.Name = "zz" + model.NewId() + "b" - o2.Type = model.CHANNEL_OPEN - _, err = ss.Channel().Save(&o2, -1) - require.Nil(t, err) - - m2 := model.ChannelMember{} - m2.ChannelId = o2.Id - m2.UserId = m1.UserId - m2.NotifyProps = model.GetDefaultChannelNotifyProps() - _, err = ss.Channel().SaveMember(&m2) - require.Nil(t, err) - - o3 := model.Channel{} - o3.TeamId = o1.TeamId - o3.DisplayName = "ChannelA" - o3.Name = "zz" + model.NewId() + "b" - o3.Type = model.CHANNEL_OPEN - _, err = ss.Channel().Save(&o3, -1) - require.Nil(t, err) - - m3 := model.ChannelMember{} - m3.ChannelId = o3.Id - m3.UserId = m1.UserId - m3.NotifyProps = model.GetDefaultChannelNotifyProps() - _, err = ss.Channel().SaveMember(&m3) - require.Nil(t, err) - - err = ss.Channel().SetDeleteAt(o3.Id, 100, 100) - require.Nil(t, err, "channel should have been deleted") - - o4 := model.Channel{} - o4.TeamId = o1.TeamId - o4.DisplayName = "ChannelA" - o4.Name = "zz" + model.NewId() + "b" - o4.Type = model.CHANNEL_PRIVATE - _, err = ss.Channel().Save(&o4, -1) - require.Nil(t, err) - - m4 := model.ChannelMember{} - m4.ChannelId = o4.Id - m4.UserId = m1.UserId - m4.NotifyProps = model.GetDefaultChannelNotifyProps() - _, err = ss.Channel().SaveMember(&m4) - require.Nil(t, err) - - o5 := model.Channel{} - o5.TeamId = o1.TeamId - o5.DisplayName = "ChannelC" - o5.Name = "zz" + model.NewId() + "b" - o5.Type = model.CHANNEL_PRIVATE - _, err = ss.Channel().Save(&o5, -1) - require.Nil(t, err) - - _, err = ss.Channel().CreateDirectChannel(u1, u2) - require.Nil(t, err) - _, err = ss.Channel().CreateDirectChannel(u2, u3) - require.Nil(t, err) - - tt := []struct { - name string - term string - includeDeleted bool - expectedMatches int - }{ - {"Empty search (list all)", "", false, 4}, - {"Narrow search", "ChannelA", false, 2}, - {"Wide search", "Cha", false, 3}, - {"Direct messages", "user", false, 1}, - {"Wide search with archived channels", "Cha", true, 4}, - {"Narrow with archived channels", "ChannelA", true, 3}, - {"Direct messages with archived channels", "user", true, 1}, - {"Search without results", "blarg", true, 0}, - } - - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - channels, err := ss.Channel().AutocompleteInTeamForSearch(o1.TeamId, m1.UserId, "ChannelA", false) - require.Nil(t, err) - require.Len(t, *channels, 2) - }) - } - - // Manually truncate Channels table until testlib can handle cleanups - s.GetMaster().Exec("TRUNCATE Channels") -} - func testChannelStoreGetMembersByIds(t *testing.T, ss store.Store) { o1 := model.Channel{} o1.TeamId = model.NewId() @@ -6084,7 +5950,7 @@ func testChannelStoreGetChannelMembersForExport(t *testing.T, ss store.Store) { assert.Equal(t, u1.Id, cmfe1.UserId) } -func testChannelStoreRemoveAllDeactivatedMembers(t *testing.T, ss store.Store) { +func testChannelStoreRemoveAllDeactivatedMembers(t *testing.T, ss store.Store, s SqlSupplier) { // Set up all the objects needed in the store. t1 := model.Team{} t1.DisplayName = "Name" @@ -6162,6 +6028,9 @@ func testChannelStoreRemoveAllDeactivatedMembers(t *testing.T, ss store.Store) { assert.Nil(t, err) assert.Len(t, *d2, 1) assert.Equal(t, u3.Id, (*d2)[0].UserId) + + // Manually truncate Channels table until testlib can handle cleanups + s.GetMaster().Exec("TRUNCATE Channels") } func testChannelStoreExportAllDirectChannels(t *testing.T, ss store.Store, s SqlSupplier) { diff --git a/store/storetest/post_store.go b/store/storetest/post_store.go index f3762494e1..6223e5c58c 100644 --- a/store/storetest/post_store.go +++ b/store/storetest/post_store.go @@ -34,7 +34,6 @@ func TestPostStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("GetPostsBeforeAfter", func(t *testing.T) { testPostStoreGetPostsBeforeAfter(t, ss) }) t.Run("GetPostsSince", func(t *testing.T) { testPostStoreGetPostsSince(t, ss) }) t.Run("GetPostBeforeAfter", func(t *testing.T) { testPostStoreGetPostBeforeAfter(t, ss) }) - t.Run("Search", func(t *testing.T) { testPostStoreSearch(t, ss) }) t.Run("UserCountsWithPostsByDay", func(t *testing.T) { testUserCountsWithPostsByDay(t, ss) }) t.Run("PostCountsByDay", func(t *testing.T) { testPostCountsByDay(t, ss) }) t.Run("GetFlaggedPostsForTeam", func(t *testing.T) { testPostStoreGetFlaggedPostsForTeam(t, ss, s) }) @@ -1319,347 +1318,6 @@ func testPostStoreGetPostBeforeAfter(t *testing.T, ss store.Store) { require.Nil(t, err) } -func testPostStoreSearch(t *testing.T, ss store.Store) { - teamId := model.NewId() - userId := model.NewId() - - u1 := &model.User{} - u1.Username = "usera1" - u1.Email = MakeEmail() - u1, err := ss.User().Save(u1) - require.Nil(t, err) - - t1 := &model.TeamMember{} - t1.TeamId = teamId - t1.UserId = u1.Id - _, err = ss.Team().SaveMember(t1, 1000) - require.Nil(t, err) - - u2 := &model.User{} - u2.Username = "userb2" - u2.Email = MakeEmail() - u2, err = ss.User().Save(u2) - require.Nil(t, err) - - t2 := &model.TeamMember{} - t2.TeamId = teamId - t2.UserId = u2.Id - _, err = ss.Team().SaveMember(t2, 1000) - require.Nil(t, err) - - u3 := &model.User{} - u3.Username = "userc3" - u3.Email = MakeEmail() - u3, err = ss.User().Save(u3) - require.Nil(t, err) - - t3 := &model.TeamMember{} - t3.TeamId = teamId - t3.UserId = u3.Id - _, err = ss.Team().SaveMember(t3, 1000) - require.Nil(t, err) - - c1 := &model.Channel{} - c1.TeamId = teamId - c1.DisplayName = "Channel1" - c1.Name = "channel-x" - c1.Type = model.CHANNEL_OPEN - c1, _ = ss.Channel().Save(c1, -1) - - m1 := model.ChannelMember{} - m1.ChannelId = c1.Id - m1.UserId = userId - m1.NotifyProps = model.GetDefaultChannelNotifyProps() - _, err = ss.Channel().SaveMember(&m1) - require.Nil(t, err) - - c2 := &model.Channel{} - c2.TeamId = teamId - c2.DisplayName = "Channel2" - c2.Name = "channel-y" - c2.Type = model.CHANNEL_OPEN - c2, _ = ss.Channel().Save(c2, -1) - - c3 := &model.Channel{} - c3.TeamId = teamId - c3.DisplayName = "Channel3" - c3.Name = "channel-z" - c3.Type = model.CHANNEL_OPEN - c3, _ = ss.Channel().Save(c3, -1) - - ss.Channel().Delete(c3.Id, model.GetMillis()) - - m3 := model.ChannelMember{} - m3.ChannelId = c3.Id - m3.UserId = userId - m3.NotifyProps = model.GetDefaultChannelNotifyProps() - _, err = ss.Channel().SaveMember(&m3) - require.Nil(t, err) - - o1 := &model.Post{} - o1.ChannelId = c1.Id - o1.UserId = u1.Id - o1.Message = "corey mattermost new york United States" - o1, err = ss.Post().Save(o1) - require.Nil(t, err) - - o1a := &model.Post{} - o1a.ChannelId = c1.Id - o1a.UserId = model.NewId() - o1a.Message = "corey mattermost new york United States" - o1a.Type = model.POST_JOIN_CHANNEL - _, err = ss.Post().Save(o1a) - require.Nil(t, err) - - o2 := &model.Post{} - o2.ChannelId = c1.Id - o2.UserId = u2.Id - o2.Message = "New Jersey United States is where John is from" - o2, err = ss.Post().Save(o2) - require.Nil(t, err) - - o3 := &model.Post{} - o3.ChannelId = c2.Id - o3.UserId = model.NewId() - o3.Message = "New Jersey United States is where John is from corey new york" - _, err = ss.Post().Save(o3) - require.Nil(t, err) - - o4 := &model.Post{} - o4.ChannelId = c1.Id - o4.UserId = model.NewId() - o4.Hashtags = "#hashtag #tagme" - o4.Message = "(message)blargh" - o4, err = ss.Post().Save(o4) - require.Nil(t, err) - - o5 := &model.Post{} - o5.ChannelId = c1.Id - o5.UserId = model.NewId() - o5.Hashtags = "#secret #howdy #tagme" - o5, err = ss.Post().Save(o5) - require.Nil(t, err) - - o6 := &model.Post{} - o6.ChannelId = c3.Id - o6.UserId = model.NewId() - o6.Hashtags = "#hashtag" - o6, err = ss.Post().Save(o6) - require.Nil(t, err) - - o7 := &model.Post{} - o7.ChannelId = c3.Id - o7.UserId = u3.Id - o7.Message = "New Jersey United States is where John is from corey new york" - o7, err = ss.Post().Save(o7) - require.Nil(t, err) - - o8 := &model.Post{} - o8.ChannelId = c3.Id - o8.UserId = model.NewId() - o8.Message = "Deleted" - o8, err = ss.Post().Save(o8) - require.Nil(t, err) - - tt := []struct { - name string - searchParams *model.SearchParams - expectedResultsCount int - expectedMessageResultIds []string - }{ - { - "normal-search-1", - &model.SearchParams{Terms: "corey"}, - 1, - []string{o1.Id}, - }, - { - "normal-search-2", - &model.SearchParams{Terms: "new"}, - 2, - []string{o1.Id, o2.Id}, - }, - { - "normal-search-3", - &model.SearchParams{Terms: "john"}, - 1, - []string{o2.Id}, - }, - { - "wildcard-search", - &model.SearchParams{Terms: "matter*"}, - 1, - []string{o1.Id}, - }, - { - "hashtag-search", - &model.SearchParams{Terms: "#hashtag", IsHashtag: true}, - 1, - []string{o4.Id}, - }, - { - "hashtag-search-2", - &model.SearchParams{Terms: "#secret", IsHashtag: true}, - 1, - []string{o5.Id}, - }, - { - "hashtag-search-with-exclusion", - &model.SearchParams{Terms: "#tagme", ExcludedTerms: "#hashtag", IsHashtag: true}, - 1, - []string{o5.Id}, - }, - { - "no-match-mention", - &model.SearchParams{Terms: "@thisshouldmatchnothing", IsHashtag: true}, - 0, - []string{}, - }, - { - "no-results-search", - &model.SearchParams{Terms: "mattermost jersey"}, - 0, - []string{}, - }, - { - "exclude-search", - &model.SearchParams{Terms: "united", ExcludedTerms: "jersey"}, - 1, - []string{o1.Id}, - }, - { - "multiple-words-search", - &model.SearchParams{Terms: "corey new york"}, - 1, - []string{o1.Id}, - }, - { - "multiple-words-with-exclusion-search", - &model.SearchParams{Terms: "united states", ExcludedTerms: "jersey"}, - 1, - []string{o1.Id}, - }, - { - "multiple-excluded-words-search", - &model.SearchParams{Terms: "united", ExcludedTerms: "corey john"}, - 0, - []string{}, - }, - { - "multiple-wildcard-search", - &model.SearchParams{Terms: "matter* jer*"}, - 0, - []string{}, - }, - { - "multiple-wildcard-with-exclusion-search", - &model.SearchParams{Terms: "unite* state*", ExcludedTerms: "jers*"}, - 1, - []string{o1.Id}, - }, - { - "multiple-wildcard-excluded-words-search", - &model.SearchParams{Terms: "united states", ExcludedTerms: "jers* yor*"}, - 0, - []string{}, - }, - { - "search-with-work-next-to-a-symbol", - &model.SearchParams{Terms: "message blargh"}, - 1, - []string{o4.Id}, - }, - { - "search-with-or", - &model.SearchParams{Terms: "Jersey corey", OrTerms: true}, - 2, - []string{o1.Id, o2.Id}, - }, - { - "exclude-search-with-or", - &model.SearchParams{Terms: "york jersey", ExcludedTerms: "john", OrTerms: true}, - 1, - []string{o1.Id}, - }, - { - "search-with-from-user", - &model.SearchParams{Terms: "united states", FromUsers: []string{"usera1"}, IncludeDeletedChannels: true}, - 1, - []string{o1.Id}, - }, - { - "search-with-multiple-from-user", - &model.SearchParams{Terms: "united states", FromUsers: []string{"usera1", "userc3"}, IncludeDeletedChannels: true}, - 2, - []string{o1.Id, o7.Id}, - }, - { - "search-with-excluded-user", - &model.SearchParams{Terms: "united states", ExcludedUsers: []string{"usera1"}, IncludeDeletedChannels: true}, - 2, - []string{o2.Id, o7.Id}, - }, - { - "search-with-multiple-excluded-user", - &model.SearchParams{Terms: "united states", ExcludedUsers: []string{"usera1", "userb2"}, IncludeDeletedChannels: true}, - 1, - []string{o7.Id}, - }, - { - "search-with-deleted-and-channel-filter", - &model.SearchParams{Terms: "Jersey corey", InChannels: []string{"channel-x"}, IncludeDeletedChannels: true, OrTerms: true}, - 2, - []string{o1.Id, o2.Id}, - }, - { - "search-with-deleted-and-multiple-channel-filter", - &model.SearchParams{Terms: "Jersey corey", InChannels: []string{"channel-x", "channel-z"}, IncludeDeletedChannels: true, OrTerms: true}, - 3, - []string{o1.Id, o2.Id, o7.Id}, - }, - { - "search-with-deleted-and-excluded-channel-filter", - &model.SearchParams{Terms: "Jersey corey", ExcludedChannels: []string{"channel-x"}, IncludeDeletedChannels: true, OrTerms: true}, - 1, - []string{o7.Id}, - }, - { - "search-with-deleted-and-multiple-excluded-channel-filter", - &model.SearchParams{Terms: "Jersey corey", ExcludedChannels: []string{"channel-x", "channel-z"}, IncludeDeletedChannels: true, OrTerms: true}, - 0, - []string{}, - }, - { - "search-with-or-and-deleted", - &model.SearchParams{Terms: "Jersey corey", OrTerms: true, IncludeDeletedChannels: true}, - 3, - []string{o1.Id, o2.Id, o7.Id}, - }, - { - "search-hashtag-deleted", - &model.SearchParams{Terms: "#hashtag", IsHashtag: true, IncludeDeletedChannels: true}, - 2, - []string{o4.Id, o6.Id}, - }, - { - "search-deleted-only", - &model.SearchParams{Terms: "Deleted", IncludeDeletedChannels: true}, - 1, - []string{o8.Id}, - }, - } - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - result, err := ss.Post().Search(teamId, userId, tc.searchParams) - require.Nil(t, err) - require.Len(t, result.Order, tc.expectedResultsCount) - for _, expectedMessageResultId := range tc.expectedMessageResultIds { - assert.Contains(t, result.Order, expectedMessageResultId) - } - }) - } -} - func testUserCountsWithPostsByDay(t *testing.T, ss store.Store) { t1 := &model.Team{} t1.DisplayName = "DisplayName" diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index 40ef0915d4..b1a2be39ba 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -67,7 +67,6 @@ func TestUserStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("UpdateMfaActive", func(t *testing.T) { testUserStoreUpdateMfaActive(t, ss) }) t.Run("GetRecentlyActiveUsersForTeam", func(t *testing.T) { testUserStoreGetRecentlyActiveUsersForTeam(t, ss, s) }) 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) }) @@ -2231,373 +2230,6 @@ func assertUsers(t *testing.T, expected, actual []*model.User) { } } -func assertUsersMatchInAnyOrder(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.ElementsMatch(t, expectedUsernames, actualUsernames) { - assert.ElementsMatch(t, expected, actual) - } -} - -func testUserStoreSearch(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", - Roles: "system_user system_admin", - } - _, err := ss.User().Save(u1) - require.Nil(t, err) - defer func() { require.Nil(t, ss.User().PermanentDelete(u1.Id)) }() - - u2 := &model.User{ - Username: "jim-bobby" + model.NewId(), - Email: MakeEmail(), - Roles: "system_user", - } - _, err = ss.User().Save(u2) - require.Nil(t, err) - defer func() { require.Nil(t, ss.User().PermanentDelete(u2.Id)) }() - - u3 := &model.User{ - Username: "jimbo3" + model.NewId(), - Email: MakeEmail(), - DeleteAt: 1, - Roles: "system_admin", - } - _, err = ss.User().Save(u3) - require.Nil(t, err) - defer func() { require.Nil(t, ss.User().PermanentDelete(u3.Id)) }() - _, err = ss.Bot().Save(&model.Bot{ - UserId: u3.Id, - Username: u3.Username, - OwnerId: u1.Id, - }) - require.Nil(t, err) - u3.IsBot = true - defer func() { require.Nil(t, ss.Bot().PermanentDelete(u3.Id)) }() - - u5 := &model.User{ - Username: "yu" + model.NewId(), - FirstName: "En", - LastName: "Yu", - Nickname: "enyu", - Email: MakeEmail(), - } - _, err = ss.User().Save(u5) - require.Nil(t, err) - defer func() { require.Nil(t, ss.User().PermanentDelete(u5.Id)) }() - - u6 := &model.User{ - Username: "underscore" + model.NewId(), - FirstName: "Du_", - LastName: "_DE", - Nickname: "lodash", - Email: MakeEmail(), - } - _, err = ss.User().Save(u6) - require.Nil(t, err) - defer func() { require.Nil(t, ss.User().PermanentDelete(u6.Id)) }() - - tid := model.NewId() - _, err = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u1.Id}, -1) - require.Nil(t, err) - _, err = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u2.Id}, -1) - require.Nil(t, err) - _, err = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u3.Id}, -1) - require.Nil(t, err) - _, err = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u5.Id}, -1) - require.Nil(t, err) - _, err = ss.Team().SaveMember(&model.TeamMember{TeamId: tid, UserId: u6.Id}, -1) - require.Nil(t, err) - - // 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}, - }, - { - "leading @ should be ignored", - tid, - "@jimb", - &model.UserSearchOptions{ - AllowFullNames: true, - Limit: model.USER_SEARCH_DEFAULT_LIMIT, - }, - []*model.User{u1}, - }, - { - "search jim-bobby with system_user roles", - tid, - "jim-bobby", - &model.UserSearchOptions{ - AllowFullNames: true, - Limit: model.USER_SEARCH_DEFAULT_LIMIT, - Role: "system_user", - }, - []*model.User{u2}, - }, - { - "search jim with system_admin roles", - tid, - "jim", - &model.UserSearchOptions{ - AllowFullNames: true, - Limit: model.USER_SEARCH_DEFAULT_LIMIT, - Role: "system_admin", - }, - []*model.User{u1}, - }, - { - "search ji with system_user roles", - tid, - "ji", - &model.UserSearchOptions{ - AllowFullNames: true, - Limit: model.USER_SEARCH_DEFAULT_LIMIT, - Role: "system_user", - }, - []*model.User{u1, u2}, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.Description, func(t *testing.T) { - users, err := ss.User().Search(testCase.TeamId, testCase.Term, testCase.Options) - require.Nil(t, err) - assertUsersMatchInAnyOrder(t, testCase.Expected, users) - }) - } - - t.Run("search empty string", func(t *testing.T) { - searchOptions := &model.UserSearchOptions{ - AllowFullNames: true, - Limit: model.USER_SEARCH_DEFAULT_LIMIT, - } - - users, err := ss.User().Search(tid, "", searchOptions) - require.Nil(t, err) - assert.Len(t, users, 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, - } - - users, err := ss.User().Search(tid, "", searchOptions) - require.Nil(t, err) - assert.Len(t, users, 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)) - }) -} - func testUserStoreSearchNotInChannel(t *testing.T, ss store.Store) { u1 := &model.User{ Username: "jimbo1" + model.NewId(), diff --git a/testlib/helper.go b/testlib/helper.go index 0ac9e847ef..e9b492749f 100644 --- a/testlib/helper.go +++ b/testlib/helper.go @@ -168,8 +168,16 @@ func (h *MainHelper) GetSQLSupplier() *sqlstore.SqlSupplier { func (h *MainHelper) GetClusterInterface() *FakeClusterInterface { if h.ClusterInterface == nil { - panic("MainHelper not initialized with sql supplier.") + panic("MainHelper not initialized with cluster interface.") } return h.ClusterInterface } + +func (h *MainHelper) GetSearchEngine() *searchengine.Broker { + if h.SearchEngine == nil { + panic("MainHelper not initialized with search engine") + } + + return h.SearchEngine +}