This PR adds functionality to search by a global search prefix. This allows Mattermost to be used across multiple data centers with multiple Elasticsearch instances synchronized using cross-cluster replication. While here, we also add tests cases to cover for some missing search interface methods. For now, no system console setting is exposed. Because IndexPrefix is also not exposed. It can be added later if a need arises. https://mattermost.atlassian.net/browse/MM-63298 ```release-note A new config setting ElasticsearchSettings.GlobalSearchPrefix is added which can be used to search across multiple indices having a common prefix. This is useful in a scenario with multiple Elasticsearch instances, where multiple instances are writing to different indices with different prefixes using the ElasticsearchSettings.IndexPrefix setting. ``` Co-authored-by: Mattermost Build <build@mattermost.com>
504 строки
14 KiB
Go
504 строки
14 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package searchtest
|
|
|
|
import (
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
"github.com/mattermost/mattermost/server/v8/channels/store"
|
|
)
|
|
|
|
type SearchTestHelper struct {
|
|
Context *request.Context
|
|
Store store.Store
|
|
Team *model.Team
|
|
AnotherTeam *model.Team
|
|
User *model.User
|
|
User2 *model.User
|
|
UserAnotherTeam *model.User
|
|
ChannelBasic *model.Channel
|
|
ChannelPrivate *model.Channel
|
|
ChannelAnotherTeam *model.Channel
|
|
ChannelDeleted *model.Channel
|
|
}
|
|
|
|
func (th *SearchTestHelper) SetupBasicFixtures() error {
|
|
// Create teams
|
|
team, err := th.createTeam("searchtest-team", "Searchtest team", model.TeamOpen)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
anotherTeam, err := th.createTeam("another-searchtest-team", "Another Searchtest team", model.TeamOpen)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create users
|
|
user, err := th.createUser("basicusername1", "basicnickname1", "basicfirstname1", "basiclastname1")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
user2, err := th.createUser("basicusername2", "basicnickname2", "basicfirstname2", "basiclastname2")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
useranother, err := th.createUser("basicusername3", "basicnickname3", "basicfirstname3", "basiclastname3")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create channels
|
|
channelBasic, err := th.createChannel(team.Id, "channel-a", "ChannelA", "", model.ChannelTypeOpen, nil, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
channelPrivate, err := th.createChannel(team.Id, "channel-private", "ChannelPrivate", "", model.ChannelTypePrivate, nil, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
channelDeleted, err := th.createChannel(team.Id, "channel-deleted", "ChannelA (deleted)", "", model.ChannelTypeOpen, nil, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
channelAnotherTeam, err := th.createChannel(anotherTeam.Id, "channel-a", "ChannelA", "", model.ChannelTypeOpen, nil, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = th.addUserToTeams(user, []string{team.Id, anotherTeam.Id})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = th.addUserToTeams(user2, []string{team.Id, anotherTeam.Id})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = th.addUserToTeams(useranother, []string{anotherTeam.Id})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = th.addUserToChannels(user, []string{channelBasic.Id, channelPrivate.Id, channelDeleted.Id})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = th.addUserToChannels(user2, []string{channelPrivate.Id, channelDeleted.Id})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = th.addUserToChannels(useranother, []string{channelAnotherTeam.Id})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
th.Team = team
|
|
th.AnotherTeam = anotherTeam
|
|
th.User = user
|
|
th.User2 = user2
|
|
th.UserAnotherTeam = useranother
|
|
th.ChannelBasic = channelBasic
|
|
th.ChannelPrivate = channelPrivate
|
|
th.ChannelAnotherTeam = channelAnotherTeam
|
|
th.ChannelDeleted = channelDeleted
|
|
|
|
return nil
|
|
}
|
|
|
|
func (th *SearchTestHelper) CleanFixtures() error {
|
|
err := th.deleteChannels([]*model.Channel{
|
|
th.ChannelBasic, th.ChannelPrivate, th.ChannelAnotherTeam, th.ChannelDeleted,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = th.deleteTeam(th.Team)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = th.deleteTeam(th.AnotherTeam)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = th.cleanAllUsers([]*model.User{th.User, th.User2, th.UserAnotherTeam})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (th *SearchTestHelper) createTeam(name, displayName, teamType string) (*model.Team, error) {
|
|
return th.Store.Team().Save(&model.Team{
|
|
Name: name,
|
|
DisplayName: displayName,
|
|
Type: teamType,
|
|
})
|
|
}
|
|
|
|
func (th *SearchTestHelper) deleteTeam(team *model.Team) error {
|
|
err := th.Store.Team().RemoveAllMembersByTeam(team.Id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return th.Store.Team().PermanentDelete(team.Id)
|
|
}
|
|
|
|
func (th *SearchTestHelper) makeEmail() string {
|
|
return "success_" + model.NewId() + "@simulator.amazon.com"
|
|
}
|
|
|
|
func (th *SearchTestHelper) createUser(username, nickname, firstName, lastName string) (*model.User, error) {
|
|
return th.Store.User().Save(th.Context, &model.User{
|
|
Username: username,
|
|
Password: username,
|
|
Nickname: nickname,
|
|
FirstName: firstName,
|
|
LastName: lastName,
|
|
Email: th.makeEmail(),
|
|
})
|
|
}
|
|
|
|
func (th *SearchTestHelper) createGuest(username, nickname, firstName, lastName string) (*model.User, error) {
|
|
return th.Store.User().Save(th.Context, &model.User{
|
|
Username: username,
|
|
Password: username,
|
|
Nickname: nickname,
|
|
FirstName: firstName,
|
|
LastName: lastName,
|
|
Email: th.makeEmail(),
|
|
Roles: model.SystemGuestRoleId,
|
|
})
|
|
}
|
|
|
|
func (th *SearchTestHelper) deleteUser(user *model.User) error {
|
|
return th.Store.User().PermanentDelete(th.Context, user.Id)
|
|
}
|
|
|
|
func (th *SearchTestHelper) deleteBotUser(botID string) error {
|
|
if err := th.deleteBot(botID); err != nil {
|
|
return err
|
|
}
|
|
return th.Store.User().PermanentDelete(th.Context, botID)
|
|
}
|
|
|
|
func (th *SearchTestHelper) cleanAllUsers(users []*model.User) error {
|
|
for _, u := range users {
|
|
err := th.deleteUser(u)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (th *SearchTestHelper) createBot(username, displayName, ownerID string) (*model.Bot, error) {
|
|
botModel := &model.Bot{
|
|
Username: username,
|
|
DisplayName: displayName,
|
|
OwnerId: ownerID,
|
|
}
|
|
|
|
user, err := th.Store.User().Save(th.Context, model.UserFromBot(botModel))
|
|
if err != nil {
|
|
return nil, errors.New(err.Error())
|
|
}
|
|
|
|
botModel.UserId = user.Id
|
|
bot, err := th.Store.Bot().Save(botModel)
|
|
if err != nil {
|
|
th.Store.User().PermanentDelete(th.Context, bot.UserId)
|
|
return nil, errors.New(err.Error())
|
|
}
|
|
|
|
return bot, nil
|
|
}
|
|
|
|
func (th *SearchTestHelper) deleteBot(botID string) error {
|
|
err := th.Store.Bot().PermanentDelete(botID)
|
|
if err != nil {
|
|
return errors.New(err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (th *SearchTestHelper) createChannel(teamID, name, displayName, purpose string, channelType model.ChannelType, user *model.User, deleted bool) (*model.Channel, error) {
|
|
channel, err := th.Store.Channel().Save(th.Context, &model.Channel{
|
|
TeamId: teamID,
|
|
DisplayName: displayName,
|
|
Name: name,
|
|
Type: channelType,
|
|
Purpose: purpose,
|
|
}, 999)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if user != nil {
|
|
err = th.addUserToChannels(user, []string{channel.Id})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if deleted {
|
|
err := th.Store.Channel().Delete(channel.Id, model.GetMillis())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
func (th *SearchTestHelper) createDirectChannel(teamID, displayName string, users []*model.User) (*model.Channel, error) {
|
|
channel := &model.Channel{
|
|
TeamId: teamID,
|
|
Name: model.GetDMNameFromIds(users[0].Id, users[1].Id),
|
|
DisplayName: displayName,
|
|
Type: model.ChannelTypeDirect,
|
|
}
|
|
|
|
m1 := &model.ChannelMember{}
|
|
m1.ChannelId = channel.Id
|
|
m1.UserId = users[0].Id
|
|
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
|
|
|
|
m2 := &model.ChannelMember{}
|
|
m2.ChannelId = channel.Id
|
|
m2.UserId = users[1].Id
|
|
m2.NotifyProps = model.GetDefaultChannelNotifyProps()
|
|
|
|
channel, err := th.Store.Channel().SaveDirectChannel(th.Context, channel, m1, m2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return channel, nil
|
|
}
|
|
|
|
func (th *SearchTestHelper) createGroupChannel(teamID, displayName string, users []*model.User) (*model.Channel, error) {
|
|
userIDS := make([]string, 0, len(users))
|
|
for _, user := range users {
|
|
userIDS = append(userIDS, user.Id)
|
|
}
|
|
|
|
group := &model.Channel{
|
|
TeamId: teamID,
|
|
Name: model.GetGroupNameFromUserIds(userIDS),
|
|
DisplayName: displayName,
|
|
Type: model.ChannelTypeGroup,
|
|
}
|
|
|
|
channel, err := th.Store.Channel().Save(th.Context, group, 10000)
|
|
if err != nil {
|
|
return nil, errors.New(err.Error())
|
|
}
|
|
|
|
for _, user := range users {
|
|
err := th.addUserToChannels(user, []string{channel.Id})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
func (th *SearchTestHelper) deleteChannel(channel *model.Channel) error {
|
|
err := th.Store.Channel().PermanentDeleteMembersByChannel(th.Context, channel.Id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return th.Store.Channel().PermanentDelete(th.Context, channel.Id)
|
|
}
|
|
|
|
func (th *SearchTestHelper) deleteChannels(channels []*model.Channel) error {
|
|
for _, channel := range channels {
|
|
err := th.deleteChannel(channel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (th *SearchTestHelper) createPostModel(userID, channelID, message, hashtags, postType string, createAt int64, pinned bool) *model.Post {
|
|
return &model.Post{
|
|
Message: message,
|
|
ChannelId: channelID,
|
|
PendingPostId: model.NewId() + ":" + strconv.FormatInt(model.GetMillis(), 10),
|
|
UserId: userID,
|
|
Hashtags: hashtags,
|
|
IsPinned: pinned,
|
|
CreateAt: createAt,
|
|
Type: postType,
|
|
}
|
|
}
|
|
|
|
func (th *SearchTestHelper) createPost(userID, channelID, message, hashtags, postType string, createAt int64, pinned bool) (*model.Post, error) {
|
|
var creationTime int64 = 1000000
|
|
if createAt > 0 {
|
|
creationTime = createAt
|
|
}
|
|
postModel := th.createPostModel(userID, channelID, message, hashtags, postType, creationTime, pinned)
|
|
return th.Store.Post().Save(th.Context, postModel)
|
|
}
|
|
|
|
func (th *SearchTestHelper) createFileInfoModel(creatorID, postID, channelID, name, content, extension, mimeType string, createAt, size int64) *model.FileInfo {
|
|
return &model.FileInfo{
|
|
CreatorId: creatorID,
|
|
PostId: postID,
|
|
ChannelId: channelID,
|
|
CreateAt: createAt,
|
|
UpdateAt: createAt,
|
|
DeleteAt: 0,
|
|
Name: name,
|
|
Content: content,
|
|
Path: name,
|
|
Extension: extension,
|
|
Size: size,
|
|
MimeType: mimeType,
|
|
}
|
|
}
|
|
|
|
func (th *SearchTestHelper) createFileInfo(creatorID, postID, channelID, name, content, extension, mimeType string, createAt, size int64) (*model.FileInfo, error) {
|
|
var creationTime int64 = 1000000
|
|
if createAt > 0 {
|
|
creationTime = createAt
|
|
}
|
|
fileInfoModel := th.createFileInfoModel(creatorID, postID, channelID, name, content, extension, mimeType, creationTime, size)
|
|
return th.Store.FileInfo().Save(th.Context, fileInfoModel)
|
|
}
|
|
|
|
func (th *SearchTestHelper) createReply(userID, message, hashtags string, parent *model.Post, createAt int64, pinned bool) (*model.Post, error) {
|
|
replyModel := th.createPostModel(userID, parent.ChannelId, message, hashtags, parent.Type, createAt, pinned)
|
|
replyModel.RootId = parent.Id
|
|
return th.Store.Post().Save(th.Context, replyModel)
|
|
}
|
|
|
|
func (th *SearchTestHelper) deleteUserPosts(userID string) error {
|
|
err := th.Store.Post().PermanentDeleteByUser(th.Context, userID)
|
|
if err != nil {
|
|
return errors.New(err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (th *SearchTestHelper) deleteUserFileInfos(userID string) error {
|
|
if _, err := th.Store.FileInfo().PermanentDeleteByUser(th.Context, userID); err != nil {
|
|
return errors.New(err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (th *SearchTestHelper) addUserToTeams(user *model.User, teamIDS []string) error {
|
|
for _, teamID := range teamIDS {
|
|
_, err := th.Store.Team().SaveMember(th.Context, &model.TeamMember{TeamId: teamID, UserId: user.Id}, -1)
|
|
if err != nil {
|
|
return errors.New(err.Error())
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (th *SearchTestHelper) addUserToChannels(user *model.User, channelIDS []string) error {
|
|
for _, channelID := range channelIDS {
|
|
_, err := th.Store.Channel().SaveMember(th.Context, &model.ChannelMember{
|
|
ChannelId: channelID,
|
|
UserId: user.Id,
|
|
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
|
})
|
|
if err != nil {
|
|
return errors.New(err.Error())
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (th *SearchTestHelper) assertUsersMatchInAnyOrder(t *testing.T, expected, actual []*model.User) {
|
|
expectedUsernames := make([]string, 0, len(expected))
|
|
for _, user := range expected {
|
|
user.Sanitize(map[string]bool{})
|
|
expectedUsernames = append(expectedUsernames, user.Username)
|
|
}
|
|
|
|
actualUsernames := make([]string, 0, len(actual))
|
|
for _, user := range actual {
|
|
user.Sanitize(map[string]bool{})
|
|
actualUsernames = append(actualUsernames, user.Username)
|
|
}
|
|
|
|
if assert.ElementsMatch(t, expectedUsernames, actualUsernames) {
|
|
assert.ElementsMatch(t, expected, actual)
|
|
}
|
|
}
|
|
|
|
func (th *SearchTestHelper) checkPostInSearchResults(t *testing.T, postID string, searchResults map[string]*model.Post) {
|
|
t.Helper()
|
|
postIDS := make([]string, 0, len(searchResults))
|
|
for ID := range searchResults {
|
|
postIDS = append(postIDS, ID)
|
|
}
|
|
assert.Contains(t, postIDS, postID, "Did not find expected post in search results.")
|
|
}
|
|
|
|
func (th *SearchTestHelper) checkFileInfoInSearchResults(t *testing.T, fileID string, searchResults map[string]*model.FileInfo) {
|
|
t.Helper()
|
|
fileIDS := make([]string, 0, len(searchResults))
|
|
for ID := range searchResults {
|
|
fileIDS = append(fileIDS, ID)
|
|
}
|
|
assert.Contains(t, fileIDS, fileID, "Did not find expected file in search results.")
|
|
}
|
|
|
|
func (th *SearchTestHelper) checkChannelIdsMatch(t *testing.T, expected []string, results model.ChannelList) {
|
|
t.Helper()
|
|
channelIds := make([]string, len(results))
|
|
for i, channel := range results {
|
|
channelIds[i] = channel.Id
|
|
}
|
|
require.ElementsMatch(t, expected, channelIds)
|
|
}
|
|
|
|
func (th *SearchTestHelper) checkChannelIdsMatchWithTeamData(t *testing.T, expected []string, results model.ChannelListWithTeamData) {
|
|
t.Helper()
|
|
channelIds := make([]string, len(results))
|
|
for i, channel := range results {
|
|
channelIds[i] = channel.Id
|
|
}
|
|
require.ElementsMatch(t, expected, channelIds)
|
|
}
|
|
|
|
type ByChannelDisplayName model.ChannelList
|
|
|
|
func (s ByChannelDisplayName) Len() int { return len(s) }
|
|
func (s ByChannelDisplayName) Swap(i, j int) {
|
|
s[i], s[j] = s[j], s[i]
|
|
}
|
|
func (s ByChannelDisplayName) Less(i, j int) bool {
|
|
if s[i].DisplayName != s[j].DisplayName {
|
|
return s[i].DisplayName < s[j].DisplayName
|
|
}
|
|
|
|
return s[i].Id < s[j].Id
|
|
}
|