Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1776 lines
59 KiB
Go
1776 lines
59 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()
|
|
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")
|
|
}
|