// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package app import ( "bufio" "bytes" "encoding/json" "fmt" "os" "path/filepath" "sort" "testing" "time" "github.com/mattermost/mattermost/server/v8/channels/testlib" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/v8/channels/app/imports" "github.com/mattermost/mattermost/server/v8/channels/utils" "github.com/mattermost/mattermost/server/v8/channels/utils/fileutils" ) func TestReactionsOfPost(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() post := th.BasicPost post.HasReactions = true th.BasicUser2.DeleteAt = 1234 reactionObject := model.Reaction{ UserId: th.BasicUser.Id, PostId: post.Id, EmojiName: "smile", CreateAt: model.GetMillis(), } reactionObjectDeleted := model.Reaction{ UserId: th.BasicUser2.Id, PostId: post.Id, EmojiName: "smile", CreateAt: model.GetMillis(), } _, err := th.App.SaveReactionForPost(th.Context, &reactionObject) require.Nil(t, err) _, err = th.App.SaveReactionForPost(th.Context, &reactionObjectDeleted) require.Nil(t, err) reactionsOfPost, err := th.App.BuildPostReactions(th.Context, post.Id) require.Nil(t, err) assert.Equal(t, reactionObject.EmojiName, *(*reactionsOfPost)[0].EmojiName) } func TestExportUserNotifyProps(t *testing.T) { mainHelper.Parallel(t) th := SetupWithStoreMock(t) defer th.TearDown() userNotifyProps := model.StringMap{ model.DesktopNotifyProp: model.UserNotifyAll, model.DesktopSoundNotifyProp: "true", model.EmailNotifyProp: "true", model.PushNotifyProp: model.UserNotifyAll, model.PushStatusNotifyProp: model.StatusOnline, model.ChannelMentionsNotifyProp: "true", model.CommentsNotifyProp: model.CommentsNotifyRoot, model.MentionKeysNotifyProp: "valid,misc", } exportNotifyProps := th.App.buildUserNotifyProps(userNotifyProps) require.Equal(t, userNotifyProps[model.DesktopNotifyProp], *exportNotifyProps.Desktop) require.Equal(t, userNotifyProps[model.DesktopSoundNotifyProp], *exportNotifyProps.DesktopSound) require.Equal(t, userNotifyProps[model.EmailNotifyProp], *exportNotifyProps.Email) require.Equal(t, userNotifyProps[model.PushNotifyProp], *exportNotifyProps.Mobile) require.Equal(t, userNotifyProps[model.PushStatusNotifyProp], *exportNotifyProps.MobilePushStatus) require.Equal(t, userNotifyProps[model.ChannelMentionsNotifyProp], *exportNotifyProps.ChannelTrigger) require.Equal(t, userNotifyProps[model.CommentsNotifyProp], *exportNotifyProps.CommentsTrigger) require.Equal(t, userNotifyProps[model.MentionKeysNotifyProp], *exportNotifyProps.MentionKeys) } func TestExportUserChannels(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() channel := th.BasicChannel user := th.BasicUser team := th.BasicTeam channelName := channel.Name notifyProps := model.StringMap{ model.DesktopNotifyProp: model.UserNotifyAll, model.PushNotifyProp: model.UserNotifyNone, } preference := model.Preference{ UserId: user.Id, Category: model.PreferenceCategoryFavoriteChannel, Name: channel.Id, Value: "true", } _, appErr := th.App.MarkChannelsAsViewed(th.Context, []string{th.BasicPost.ChannelId}, user.Id, "", true, th.App.IsCRTEnabledForUser(th.Context, user.Id)) require.Nil(t, appErr) var preferences model.Preferences preferences = append(preferences, preference) err := th.App.Srv().Store().Preference().Save(preferences) require.NoError(t, err) _, appErr = th.App.UpdateChannelMemberNotifyProps(th.Context, notifyProps, channel.Id, user.Id) require.Nil(t, appErr) exportData, appErr := th.App.buildUserChannelMemberships(th.Context, user.Id, team.Id, false) require.Nil(t, appErr) assert.Equal(t, len(*exportData), 3) for _, data := range *exportData { if *data.Name == channelName { assert.Equal(t, "all", *data.NotifyProps.Desktop) assert.Equal(t, "none", *data.NotifyProps.Mobile) assert.Equal(t, "all", *data.NotifyProps.MarkUnread) // default value assert.True(t, *data.Favorite) assert.NotEqualValues(t, 0, *data.LastViewedAt) assert.NotEqualValues(t, 0, *data.MsgCount) } else { // default values assert.Equal(t, "default", *data.NotifyProps.Desktop) assert.Equal(t, "default", *data.NotifyProps.Mobile) assert.Equal(t, "all", *data.NotifyProps.MarkUnread) assert.False(t, *data.Favorite) assert.EqualValues(t, 0, *data.LastViewedAt) assert.EqualValues(t, 0, *data.MsgCount) } } } func TestCopyEmojiImages(t *testing.T) { mainHelper.Parallel(t) th := SetupWithStoreMock(t) defer th.TearDown() emoji := &model.Emoji{ Id: model.NewId(), } // Creating a dir named `exported_emoji_test` in the root of the repo pathToDir := "../exported_emoji_test" err := os.Mkdir(pathToDir, 0777) require.NoError(t, err) defer os.RemoveAll(pathToDir) filePath := "../data/emoji/" + emoji.Id emojiImagePath := filePath + "/image" _, err = os.Stat(filePath) if os.IsNotExist(err) { err = os.MkdirAll(filePath, 0777) require.NoError(t, err) } // Creating a file with the name `image` to copy it to `exported_emoji_test` _, err = os.OpenFile(filePath+"/image", os.O_RDONLY|os.O_CREATE, 0777) require.NoError(t, err) defer os.RemoveAll(filePath) copyError := th.App.copyEmojiImages(th.Context, emoji.Id, emojiImagePath, pathToDir) require.NoError(t, copyError) _, err = os.Stat(pathToDir + "/" + emoji.Id + "/image") require.False(t, os.IsNotExist(err), "File should exist ") } func TestExportCustomEmoji(t *testing.T) { mainHelper.Parallel(t) th := Setup(t) defer th.TearDown() filePath := "../demo.json" fileWriter, err := os.Create(filePath) require.NoError(t, err) defer os.Remove(filePath) dirNameToExportEmoji := "exported_emoji_test" defer os.RemoveAll("../" + dirNameToExportEmoji) outPath, err := filepath.Abs(filePath) require.NoError(t, err) _, appErr := th.App.exportCustomEmoji(th.Context, nil, fileWriter, outPath, dirNameToExportEmoji, false) require.Nil(t, appErr, "should not have failed") } func TestExportAllUsers(t *testing.T) { mainHelper.Parallel(t) th1 := Setup(t) defer th1.TearDown() // Adding a user and deactivating it to check whether it gets included in bulk export user := th1.CreateUser() _, err := th1.App.UpdateActive(th1.Context, user, false) require.Nil(t, err) var b bytes.Buffer err = th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, err) var th2 *TestHelper if mainHelper.Options.RunParallel { th1.Store.DropAllTables() th2 = th1 } else { th2 = Setup(t) defer th2.TearDown() } i, err := th2.App.BulkImport(th2.Context, &b, nil, false, 5) assert.Nil(t, err) assert.EqualValues(t, 0, i) users1, err := th1.App.GetUsersFromProfiles(&model.UserGetOptions{ Page: 0, PerPage: 10, }) assert.Nil(t, err) users2, err := th2.App.GetUsersFromProfiles(&model.UserGetOptions{ Page: 0, PerPage: 10, }) assert.Nil(t, err) assert.Equal(t, len(users1), len(users2)) assert.ElementsMatch(t, users1, users2) // Checking whether deactivated users were included in bulk export deletedUsers1, err := th1.App.GetUsersFromProfiles(&model.UserGetOptions{ Inactive: true, Page: 0, PerPage: 10, }) assert.Nil(t, err) deletedUsers2, err := th1.App.GetUsersFromProfiles(&model.UserGetOptions{ Inactive: true, Page: 0, PerPage: 10, }) assert.Nil(t, err) assert.Equal(t, len(deletedUsers1), len(deletedUsers2)) assert.ElementsMatch(t, deletedUsers1, deletedUsers2) } func TestExportAllBots(t *testing.T) { mainHelper.Parallel(t) th1 := Setup(t) defer th1.TearDown() u := th1.CreateUser() bot, err := th1.App.CreateBot(th1.Context, &model.Bot{ Username: "bot_1", DisplayName: model.NewId(), OwnerId: u.Id, }) require.Nil(t, err) var b bytes.Buffer err = th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, err) th2 := Setup(t) defer th2.TearDown() i, err := th2.App.BulkImport(th2.Context, &b, nil, false, 5) require.Nil(t, err) assert.EqualValues(t, 0, i) u, err = th2.App.GetUserByUsername(u.Username) require.Nil(t, err) bots, err := th2.App.GetBots(th2.Context, &model.BotGetOptions{ OwnerId: u.Id, Page: 0, PerPage: 10, }) require.Nil(t, err) require.Len(t, bots, 1) assert.Equal(t, bot.Username, bots[0].Username) } func TestExportDMChannel(t *testing.T) { mainHelper.Parallel(t) t.Run("Export a DM channel to another server", func(t *testing.T) { th1 := Setup(t).InitBasic() defer th1.TearDown() // DM Channel ch := th1.CreateDmChannel(th1.BasicUser2) err := th1.App.Srv().Store().Preference().Save(model.Preferences{ { UserId: th1.BasicUser2.Id, Category: model.PreferenceCategoryFavoriteChannel, Name: ch.Id, Value: "true", }, }) require.NoError(t, err) var b bytes.Buffer appErr := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, appErr) channels, nErr := th1.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) require.NoError(t, nErr) assert.Equal(t, 1, len(channels)) th2 := Setup(t).InitBasic() defer th2.TearDown() channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) require.NoError(t, nErr) assert.Equal(t, 0, len(channels)) // import the exported channel i, appErr := th2.App.BulkImport(th2.Context, &b, nil, false, 5) require.Nil(t, appErr) assert.Equal(t, 0, i) // Ensure the Members of the imported DM channel is the same was from the exported channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) require.NoError(t, nErr) require.Len(t, channels, 1) require.Len(t, channels[0].Members, 2) assert.ElementsMatch(t, []string{th1.BasicUser.Username, th1.BasicUser2.Username}, []string{channels[0].Members[0].Username, channels[0].Members[1].Username}) // Ensure the favorited channel was retained fav, nErr := th2.App.Srv().Store().Preference().Get(th2.BasicUser2.Id, model.PreferenceCategoryFavoriteChannel, channels[0].Id) require.NoError(t, nErr) require.NotNil(t, fav) require.Equal(t, "true", fav.Value) }) t.Run("Invalid DM channel export", func(t *testing.T) { th1 := Setup(t).InitBasic() defer th1.TearDown() // DM Channel th1.CreateDmChannel(th1.BasicUser2) channels, nErr := th1.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) require.NoError(t, nErr) assert.Equal(t, 1, len(channels)) appErr := th1.App.PermanentDeleteUser(th1.Context, th1.BasicUser2) require.Nil(t, appErr) appErr = th1.App.PermanentDeleteUser(th1.Context, th1.BasicUser) require.Nil(t, appErr) var b bytes.Buffer appErr = th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, appErr) th2 := Setup(t).InitBasic() defer th2.TearDown() // import the exported channel _, appErr = th2.App.BulkImport(th2.Context, &b, nil, true, 5) require.Nil(t, appErr) channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) require.NoError(t, nErr) assert.Empty(t, channels) }) t.Run("Should not export DM channel if other user is permanently deleted", func(t *testing.T) { th1 := Setup(t).InitBasic() defer th1.TearDown() // Create a DM Channel with another user dmc1 := th1.CreateDmChannel(th1.BasicUser2) th1.CreatePost(dmc1) // Create a DM Channel with self dmc2 := th1.CreateDmChannel(th1.BasicUser) th1.CreatePost(dmc2) channels, nErr := th1.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) require.NoError(t, nErr) assert.Equal(t, 2, len(channels)) // Permanentley delete other user appErr := th1.App.PermanentDeleteUser(th1.Context, th1.BasicUser2) require.Nil(t, appErr) var b bytes.Buffer err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, err) th2 := Setup(t).InitBasic() defer th2.TearDown() // import the exported channel _, err = th2.App.BulkImport(th2.Context, &b, nil, false, 5) require.Nil(t, err) channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) require.NoError(t, nErr) assert.Equal(t, 1, len(channels)) // Ensure the posts of the deleted DM channel do not leak to the self-DM channel posts, nErr := th2.App.Srv().Store().Post().GetPosts(th2.Context, model.GetPostsOptions{ ChannelId: channels[0].Id, PerPage: 1000, IncludeDeleted: true, }, false, nil) require.NoError(t, nErr) assert.Equal(t, 1, len(posts.Posts)) }) } func TestExportDMChannelToSelf(t *testing.T) { mainHelper.Parallel(t) th1 := Setup(t).InitBasic() defer th1.TearDown() // DM Channel with self (me channel) th1.CreateDmChannel(th1.BasicUser) var b bytes.Buffer err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, err) channels, nErr := th1.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) require.NoError(t, nErr) assert.Equal(t, 1, len(channels)) th2 := Setup(t) defer th2.TearDown() channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) require.NoError(t, nErr) assert.Equal(t, 0, len(channels)) // import the exported channel i, err := th2.App.BulkImport(th2.Context, &b, nil, false, 5) assert.Nil(t, err) assert.EqualValues(t, 0, i) channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) require.NoError(t, nErr) assert.Equal(t, 1, len(channels)) assert.Equal(t, 1, len((channels[0].Members))) assert.Equal(t, th1.BasicUser.Username, channels[0].Members[0].Username) } func TestExportGMChannel(t *testing.T) { mainHelper.Parallel(t) th1 := Setup(t).InitBasic() user1 := th1.CreateUser() th1.LinkUserToTeam(user1, th1.BasicTeam) user2 := th1.CreateUser() th1.LinkUserToTeam(user2, th1.BasicTeam) // GM Channel th1.CreateGroupChannel(th1.Context, user1, user2) var b bytes.Buffer err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, err) channels, nErr := th1.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) require.NoError(t, nErr) assert.Equal(t, 1, len(channels)) th1.TearDown() th2 := Setup(t) defer th2.TearDown() channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) require.NoError(t, nErr) assert.Equal(t, 0, len(channels)) } func TestExportGMandDMChannels(t *testing.T) { mainHelper.Parallel(t) th1 := Setup(t).InitBasic() // DM Channel th1.CreateDmChannel(th1.BasicUser2) user1 := th1.CreateUser() th1.LinkUserToTeam(user1, th1.BasicTeam) user2 := th1.CreateUser() th1.LinkUserToTeam(user2, th1.BasicTeam) // GM Channel th1.CreateGroupChannel(th1.Context, user1, user2) var b bytes.Buffer err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, err) channels, nErr := th1.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) require.NoError(t, nErr) assert.Equal(t, 2, len(channels)) th1.TearDown() th2 := Setup(t) defer th2.TearDown() channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) require.NoError(t, nErr) assert.Equal(t, 0, len(channels)) // import the exported channel i, err := th2.App.BulkImport(th2.Context, &b, nil, false, 5) assert.Nil(t, err) assert.Equal(t, 0, i) // Ensure the Members of the imported GM channel is the same was from the exported channels, nErr = th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) require.NoError(t, nErr) // Adding some determinism so its possible to assert on slice index sort.Slice(channels, func(i, j int) bool { return channels[i].Type > channels[j].Type }) assert.Equal(t, 2, len(channels)) assert.ElementsMatch(t, []string{th1.BasicUser.Username, user1.Username, user2.Username}, []string{channels[0].Members[0].Username, channels[0].Members[1].Username, channels[0].Members[2].Username}) assert.ElementsMatch(t, []string{th1.BasicUser.Username, th1.BasicUser2.Username}, []string{channels[1].Members[0].Username, channels[1].Members[1].Username}) } func TestExportDMandGMPost(t *testing.T) { mainHelper.Parallel(t) th1 := Setup(t).InitBasic() // DM Channel dmChannel := th1.CreateDmChannel(th1.BasicUser2) dmMembers := []string{th1.BasicUser.Username, th1.BasicUser2.Username} user1 := th1.CreateUser() th1.LinkUserToTeam(user1, th1.BasicTeam) user2 := th1.CreateUser() th1.LinkUserToTeam(user2, th1.BasicTeam) // GM Channel gmChannel := th1.CreateGroupChannel(th1.Context, user1, user2) gmMembers := []string{th1.BasicUser.Username, user1.Username, user2.Username} // DM posts p1 := &model.Post{ ChannelId: dmChannel.Id, Message: "aa" + model.NewId() + "a", UserId: th1.BasicUser.Id, } _, appErr := th1.App.CreatePost(th1.Context, p1, dmChannel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, appErr) p2 := &model.Post{ ChannelId: dmChannel.Id, Message: "bb" + model.NewId() + "a", UserId: th1.BasicUser.Id, } _, appErr = th1.App.CreatePost(th1.Context, p2, dmChannel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, appErr) // GM posts p3 := &model.Post{ ChannelId: gmChannel.Id, Message: "cc" + model.NewId() + "a", UserId: th1.BasicUser.Id, } _, appErr = th1.App.CreatePost(th1.Context, p3, gmChannel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, appErr) p4 := &model.Post{ ChannelId: gmChannel.Id, Message: "dd" + model.NewId() + "a", UserId: th1.BasicUser.Id, } _, appErr = th1.App.CreatePost(th1.Context, p4, gmChannel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, appErr) posts, err := th1.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000", false) require.NoError(t, err) assert.Equal(t, 4, len(posts)) var b bytes.Buffer appErr = th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, appErr) th1.TearDown() th2 := Setup(t) defer th2.TearDown() posts, err = th2.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000", false) require.NoError(t, err) assert.Equal(t, 0, len(posts)) // import the exported posts i, appErr := th2.App.BulkImport(th2.Context, &b, nil, false, 5) assert.Nil(t, appErr) assert.Equal(t, 0, i) posts, err = th2.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000", false) require.NoError(t, err) // Adding some determinism so its possible to assert on slice index sort.Slice(posts, func(i, j int) bool { return posts[i].Message > posts[j].Message }) assert.Equal(t, 4, len(posts)) assert.ElementsMatch(t, gmMembers, *posts[0].ChannelMembers) assert.ElementsMatch(t, gmMembers, *posts[1].ChannelMembers) assert.ElementsMatch(t, dmMembers, *posts[2].ChannelMembers) assert.ElementsMatch(t, dmMembers, *posts[3].ChannelMembers) } func TestExportPostWithProps(t *testing.T) { mainHelper.Parallel(t) th1 := Setup(t).InitBasic() attachments := []*model.SlackAttachment{{Footer: "footer"}} // DM Channel dmChannel := th1.CreateDmChannel(th1.BasicUser2) dmMembers := []string{th1.BasicUser.Username, th1.BasicUser2.Username} user1 := th1.CreateUser() th1.LinkUserToTeam(user1, th1.BasicTeam) user2 := th1.CreateUser() th1.LinkUserToTeam(user2, th1.BasicTeam) // GM Channel gmChannel := th1.CreateGroupChannel(th1.Context, user1, user2) gmMembers := []string{th1.BasicUser.Username, user1.Username, user2.Username} // DM posts p1 := &model.Post{ ChannelId: dmChannel.Id, Message: "aa" + model.NewId() + "a", Props: map[string]any{ model.PostPropsAttachments: attachments, }, UserId: th1.BasicUser.Id, } _, appErr := th1.App.CreatePost(th1.Context, p1, dmChannel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, appErr) p2 := &model.Post{ ChannelId: gmChannel.Id, Message: "dd" + model.NewId() + "a", Props: map[string]any{ model.PostPropsAttachments: attachments, }, UserId: th1.BasicUser.Id, } _, appErr = th1.App.CreatePost(th1.Context, p2, gmChannel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, appErr) posts, err := th1.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000", false) require.NoError(t, err) assert.Len(t, posts, 2) require.NotEmpty(t, posts[0].Props) require.NotEmpty(t, posts[1].Props) var b bytes.Buffer appErr = th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, appErr) th1.TearDown() th2 := Setup(t) defer th2.TearDown() posts, err = th2.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000", false) require.NoError(t, err) assert.Len(t, posts, 0) // import the exported posts i, appErr := th2.App.BulkImport(th2.Context, &b, nil, false, 5) assert.Nil(t, appErr) assert.Equal(t, 0, i) posts, err = th2.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000", false) require.NoError(t, err) // Adding some determinism so its possible to assert on slice index sort.Slice(posts, func(i, j int) bool { return posts[i].Message > posts[j].Message }) assert.Len(t, posts, 2) assert.ElementsMatch(t, gmMembers, *posts[0].ChannelMembers) assert.ElementsMatch(t, dmMembers, *posts[1].ChannelMembers) assert.Contains(t, posts[0].Props[model.PostPropsAttachments].([]any)[0], "footer") assert.Contains(t, posts[1].Props[model.PostPropsAttachments].([]any)[0], "footer") } func TestExportUserCustomStatus(t *testing.T) { mainHelper.Parallel(t) th1 := Setup(t).InitBasic() cs := &model.CustomStatus{ Emoji: "palm_tree", Text: "on a vacation", Duration: "this_week", ExpiresAt: time.Now().Add(24 * time.Hour), } appErr := th1.App.SetCustomStatus(th1.Context, th1.BasicUser.Id, cs) require.Nil(t, appErr) uname := th1.BasicUser.Username var b bytes.Buffer appErr = th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, appErr) th1.TearDown() th2 := Setup(t) defer th2.TearDown() i, appErr := th2.App.BulkImport(th2.Context, &b, nil, false, 1) require.Nil(t, appErr) assert.Equal(t, 0, i) gotUser, err := th2.Server.Store().User().GetByUsername(uname) require.NoError(t, err) gotCs := gotUser.GetCustomStatus() require.Equal(t, cs.Emoji, gotCs.Emoji) require.Equal(t, cs.Text, gotCs.Text) } func TestExportDMPostWithSelf(t *testing.T) { mainHelper.Parallel(t) th1 := Setup(t).InitBasic() // DM Channel with self (me channel) dmChannel := th1.CreateDmChannel(th1.BasicUser) th1.CreatePost(dmChannel) var b bytes.Buffer err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, err) posts, nErr := th1.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000", false) require.NoError(t, nErr) assert.Equal(t, 1, len(posts)) th1.TearDown() th2 := Setup(t) defer th2.TearDown() posts, nErr = th2.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000", false) require.NoError(t, nErr) assert.Equal(t, 0, len(posts)) // import the exported posts i, err := th2.App.BulkImport(th2.Context, &b, nil, false, 5) assert.Nil(t, err) assert.Equal(t, 0, i) posts, nErr = th2.App.Srv().Store().Post().GetDirectPostParentsForExportAfter(1000, "0000000", false) require.NoError(t, nErr) assert.Equal(t, 1, len(posts)) assert.Equal(t, 1, len((*posts[0].ChannelMembers))) assert.Equal(t, th1.BasicUser.Username, (*posts[0].ChannelMembers)[0]) } func TestExportPostsWithThread(t *testing.T) { mainHelper.Parallel(t) th1 := Setup(t).InitBasic() defer th1.TearDown() assertThreadFollowers := func(t *testing.T, b *bytes.Buffer, postCreateAt int64, userNames []string) { scanner := bufio.NewScanner(b) usersToAssert := make([]string, 0) for scanner.Scan() { var line imports.LineImportData err := json.Unmarshal(scanner.Bytes(), &line) require.NoError(t, err) switch line.Type { case "post": postLine := line.Post require.NotNil(t, postLine) if postLine.CreateAt != nil && *postLine.CreateAt != postCreateAt { continue } for _, follower := range *postLine.ThreadFollowers { if follower.User == nil { require.Fail(t, "follower.User is nil") } usersToAssert = append(usersToAssert, *follower.User) } case "direct_post": postLine := line.DirectPost require.NotNil(t, postLine) if postLine.CreateAt != nil && *postLine.CreateAt != postCreateAt { continue } for _, follower := range *postLine.ThreadFollowers { if follower.User == nil { require.Fail(t, "follower.User is nil") } usersToAssert = append(usersToAssert, *follower.User) } default: continue } } require.ElementsMatch(t, userNames, usersToAssert) } t.Run("Export thread followers for a thread (public channel)", func(t *testing.T) { thread := th1.CreatePost(th1.BasicChannel) _ = th1.CreatePostReply(thread) appErr := th1.App.UpdateThreadFollowForUser(th1.BasicUser2.Id, th1.BasicTeam.Id, thread.Id, true) require.Nil(t, appErr) member1, appErr := th1.App.GetThreadMembershipForUser(th1.BasicUser.Id, thread.Id) require.Nil(t, appErr) require.NotNil(t, member1) member2, appErr := th1.App.GetThreadMembershipForUser(th1.BasicUser2.Id, thread.Id) require.Nil(t, appErr) require.NotNil(t, member2) var b bytes.Buffer err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, err) assertThreadFollowers(t, &b, thread.CreateAt, []string{th1.BasicUser.Username, th1.BasicUser2.Username}) }) t.Run("Export thread followers for a thread (direct messages)", func(t *testing.T) { dmc := th1.CreateDmChannel(th1.BasicUser2) thread := th1.CreatePost(dmc) _ = th1.CreatePostReply(thread) appErr := th1.App.UpdateThreadFollowForUser(th1.BasicUser2.Id, th1.BasicTeam.Id, thread.Id, true) require.Nil(t, appErr) member1, appErr := th1.App.GetThreadMembershipForUser(th1.BasicUser.Id, thread.Id) require.Nil(t, appErr) require.NotNil(t, member1) member2, appErr := th1.App.GetThreadMembershipForUser(th1.BasicUser2.Id, thread.Id) require.Nil(t, appErr) require.NotNil(t, member2) var b bytes.Buffer err := th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, err) assertThreadFollowers(t, &b, thread.CreateAt, []string{th1.BasicUser.Username, th1.BasicUser2.Username}) }) } func TestExportFileWarnings(t *testing.T) { testCases := []struct { Description string ConfigFunc func(cfg *model.Config) }{ { "local", func(cfg *model.Config) { cfg.FileSettings.DriverName = model.NewPointer(model.ImageDriverLocal) }, }, { "s3", func(cfg *model.Config) { s3Host := os.Getenv("CI_MINIO_HOST") if s3Host == "" { s3Host = "localhost" } s3Port := os.Getenv("CI_MINIO_PORT") if s3Port == "" { s3Port = "9000" } s3Endpoint := fmt.Sprintf("%s:%s", s3Host, s3Port) cfg.FileSettings.DriverName = model.NewPointer(model.ImageDriverS3) cfg.FileSettings.AmazonS3AccessKeyId = model.NewPointer(model.MinioAccessKey) cfg.FileSettings.AmazonS3SecretAccessKey = model.NewPointer(model.MinioSecretKey) cfg.FileSettings.AmazonS3Bucket = model.NewPointer(model.MinioBucket) cfg.FileSettings.AmazonS3PathPrefix = model.NewPointer("") cfg.FileSettings.AmazonS3Endpoint = model.NewPointer(s3Endpoint) cfg.FileSettings.AmazonS3Region = model.NewPointer("") cfg.FileSettings.AmazonS3SSL = model.NewPointer(false) }, }, } for _, testCase := range testCases { t.Run(testCase.Description, func(t *testing.T) { th := Setup(t) defer th.TearDown() th.App.UpdateConfig(testCase.ConfigFunc) // Create a buffer to capture logs buffer := &mlog.Buffer{} err := mlog.AddWriterTarget(th.TestLogger, buffer, true, mlog.StdAll...) require.NoError(t, err) testsDir, _ := fileutils.FindDir("tests") dir, err := os.MkdirTemp("", "import_test") require.NoError(t, err) defer os.RemoveAll(dir) extractImportFile := func(filePath string) *os.File { importFile, err2 := os.Open(filePath) require.NoError(t, err2) defer importFile.Close() info, err2 := importFile.Stat() require.NoError(t, err2) paths, err2 := utils.UnzipToPath(importFile, info.Size(), dir) require.NoError(t, err2) require.NotEmpty(t, paths) jsonFile, err2 := os.Open(filepath.Join(dir, "import.jsonl")) require.NoError(t, err2) return jsonFile } jsonFile := extractImportFile(filepath.Join(testsDir, "import_test.zip")) defer jsonFile.Close() _, appErr := th.App.BulkImportWithPath(th.Context, jsonFile, nil, false, true, 1, dir) require.Nil(t, appErr) // delete one of the files params := &model.SearchParams{Terms: "test3.png", SearchWithoutUserId: true} results, err := th.App.Srv().Store().FileInfo().Search(th.Context, []*model.SearchParams{params}, "", "", 0, 20) require.NoError(t, err) require.Len(t, results.FileInfos, 1) for _, info2 := range results.FileInfos { err2 := th.App.RemoveFile(info2.Path) require.Nil(t, err2) } exportFile, err := os.Create(filepath.Join(dir, "export.zip")) require.NoError(t, err) job, appErr := th.App.Srv().Jobs.CreateJob(th.Context, model.JobTypeExportProcess, nil) require.Nil(t, appErr) newJob, appErr := th.App.Srv().Jobs.ClaimJob(job) require.Nil(t, appErr) require.NotNil(t, newJob) opts := model.BulkExportOpts{ IncludeAttachments: true, CreateArchive: true, } appErr = th.App.BulkExport(th.Context, exportFile, dir, newJob, opts) // should not get an error for the missing file require.Nil(t, appErr) // should get a warning instead: testlib.AssertLog(t, buffer, mlog.LvlWarn.Name, "Unable to export file attachment") // should get info in the newJob data: newJob, appErr = th.App.Srv().Jobs.GetJob(th.Context, newJob.Id) require.Nil(t, appErr) warnings, ok := newJob.Data["num_warnings"] require.True(t, ok) require.Equal(t, "1", warnings) exportFile.Close() // Verify warnings.txt exists in the zip and contains expected content exportZipPath := filepath.Join(dir, "export.zip") exportZipFile, err := os.Open(exportZipPath) require.NoError(t, err) defer exportZipFile.Close() info, err := exportZipFile.Stat() require.NoError(t, err) paths, err := utils.UnzipToPath(exportZipFile, info.Size(), dir) require.NoError(t, err) require.Contains(t, paths, filepath.Join(dir, warningsFilename)) warningsContent, err := os.ReadFile(filepath.Join(dir, warningsFilename)) require.NoError(t, err) require.Contains(t, string(warningsContent), "Unable to export file attachment, attachment path:") }) } } func TestBulkExport(t *testing.T) { mainHelper.Parallel(t) th := Setup(t) testsDir, _ := fileutils.FindDir("tests") dir, err := os.MkdirTemp("", "import_test") require.NoError(t, err) defer os.RemoveAll(dir) extractImportFile := func(filePath string) *os.File { importFile, err2 := os.Open(filePath) require.NoError(t, err2) defer importFile.Close() info, err2 := importFile.Stat() require.NoError(t, err2) paths, err2 := utils.UnzipToPath(importFile, info.Size(), dir) require.NoError(t, err2) require.NotEmpty(t, paths) jsonFile, err2 := os.Open(filepath.Join(dir, "import.jsonl")) require.NoError(t, err2) return jsonFile } jsonFile := extractImportFile(filepath.Join(testsDir, "import_test.zip")) defer jsonFile.Close() _, appErr := th.App.BulkImportWithPath(th.Context, jsonFile, nil, false, true, 1, dir) require.Nil(t, appErr) exportFile, err := os.Create(filepath.Join(dir, "export.zip")) require.NoError(t, err) defer exportFile.Close() opts := model.BulkExportOpts{ IncludeAttachments: true, CreateArchive: true, } appErr = th.App.BulkExport(th.Context, exportFile, dir, nil, opts) require.Nil(t, appErr) th.TearDown() th = Setup(t) defer th.TearDown() jsonFile = extractImportFile(filepath.Join(dir, "export.zip")) defer jsonFile.Close() _, appErr = th.App.BulkImportWithPath(th.Context, jsonFile, nil, false, true, 1, filepath.Join(dir, "data")) require.Nil(t, appErr) } func TestBuildPostReplies(t *testing.T) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() createPostWithAttachments := func(th *TestHelper, n int, rootID string) *model.Post { var fileIDs []string for i := range n { info, err := th.App.Srv().Store().FileInfo().Save(th.Context, &model.FileInfo{ CreatorId: th.BasicUser.Id, Name: fmt.Sprintf("file%d", i), Path: fmt.Sprintf("/data/file%d", i), }) require.NoError(t, err) fileIDs = append(fileIDs, info.Id) } post, err := th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, ChannelId: th.BasicChannel.Id, RootId: rootID, FileIds: fileIDs}, th.BasicChannel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, err) return post } t.Run("basic post", func(t *testing.T) { data, attachments, err := th.App.buildPostReplies(th.Context, th.BasicPost.Id, true) require.Nil(t, err) require.Empty(t, data) require.Empty(t, attachments) }) t.Run("root post with attachments and no replies", func(t *testing.T) { post := createPostWithAttachments(th, 5, "") data, attachments, err := th.App.buildPostReplies(th.Context, post.Id, true) require.Nil(t, err) require.Empty(t, data) require.Empty(t, attachments) }) t.Run("root post with attachments and a reply", func(t *testing.T) { post := createPostWithAttachments(th, 5, "") createPostWithAttachments(th, 0, post.Id) data, attachments, err := th.App.buildPostReplies(th.Context, post.Id, true) require.Nil(t, err) require.Len(t, data, 1) require.Empty(t, attachments) }) t.Run("root post with attachments and multiple replies with attachments", func(t *testing.T) { post := createPostWithAttachments(th, 5, "") reply1 := createPostWithAttachments(th, 2, post.Id) reply2 := createPostWithAttachments(th, 3, post.Id) data, attachments, err := th.App.buildPostReplies(th.Context, post.Id, true) require.Nil(t, err) require.Len(t, data, 2) require.Len(t, attachments, 5) if reply1.Id < reply2.Id { require.Len(t, *data[0].Attachments, 2) require.Len(t, *data[1].Attachments, 3) } else { require.Len(t, *data[1].Attachments, 2) require.Len(t, *data[0].Attachments, 3) } }) } func TestExportDeletedTeams(t *testing.T) { mainHelper.Parallel(t) th1 := Setup(t).InitBasic() defer th1.TearDown() team1 := th1.CreateTeam() channel1 := th1.CreateChannel(th1.Context, team1) th1.CreatePost(channel1) // Delete the team to check that this is handled correctly on import. err := th1.App.SoftDeleteTeam(team1.Id) require.Nil(t, err) var b bytes.Buffer err = th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, err) var th2 *TestHelper if mainHelper.Options.RunParallel { th1.Store.DropAllTables() th2 = th1 } else { th2 = Setup(t) defer th2.TearDown() } i, err := th2.App.BulkImport(th2.Context, &b, nil, false, 5) assert.Nil(t, err) assert.Equal(t, 0, i) teams1, err := th1.App.GetAllTeams() assert.Nil(t, err) teams2, err := th2.App.GetAllTeams() assert.Nil(t, err) assert.Equal(t, len(teams1), len(teams2)) assert.ElementsMatch(t, teams1, teams2) channels1, err := th1.App.GetAllChannels(th1.Context, 0, 10, model.ChannelSearchOpts{}) assert.Nil(t, err) channels2, err := th2.App.GetAllChannels(th1.Context, 0, 10, model.ChannelSearchOpts{}) assert.Nil(t, err) assert.Equal(t, len(channels1), len(channels2)) assert.ElementsMatch(t, channels1, channels2) for _, team := range teams2 { assert.NotContains(t, team.Name, team1.Name) assert.NotContains(t, team.Id, team1.Id) } } func TestExportArchivedChannels(t *testing.T) { mainHelper.Parallel(t) th1 := Setup(t).InitBasic() defer th1.TearDown() archivedChannel := th1.CreateChannel(th1.Context, th1.BasicTeam) th1.CreatePost(archivedChannel) appErr := th1.App.DeleteChannel(th1.Context, archivedChannel, th1.SystemAdminUser.Id) require.Nil(t, appErr) var b bytes.Buffer appErr = th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{ IncludeArchivedChannels: true, }) require.Nil(t, appErr) th2 := Setup(t) defer th2.TearDown() i, err := th2.App.BulkImport(th2.Context, &b, nil, false, 5) assert.Nil(t, err) assert.Equal(t, 0, i) channels2, err := th2.App.GetAllChannels(th1.Context, 0, 10, model.ChannelSearchOpts{ IncludeDeleted: true, }) assert.Nil(t, err) found := false for i := range channels2 { if channels2[i].Name == archivedChannel.Name { found = true break } } require.True(t, found, "archived channel not found after import") } func TestExportRoles(t *testing.T) { mainHelper.Parallel(t) t.Run("defaults", func(t *testing.T) { th1 := Setup(t).InitBasic() defer th1.TearDown() var b bytes.Buffer appErr := th1.App.BulkExport(th1.Context, &b, "", nil, model.BulkExportOpts{}) require.Nil(t, appErr) exportedRoles, appErr := th1.App.GetAllRoles() assert.Nil(t, appErr) assert.NotEmpty(t, exportedRoles) th2 := Setup(t) defer th2.TearDown() i, appErr := th2.App.BulkImport(th2.Context, &b, nil, false, 1) assert.Nil(t, appErr) assert.Equal(t, 0, i) importedRoles, appErr := th2.App.GetAllRoles() assert.Nil(t, appErr) assert.NotEmpty(t, importedRoles) require.Equal(t, len(exportedRoles), len(importedRoles)) }) t.Run("modified roles", func(t *testing.T) { th1 := Setup(t).InitBasic() defer th1.TearDown() exportedRole, appErr := th1.App.GetRoleByName(th1.Context, model.TeamUserRoleId) require.Nil(t, appErr) exportedRole.Permissions = exportedRole.Permissions[1:] _, appErr = th1.App.UpdateRole(exportedRole) require.Nil(t, appErr) var b bytes.Buffer appErr = th1.App.BulkExport(th1.Context, &b, "", nil, model.BulkExportOpts{ IncludeRolesAndSchemes: true, }) require.Nil(t, appErr) th2 := Setup(t) defer th2.TearDown() i, appErr := th2.App.BulkImport(th2.Context, &b, nil, false, 1) require.Nil(t, appErr) require.Equal(t, 0, i) importedRole, appErr := th2.App.GetRoleByName(th2.Context, model.TeamUserRoleId) require.Nil(t, appErr) require.Equal(t, exportedRole.DisplayName, importedRole.DisplayName) require.Equal(t, exportedRole.Description, importedRole.Description) require.Equal(t, exportedRole.SchemeManaged, importedRole.SchemeManaged) require.Equal(t, exportedRole.BuiltIn, importedRole.BuiltIn) require.ElementsMatch(t, exportedRole.Permissions, importedRole.Permissions) }) t.Run("custom roles", func(t *testing.T) { th1 := Setup(t).InitBasic() defer th1.TearDown() exportedRoles, appErr := th1.App.GetAllRoles() require.Nil(t, appErr) require.NotEmpty(t, exportedRoles) customRole, appErr := th1.App.CreateRole(&model.Role{ Name: "custom_role", DisplayName: "custom_role", Permissions: exportedRoles[0].Permissions, }) require.Nil(t, appErr) var b bytes.Buffer appErr = th1.App.BulkExport(th1.Context, &b, "", nil, model.BulkExportOpts{ IncludeRolesAndSchemes: true, }) require.Nil(t, appErr) th2 := Setup(t) defer th2.TearDown() i, appErr := th2.App.BulkImport(th2.Context, &b, nil, false, 1) require.Nil(t, appErr) require.Equal(t, 0, i) importedCustomRole, appErr := th2.App.GetRoleByName(th2.Context, customRole.Name) require.Nil(t, appErr) require.Equal(t, customRole.DisplayName, importedCustomRole.DisplayName) require.Equal(t, customRole.Description, importedCustomRole.Description) require.Equal(t, customRole.SchemeManaged, importedCustomRole.SchemeManaged) require.Equal(t, customRole.BuiltIn, importedCustomRole.BuiltIn) require.ElementsMatch(t, customRole.Permissions, importedCustomRole.Permissions) }) } func TestExportSchemes(t *testing.T) { mainHelper.Parallel(t) t.Run("no schemes", func(t *testing.T) { th1 := Setup(t).InitBasic() defer th1.TearDown() // Need to set this or working with schemes won't work until the job is // completed which is unnecessary for the purpose of this test. err := th1.App.Srv().Store().System().Save(&model.System{Name: model.MigrationKeyAdvancedPermissionsPhase2, Value: "true"}) require.NoError(t, err) schemes, err := th1.App.Srv().Store().Scheme().GetAllPage(model.SchemeScopeChannel, 0, 1) require.NoError(t, err) require.Empty(t, schemes) schemes, err = th1.App.Srv().Store().Scheme().GetAllPage(model.SchemeScopeTeam, 0, 1) require.NoError(t, err) require.Empty(t, schemes) var b bytes.Buffer appErr := th1.App.BulkExport(th1.Context, &b, "", nil, model.BulkExportOpts{ IncludeRolesAndSchemes: true, }) require.Nil(t, appErr) // The following causes the original store to be wiped so from here on we are targeting the // second instance where the import will be loaded. th2 := Setup(t) defer th2.TearDown() err = th2.App.Srv().Store().System().Save(&model.System{Name: model.MigrationKeyAdvancedPermissionsPhase2, Value: "true"}) require.NoError(t, err) i, appErr := th2.App.BulkImport(th2.Context, &b, nil, false, 1) require.Nil(t, appErr) require.Equal(t, 0, i) schemes, err = th2.App.Srv().Store().Scheme().GetAllPage(model.SchemeScopeChannel, 0, 1) require.NoError(t, err) require.Empty(t, schemes) schemes, err = th2.App.Srv().Store().Scheme().GetAllPage(model.SchemeScopeTeam, 0, 1) require.NoError(t, err) require.Empty(t, schemes) }) t.Run("skip export", func(t *testing.T) { th1 := Setup(t).InitBasic() defer th1.TearDown() // Need to set this or working with schemes won't work until the job is // completed which is unnecessary for the purpose of this test. err := th1.App.Srv().Store().System().Save(&model.System{Name: model.MigrationKeyAdvancedPermissionsPhase2, Value: "true"}) require.NoError(t, err) customScheme, appErr := th1.App.CreateScheme(&model.Scheme{ Name: "custom_scheme", DisplayName: "Custom Scheme", Scope: model.SchemeScopeChannel, }) require.Nil(t, appErr) var b bytes.Buffer appErr = th1.App.BulkExport(th1.Context, &b, "", nil, model.BulkExportOpts{}) require.Nil(t, appErr) // The following causes the original store to be wiped so from here on we are targeting the // second instance where the import will be loaded. th2 := Setup(t) defer th2.TearDown() err = th2.App.Srv().Store().System().Save(&model.System{Name: model.MigrationKeyAdvancedPermissionsPhase2, Value: "true"}) require.NoError(t, err) i, appErr := th2.App.BulkImport(th2.Context, &b, nil, false, 1) require.Nil(t, appErr) require.Equal(t, 0, i) // Verify the scheme doesn't exist which is the expectation as it wasn't exported. _, appErr = th2.App.GetScheme(customScheme.Name) require.NotNil(t, appErr) }) t.Run("export channel scheme", func(t *testing.T) { th1 := Setup(t).InitBasic() defer th1.TearDown() // Need to set this or working with schemes won't work until the job is // completed which is unnecessary for the purpose of this test. err := th1.App.Srv().Store().System().Save(&model.System{Name: model.MigrationKeyAdvancedPermissionsPhase2, Value: "true"}) require.NoError(t, err) builtInRoles := 23 defaultChannelSchemeRoles := 3 // Verify the roles count is expected prior to scheme creation. roles, appErr := th1.App.GetAllRoles() require.Nil(t, appErr) require.Len(t, roles, builtInRoles) customScheme, appErr := th1.App.CreateScheme(&model.Scheme{ Name: "custom_channel_scheme", DisplayName: "Custom Channel Scheme", Scope: model.SchemeScopeChannel, }) require.Nil(t, appErr) // Verify the roles count is expected after scheme creation. roles, appErr = th1.App.GetAllRoles() require.Nil(t, appErr) require.Len(t, roles, builtInRoles+defaultChannelSchemeRoles) // Fetch the scheme roles for later comparison customChannelAdminRole, appErr := th1.App.GetRoleByName(th1.Context, customScheme.DefaultChannelAdminRole) require.Nil(t, appErr) customChannelUserRole, appErr := th1.App.GetRoleByName(th1.Context, customScheme.DefaultChannelUserRole) require.Nil(t, appErr) customChannelGuestRole, appErr := th1.App.GetRoleByName(th1.Context, customScheme.DefaultChannelGuestRole) require.Nil(t, appErr) var b bytes.Buffer appErr = th1.App.BulkExport(th1.Context, &b, "", nil, model.BulkExportOpts{ IncludeRolesAndSchemes: true, }) require.Nil(t, appErr) // The following causes the original store to be wiped so from here on we are targeting the // second instance where the import will be loaded. th2 := Setup(t) defer th2.TearDown() err = th2.App.Srv().Store().System().Save(&model.System{Name: model.MigrationKeyAdvancedPermissionsPhase2, Value: "true"}) require.NoError(t, err) // Verify roles count before importing is as expected. roles, appErr = th2.App.GetAllRoles() require.Nil(t, appErr) require.Len(t, roles, builtInRoles) i, appErr := th2.App.BulkImport(th2.Context, &b, nil, false, 1) require.Nil(t, appErr) require.Equal(t, 0, i) // Verify roles count after importing is as expected. roles, appErr = th2.App.GetAllRoles() require.Nil(t, appErr) require.Len(t, roles, builtInRoles+defaultChannelSchemeRoles) // Verify schemes match importedScheme, appErr := th2.App.GetSchemeByName(customScheme.Name) require.Nil(t, appErr) require.Equal(t, customScheme.Name, importedScheme.Name) require.Equal(t, customScheme.DisplayName, importedScheme.DisplayName) require.Equal(t, customScheme.Description, importedScheme.Description) require.Equal(t, customScheme.Scope, importedScheme.Scope) // Verify scheme roles match importedChannelAdminRole, appErr := th2.App.GetRoleByName(th2.Context, importedScheme.DefaultChannelAdminRole) require.Nil(t, appErr) require.Equal(t, customChannelAdminRole.DisplayName, importedChannelAdminRole.DisplayName) require.Equal(t, customChannelAdminRole.Description, importedChannelAdminRole.Description) require.Equal(t, customChannelAdminRole.Permissions, importedChannelAdminRole.Permissions) require.Equal(t, customChannelAdminRole.SchemeManaged, importedChannelAdminRole.SchemeManaged) require.Equal(t, customChannelAdminRole.BuiltIn, importedChannelAdminRole.BuiltIn) importedChannelUserRole, appErr := th2.App.GetRoleByName(th2.Context, importedScheme.DefaultChannelUserRole) require.Nil(t, appErr) require.Equal(t, customChannelUserRole.DisplayName, importedChannelUserRole.DisplayName) require.Equal(t, customChannelUserRole.Description, importedChannelUserRole.Description) require.Equal(t, customChannelUserRole.Permissions, importedChannelUserRole.Permissions) require.Equal(t, customChannelUserRole.SchemeManaged, importedChannelUserRole.SchemeManaged) require.Equal(t, customChannelUserRole.BuiltIn, importedChannelUserRole.BuiltIn) importedChannelGuestRole, appErr := th2.App.GetRoleByName(th2.Context, importedScheme.DefaultChannelGuestRole) require.Nil(t, appErr) require.Equal(t, customChannelGuestRole.DisplayName, importedChannelGuestRole.DisplayName) require.Equal(t, customChannelGuestRole.Description, importedChannelGuestRole.Description) require.Equal(t, customChannelGuestRole.Permissions, importedChannelGuestRole.Permissions) require.Equal(t, customChannelGuestRole.SchemeManaged, importedChannelGuestRole.SchemeManaged) require.Equal(t, customChannelGuestRole.BuiltIn, importedChannelGuestRole.BuiltIn) }) t.Run("export team scheme", func(t *testing.T) { th1 := Setup(t).InitBasic() defer th1.TearDown() // Need to set this or working with schemes won't work until the job is // completed which is unnecessary for the purpose of this test. err := th1.App.Srv().Store().System().Save(&model.System{Name: model.MigrationKeyAdvancedPermissionsPhase2, Value: "true"}) require.NoError(t, err) builtInRoles := 23 defaultTeamSchemeRoles := 10 // Verify the roles count is expected prior to scheme creation. roles, appErr := th1.App.GetAllRoles() require.Nil(t, appErr) require.Len(t, roles, builtInRoles) customScheme, appErr := th1.App.CreateScheme(&model.Scheme{ Name: "custom_team_scheme", DisplayName: "Custom Team Scheme", Scope: model.SchemeScopeTeam, }) require.Nil(t, appErr) // Verify the roles count is expected after scheme creation. roles, appErr = th1.App.GetAllRoles() require.Nil(t, appErr) require.Len(t, roles, builtInRoles+defaultTeamSchemeRoles) customChannelAdminRole, appErr := th1.App.GetRoleByName(th1.Context, customScheme.DefaultChannelAdminRole) require.Nil(t, appErr) customChannelUserRole, appErr := th1.App.GetRoleByName(th1.Context, customScheme.DefaultChannelUserRole) require.Nil(t, appErr) customChannelGuestRole, appErr := th1.App.GetRoleByName(th1.Context, customScheme.DefaultChannelGuestRole) require.Nil(t, appErr) customTeamAdminRole, appErr := th1.App.GetRoleByName(th1.Context, customScheme.DefaultTeamAdminRole) require.Nil(t, appErr) customTeamUserRole, appErr := th1.App.GetRoleByName(th1.Context, customScheme.DefaultTeamUserRole) require.Nil(t, appErr) customTeamGuestRole, appErr := th1.App.GetRoleByName(th1.Context, customScheme.DefaultTeamGuestRole) require.Nil(t, appErr) var b bytes.Buffer appErr = th1.App.BulkExport(th1.Context, &b, "", nil, model.BulkExportOpts{ IncludeRolesAndSchemes: true, }) require.Nil(t, appErr) // The following causes the original store to be wiped so from here on we are targeting the // second instance where the import will be loaded. th2 := Setup(t) defer th2.TearDown() err = th2.App.Srv().Store().System().Save(&model.System{Name: model.MigrationKeyAdvancedPermissionsPhase2, Value: "true"}) require.NoError(t, err) // Verify roles count before importing is as expected. roles, appErr = th2.App.GetAllRoles() require.Nil(t, appErr) require.Len(t, roles, builtInRoles) i, appErr := th2.App.BulkImport(th2.Context, &b, nil, false, 1) require.Nil(t, appErr) require.Equal(t, 0, i) // Verify roles count after importing is as expected. roles, appErr = th2.App.GetAllRoles() require.Nil(t, appErr) require.Len(t, roles, builtInRoles+defaultTeamSchemeRoles) // Verify schemes match importedScheme, appErr := th2.App.GetSchemeByName(customScheme.Name) require.Nil(t, appErr) require.Equal(t, customScheme.Name, importedScheme.Name) require.Equal(t, customScheme.DisplayName, importedScheme.DisplayName) require.Equal(t, customScheme.Description, importedScheme.Description) require.Equal(t, customScheme.Scope, importedScheme.Scope) // Verify scheme roles match importedChannelAdminRole, appErr := th2.App.GetRoleByName(th2.Context, importedScheme.DefaultChannelAdminRole) require.Nil(t, appErr) require.Equal(t, customChannelAdminRole.DisplayName, importedChannelAdminRole.DisplayName) require.Equal(t, customChannelAdminRole.Description, importedChannelAdminRole.Description) require.Equal(t, customChannelAdminRole.Permissions, importedChannelAdminRole.Permissions) require.Equal(t, customChannelAdminRole.SchemeManaged, importedChannelAdminRole.SchemeManaged) require.Equal(t, customChannelAdminRole.BuiltIn, importedChannelAdminRole.BuiltIn) importedChannelUserRole, appErr := th2.App.GetRoleByName(th2.Context, importedScheme.DefaultChannelUserRole) require.Nil(t, appErr) require.Equal(t, customChannelUserRole.DisplayName, importedChannelUserRole.DisplayName) require.Equal(t, customChannelUserRole.Description, importedChannelUserRole.Description) require.Equal(t, customChannelUserRole.Permissions, importedChannelUserRole.Permissions) require.Equal(t, customChannelUserRole.SchemeManaged, importedChannelUserRole.SchemeManaged) require.Equal(t, customChannelUserRole.BuiltIn, importedChannelUserRole.BuiltIn) importedChannelGuestRole, appErr := th2.App.GetRoleByName(th2.Context, importedScheme.DefaultChannelGuestRole) require.Nil(t, appErr) require.Equal(t, customChannelGuestRole.DisplayName, importedChannelGuestRole.DisplayName) require.Equal(t, customChannelGuestRole.Description, importedChannelGuestRole.Description) require.Equal(t, customChannelGuestRole.Permissions, importedChannelGuestRole.Permissions) require.Equal(t, customChannelGuestRole.SchemeManaged, importedChannelGuestRole.SchemeManaged) require.Equal(t, customChannelGuestRole.BuiltIn, importedChannelGuestRole.BuiltIn) importedTeamAdminRole, appErr := th2.App.GetRoleByName(th2.Context, importedScheme.DefaultTeamAdminRole) require.Nil(t, appErr) require.Equal(t, customTeamAdminRole.DisplayName, importedTeamAdminRole.DisplayName) require.Equal(t, customTeamAdminRole.Description, importedTeamAdminRole.Description) require.Equal(t, customTeamAdminRole.Permissions, importedTeamAdminRole.Permissions) require.Equal(t, customTeamAdminRole.SchemeManaged, importedTeamAdminRole.SchemeManaged) require.Equal(t, customTeamAdminRole.BuiltIn, importedTeamAdminRole.BuiltIn) importedTeamUserRole, appErr := th2.App.GetRoleByName(th2.Context, importedScheme.DefaultTeamUserRole) require.Nil(t, appErr) require.Equal(t, customTeamUserRole.DisplayName, importedTeamUserRole.DisplayName) require.Equal(t, customTeamUserRole.Description, importedTeamUserRole.Description) require.Equal(t, customTeamUserRole.Permissions, importedTeamUserRole.Permissions) require.Equal(t, customTeamUserRole.SchemeManaged, importedTeamUserRole.SchemeManaged) require.Equal(t, customTeamUserRole.BuiltIn, importedTeamUserRole.BuiltIn) importedTeamGuestRole, appErr := th2.App.GetRoleByName(th2.Context, importedScheme.DefaultTeamGuestRole) require.Nil(t, appErr) require.Equal(t, customTeamGuestRole.DisplayName, importedTeamGuestRole.DisplayName) require.Equal(t, customTeamGuestRole.Description, importedTeamGuestRole.Description) require.Equal(t, customTeamGuestRole.Permissions, importedTeamGuestRole.Permissions) require.Equal(t, customTeamGuestRole.SchemeManaged, importedTeamGuestRole.SchemeManaged) require.Equal(t, customTeamGuestRole.BuiltIn, importedTeamGuestRole.BuiltIn) }) } // TestExportDeactivatedUserDMs specifically tests the MM-43598 bug // by validating that direct messages from deactivated users are exported correctly func TestExportDeactivatedUserDMs(t *testing.T) { th1 := Setup(t).InitBasic() defer th1.TearDown() // Create a DM Channel user2 := th1.BasicUser2 dmChannel := th1.CreateDmChannel(user2) // 1. First basic user (active) sends a message to user2 (who will later be deactivated) initialMessage := "initial_message_from_basic_user" initialPost := &model.Post{ ChannelId: dmChannel.Id, Message: initialMessage, UserId: th1.BasicUser.Id, } initialPostCreated, appErr := th1.App.CreatePost(th1.Context, initialPost, dmChannel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, appErr) // 2. Have user2 reply with TWO types of replies: // 2a. User2 replies in a thread (to the initial message) threadedReplyMessage := "threaded_reply_from_user2" threadedReply := &model.Post{ ChannelId: dmChannel.Id, Message: threadedReplyMessage, UserId: user2.Id, RootId: initialPostCreated.Id, // This makes it a threaded reply } _, appErr = th1.App.CreatePost(th1.Context, threadedReply, dmChannel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, appErr) // 2b. User2 sends a standalone reply (NOT in a thread) nonThreadedReplyMessage := "non_threaded_reply_from_user2" nonThreadedReply := &model.Post{ ChannelId: dmChannel.Id, Message: nonThreadedReplyMessage, UserId: user2.Id, // No RootId, making it a standalone message, not a thread reply } _, appErr = th1.App.CreatePost(th1.Context, nonThreadedReply, dmChannel, model.CreatePostFlags{SetOnline: true}) require.Nil(t, appErr) // 3. Now deactivate user2 _, err := th1.App.UpdateActive(th1.Context, user2, false) require.Nil(t, err) // 4. Export data var b bytes.Buffer appErr = th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{}) require.Nil(t, appErr) // 5. Make a copy of the buffer for export validation var exportDataCopy bytes.Buffer _, nErr := exportDataCopy.Write(b.Bytes()) require.NoError(t, nErr) // 6. Validate export data directly to ensure both types of replies are present scanner := bufio.NewScanner(&exportDataCopy) foundThreadedReply := false foundNonThreadedReply := false for scanner.Scan() { var line imports.LineImportData err := json.Unmarshal(scanner.Bytes(), &line) if err != nil { continue } // Check for direct posts if line.Type == "direct_post" && line.DirectPost != nil { // Check for the non-threaded reply (the standalone message) if line.DirectPost.Message != nil && *line.DirectPost.Message == nonThreadedReplyMessage { foundNonThreadedReply = true // Verify username is correctly preserved in export require.Equal(t, user2.Username, *line.DirectPost.User, "Deactivated user's username should be preserved in export for non-threaded reply") } // Check for the thread starter and its replies if line.DirectPost.Message != nil && *line.DirectPost.Message == initialMessage { // Check if the threaded reply is in the replies array if line.DirectPost.Replies != nil { for _, reply := range *line.DirectPost.Replies { if reply.Message != nil && *reply.Message == threadedReplyMessage { foundThreadedReply = true require.Equal(t, user2.Username, *reply.User, "Deactivated user's username should be preserved in export for threaded reply") } } } } } } // This is key for testing MM-43598 require.True(t, foundNonThreadedReply, "Non-threaded reply from deactivated user must be present in export data") require.True(t, foundThreadedReply, "Threaded reply from deactivated user must be present in export data") // 7. Import data into a new instance th2 := Setup(t) defer th2.TearDown() i, appErr := th2.App.BulkImport(th2.Context, &b, nil, false, 5) require.Nil(t, appErr) require.Equal(t, 0, i) // 8. Verify the DM channel was imported channels, nErr := th2.App.Srv().Store().Channel().GetAllDirectChannelsForExportAfter(1000, "00000000", false) require.NoError(t, nErr) require.Equal(t, 1, len(channels), "Direct channel should be imported") // 9. Verify all posts were imported posts, nErr := th2.App.Srv().Store().Post().GetPosts(th2.Context, model.GetPostsOptions{ ChannelId: channels[0].Id, PerPage: 1000, }, false, nil) require.NoError(t, nErr) // We should have exactly 3 posts require.Equal(t, 3, len(posts.Posts), "Should have imported exactly 3 posts") // 10. Specifically check that both types of replies are present foundThreadedReplyInImport := false foundNonThreadedReplyInImport := false for _, post := range posts.Posts { if post.Message == threadedReplyMessage { foundThreadedReplyInImport = true // Verify this is a reply in a thread require.NotEmpty(t, post.RootId, "Threaded reply should have a RootId") } if post.Message == nonThreadedReplyMessage { foundNonThreadedReplyInImport = true // Verify this is NOT a reply in a thread require.Empty(t, post.RootId, "Non-threaded reply should not have a RootId") } } // This directly tests the issue in MM-43598 require.True(t, foundNonThreadedReplyInImport, "Non-threaded reply from deactivated user should be imported") require.True(t, foundThreadedReplyInImport, "Threaded reply from deactivated user should be imported") }