Files
team/server/channels/app/export_test.go
Doug LauderandGitHub 2e66a5f3ce MM-68258 Remove system_secure_connection_manager role (#36009)
* Remove system_secure_connection_manager role

  The dedicated role for delegating secure connection management is no
  longer needed. The manage_secure_connections permission remains and
  continues to be granted to system admins via AllPermissions.

  Removes the role definition, migration, permissions migration, UI
  components, i18n strings, and all associated tests across server,
  webapp, and e2e-tests.
2026-04-10 10:49:04 -04:00

1731 lines
58 KiB
Go

// 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(t)
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)
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(t)
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)
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)
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)
// Adding a user and deactivating it to check whether it gets included in bulk export
user := th1.CreateUser(t)
_, 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)
}
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)
u := th1.CreateUser(t)
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)
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(t)
// DM Channel
ch := th1.CreateDmChannel(t, 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)
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})
// Verify the users were imported and get their IDs in th2
_, appErr = th2.App.GetUserByUsername(th1.BasicUser.Username)
require.Nil(t, appErr)
importedUser2, appErr := th2.App.GetUserByUsername(th1.BasicUser2.Username)
require.Nil(t, appErr)
// Ensure the favorited channel was retained for the imported user
fav, nErr := th2.App.Srv().Store().Preference().Get(importedUser2.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(t)
// DM Channel
th1.CreateDmChannel(t, 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(t)
// 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(t)
// Create a DM Channel with another user
dmc1 := th1.CreateDmChannel(t, th1.BasicUser2)
th1.CreatePost(t, dmc1)
// Create a DM Channel with self
dmc2 := th1.CreateDmChannel(t, th1.BasicUser)
th1.CreatePost(t, 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(t)
// 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(t)
// DM Channel with self (me channel)
th1.CreateDmChannel(t, 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)
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(t)
user1 := th1.CreateUser(t)
th1.LinkUserToTeam(t, user1, th1.BasicTeam)
user2 := th1.CreateUser(t)
th1.LinkUserToTeam(t, user2, th1.BasicTeam)
// GM Channel
th1.CreateGroupChannel(t, 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))
th2 := Setup(t)
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(t)
// DM Channel
th1.CreateDmChannel(t, th1.BasicUser2)
user1 := th1.CreateUser(t)
th1.LinkUserToTeam(t, user1, th1.BasicTeam)
user2 := th1.CreateUser(t)
th1.LinkUserToTeam(t, user2, th1.BasicTeam)
// GM Channel
th1.CreateGroupChannel(t, 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))
th2 := Setup(t)
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(t)
// DM Channel
dmChannel := th1.CreateDmChannel(t, th1.BasicUser2)
dmMembers := []string{th1.BasicUser.Username, th1.BasicUser2.Username}
user1 := th1.CreateUser(t)
th1.LinkUserToTeam(t, user1, th1.BasicTeam)
user2 := th1.CreateUser(t)
th1.LinkUserToTeam(t, user2, th1.BasicTeam)
// GM Channel
gmChannel := th1.CreateGroupChannel(t, 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)
th2 := Setup(t)
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(t)
attachments := []*model.MessageAttachment{{Footer: "footer"}}
// DM Channel
dmChannel := th1.CreateDmChannel(t, th1.BasicUser2)
dmMembers := []string{th1.BasicUser.Username, th1.BasicUser2.Username}
user1 := th1.CreateUser(t)
th1.LinkUserToTeam(t, user1, th1.BasicTeam)
user2 := th1.CreateUser(t)
th1.LinkUserToTeam(t, user2, th1.BasicTeam)
// GM Channel
gmChannel := th1.CreateGroupChannel(t, 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)
th2 := Setup(t)
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(t)
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)
th2 := Setup(t)
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(t)
// DM Channel with self (me channel)
dmChannel := th1.CreateDmChannel(t, th1.BasicUser)
th1.CreatePost(t, 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))
th2 := Setup(t)
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(t)
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(t, th1.BasicChannel)
_ = th1.CreatePostReply(t, 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(t, th1.BasicUser2)
thread := th1.CreatePost(t, dmc)
_ = th1.CreatePostReply(t, 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)
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 = Setup(t)
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(t)
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(t)
team1 := th1.CreateTeam(t)
channel1 := th1.CreateChannel(t, team1)
th1.CreatePost(t, 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)
}
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(t)
archivedChannel := th1.CreateChannel(t, th1.BasicTeam)
th1.CreatePost(t, 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)
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(t)
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)
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(t)
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)
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(t)
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)
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(t)
// 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)
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(t)
// 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)
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(t)
// 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 := 24
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)
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(t)
// 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 := 24
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)
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(t)
// Create a DM Channel
user2 := th1.BasicUser2
dmChannel := th1.CreateDmChannel(t, 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)
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")
}