diff --git a/app/export.go b/app/export.go index bf734ffc2b..3b7f827e5e 100644 --- a/app/export.go +++ b/app/export.go @@ -11,6 +11,7 @@ import ( "path/filepath" "strings" + "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" "github.com/pkg/errors" ) @@ -70,10 +71,19 @@ func (a *App) BulkExport(writer io.Writer, file string, pathToEmojiDir string, d if err := a.ExportAllPosts(writer); err != nil { return err } + if err := a.ExportCustomEmoji(writer, file, pathToEmojiDir, dirNameToExportEmoji); err != nil { return err } + if err := a.ExportAllDirectChannels(writer); err != nil { + return err + } + + if err := a.ExportAllDirectPosts(writer); err != nil { + return err + } + return nil } @@ -503,3 +513,83 @@ func (a *App) copyEmojiImages(emojiId string, emojiImagePath string, pathToDir s return nil } + +func (a *App) ExportAllDirectChannels(writer io.Writer) *model.AppError { + afterId := strings.Repeat("0", 26) + for { + result := <-a.Srv.Store.Channel().GetAllDirectChannelsForExportAfter(1000, afterId) + if result.Err != nil { + return result.Err + } + + channels := result.Data.([]*model.DirectChannelForExport) + if len(channels) == 0 { + break + } + + for _, channel := range channels { + afterId = channel.Id + + // Skip deleted. + if channel.DeleteAt != 0 { + continue + } + + // There's no import support for single member channels yet. + if len(*channel.Members) == 1 { + mlog.Debug("Bulk export for direct channels containing a single member is not supported.") + continue + } + + channelLine := ImportLineFromDirectChannel(channel) + if err := a.ExportWriteLine(writer, channelLine); err != nil { + return err + } + } + } + + return nil +} + +func (a *App) ExportAllDirectPosts(writer io.Writer) *model.AppError { + afterId := strings.Repeat("0", 26) + for { + result := <-a.Srv.Store.Post().GetDirectPostParentsForExportAfter(1000, afterId) + if result.Err != nil { + return result.Err + } + + posts := result.Data.([]*model.DirectPostForExport) + if len(posts) == 0 { + break + } + + for _, post := range posts { + afterId = post.Id + + // Skip deleted. + if post.DeleteAt != 0 { + continue + } + + // There's no import support for single member channels yet. + if len(*post.ChannelMembers) == 1 { + mlog.Debug("Bulk export for posts containing a single member is not supported.") + continue + } + + // Do the Replies. + replies, err := a.buildPostReplies(post.Id) + if err != nil { + return err + } + + postLine := ImportLineForDirectPost(post) + postLine.DirectPost.Replies = replies + if err := a.ExportWriteLine(writer, postLine); err != nil { + return err + } + } + } + return nil +} diff --git a/app/export_converters.go b/app/export_converters.go index 0a57636691..a29fde2e81 100644 --- a/app/export_converters.go +++ b/app/export_converters.go @@ -38,6 +38,16 @@ func ImportLineFromChannel(channel *model.ChannelForExport) *LineImportData { } } +func ImportLineFromDirectChannel(channel *model.DirectChannelForExport) *LineImportData { + return &LineImportData{ + Type: "direct_channel", + DirectChannel: &DirectChannelImportData{ + Header: &channel.Header, + Members: channel.Members, + }, + } +} + func ImportLineFromUser(user *model.User, exportedPrefs map[string]*string) *LineImportData { // Bulk Importer doesn't accept "empty string" for AuthService. var authService *string @@ -141,6 +151,18 @@ func ImportLineForPost(post *model.PostForExport) *LineImportData { } } +func ImportLineForDirectPost(post *model.DirectPostForExport) *LineImportData { + return &LineImportData{ + Type: "direct_post", + DirectPost: &DirectPostImportData{ + ChannelMembers: post.ChannelMembers, + User: &post.User, + Message: &post.Message, + CreateAt: &post.CreateAt, + }, + } +} + func ImportReplyFromPost(post *model.ReplyForExport) *ReplyImportData { return &ReplyImportData{ User: &post.Username, diff --git a/app/export_test.go b/app/export_test.go index 6f852e7c89..420807de6b 100644 --- a/app/export_test.go +++ b/app/export_test.go @@ -3,6 +3,7 @@ package app import ( "bytes" "os" + "sort" "testing" "github.com/stretchr/testify/assert" @@ -201,16 +202,252 @@ func TestExportAllUsers(t *testing.T) { // Checking whether deactivated users were included in bulk export deletedUsers1, err := th1.App.GetUsers(&model.UserGetOptions{ Inactive: true, - Page: 0, - PerPage: 10, + Page: 0, + PerPage: 10, }) assert.Nil(t, err) deletedUsers2, err := th1.App.GetUsers(&model.UserGetOptions{ Inactive: true, - Page: 0, - PerPage: 10, + Page: 0, + PerPage: 10, }) assert.Nil(t, err) assert.Equal(t, len(deletedUsers1), len(deletedUsers2)) assert.ElementsMatch(t, deletedUsers1, deletedUsers2) } + +func TestExportDMChannel(t *testing.T) { + th1 := Setup(t).InitBasic() + + // DM Channel + th1.CreateDmChannel(th1.BasicUser2) + + var b bytes.Buffer + err := th1.App.BulkExport(&b, "somefile", "somePath", "someDir") + require.Nil(t, err) + + result := <-th1.App.Srv.Store.Channel().GetAllDirectChannelsForExportAfter(1000, "00000000") + channels := result.Data.([]*model.DirectChannelForExport) + assert.Equal(t, 1, len(channels)) + + th1.TearDown() + + th2 := Setup(t) + defer th2.TearDown() + + result = <-th2.App.Srv.Store.Channel().GetAllDirectChannelsForExportAfter(1000, "00000000") + channels = result.Data.([]*model.DirectChannelForExport) + assert.Equal(t, 0, len(channels)) + + // import the exported channel + err, i := th2.App.BulkImport(&b, false, 5) + assert.Nil(t, err) + assert.Equal(t, 0, i) + + // Ensure the Members of the imported DM channel is the same was from the exported + result = <-th2.App.Srv.Store.Channel().GetAllDirectChannelsForExportAfter(1000, "00000000") + channels = result.Data.([]*model.DirectChannelForExport) + assert.Equal(t, 1, len(channels)) + assert.ElementsMatch(t, []string{th1.BasicUser.Username, th1.BasicUser2.Username}, *channels[0].Members) +} + +func TestExportDMChannelToSelf(t *testing.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(&b, "somefile", "somePath", "someDir") + require.Nil(t, err) + + result := <-th1.App.Srv.Store.Channel().GetAllDirectChannelsForExportAfter(1000, "00000000") + channels := result.Data.([]*model.DirectChannelForExport) + assert.Equal(t, 1, len(channels)) + + th2 := Setup(t) + defer th2.TearDown() + + result = <-th2.App.Srv.Store.Channel().GetAllDirectChannelsForExportAfter(1000, "00000000") + channels = result.Data.([]*model.DirectChannelForExport) + assert.Equal(t, 0, len(channels)) + + // import the exported channel + err, i := th2.App.BulkImport(&b, false, 5) + assert.Nil(t, err) + assert.Equal(t, 0, i) + + // Ensure no channels were imported + result = <-th2.App.Srv.Store.Channel().GetAllDirectChannelsForExportAfter(1000, "00000000") + channels = result.Data.([]*model.DirectChannelForExport) + assert.Equal(t, 0, len(channels)) +} + +func TestExportGMChannel(t *testing.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(user1, user2) + + var b bytes.Buffer + err := th1.App.BulkExport(&b, "somefile", "somePath", "someDir") + require.Nil(t, err) + + result := <-th1.App.Srv.Store.Channel().GetAllDirectChannelsForExportAfter(1000, "00000000") + channels := result.Data.([]*model.DirectChannelForExport) + assert.Equal(t, 1, len(channels)) + + th1.TearDown() + + th2 := Setup(t) + defer th2.TearDown() + + result = <-th2.App.Srv.Store.Channel().GetAllDirectChannelsForExportAfter(1000, "00000000") + channels = result.Data.([]*model.DirectChannelForExport) + assert.Equal(t, 0, len(channels)) +} + +func TestExportGMandDMChannels(t *testing.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(user1, user2) + + var b bytes.Buffer + err := th1.App.BulkExport(&b, "somefile", "somePath", "someDir") + require.Nil(t, err) + + result := <-th1.App.Srv.Store.Channel().GetAllDirectChannelsForExportAfter(1000, "00000000") + channels := result.Data.([]*model.DirectChannelForExport) + assert.Equal(t, 2, len(channels)) + + th1.TearDown() + + th2 := Setup(t) + defer th2.TearDown() + + result = <-th2.App.Srv.Store.Channel().GetAllDirectChannelsForExportAfter(1000, "00000000") + channels = result.Data.([]*model.DirectChannelForExport) + assert.Equal(t, 0, len(channels)) + + // import the exported channel + err, i := th2.App.BulkImport(&b, 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 + result = <-th2.App.Srv.Store.Channel().GetAllDirectChannelsForExportAfter(1000, "00000000") + channels = result.Data.([]*model.DirectChannelForExport) + + // Adding some deteminism so its possible to assert on slice index + sort.Slice(channels, func(i, j int) bool { return channels[i].CreateAt > channels[j].CreateAt }) + assert.Equal(t, 2, len(channels)) + assert.ElementsMatch(t, []string{th1.BasicUser.Username, user1.Username, user2.Username}, *channels[0].Members) + assert.ElementsMatch(t, []string{th1.BasicUser.Username, th1.BasicUser2.Username}, *channels[1].Members) +} + +func TestExportDMandGMPost(t *testing.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(user1, user2) + gmMembers := []string{th1.BasicUser.Username, user1.Username, user2.Username} + + // DM posts + th1.CreatePost(dmChannel) + th1.CreatePost(dmChannel) + // GM posts + th1.CreatePost(gmChannel) + th1.CreatePost(gmChannel) + + result := <-th1.App.Srv.Store.Post().GetDirectPostParentsForExportAfter(1000, "0000000") + posts := result.Data.([]*model.DirectPostForExport) + assert.Equal(t, 4, len(posts)) + + var b bytes.Buffer + err := th1.App.BulkExport(&b, "somefile", "somePath", "someDir") + require.Nil(t, err) + + th1.TearDown() + + th2 := Setup(t) + defer th2.TearDown() + + result = <-th2.App.Srv.Store.Post().GetDirectPostParentsForExportAfter(1000, "0000000") + posts = result.Data.([]*model.DirectPostForExport) + assert.Equal(t, 0, len(posts)) + + // import the exported posts + err, i := th2.App.BulkImport(&b, false, 5) + assert.Nil(t, err) + assert.Equal(t, 0, i) + + result = <-th2.App.Srv.Store.Post().GetDirectPostParentsForExportAfter(1000, "0000000") + posts = result.Data.([]*model.DirectPostForExport) + + // Adding some deteminism so its possible to assert on slice index + sort.Slice(posts, func(i, j int) bool { return posts[i].CreateAt > posts[j].CreateAt }) + 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 TestExportDMPostWithSelf(t *testing.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(&b, "somefile", "somePath", "someDir") + require.Nil(t, err) + + result := <-th1.App.Srv.Store.Post().GetDirectPostParentsForExportAfter(1000, "0000000") + posts := result.Data.([]*model.DirectPostForExport) + assert.Equal(t, 1, len(posts)) + + th1.TearDown() + + th2 := Setup(t) + defer th2.TearDown() + + result = <-th2.App.Srv.Store.Post().GetDirectPostParentsForExportAfter(1000, "0000000") + posts = result.Data.([]*model.DirectPostForExport) + assert.Equal(t, 0, len(posts)) + + // import the exported posts + err, i := th2.App.BulkImport(&b, false, 5) + assert.Nil(t, err) + assert.Equal(t, 0, i) + + result = <-th2.App.Srv.Store.Post().GetDirectPostParentsForExportAfter(1000, "0000000") + posts = result.Data.([]*model.DirectPostForExport) + assert.Equal(t, 0, len(posts)) +} diff --git a/model/channel.go b/model/channel.go index 16e48743cd..7dafc7a909 100644 --- a/model/channel.go +++ b/model/channel.go @@ -73,6 +73,11 @@ type ChannelForExport struct { SchemeName *string } +type DirectChannelForExport struct { + Channel + Members *[]string +} + func (o *Channel) DeepCopy() *Channel { copy := *o if copy.SchemeId != nil { diff --git a/model/channel_member.go b/model/channel_member.go index 753e0eb552..f2c49efac1 100644 --- a/model/channel_member.go +++ b/model/channel_member.go @@ -50,6 +50,7 @@ type ChannelMembers []ChannelMember type ChannelMemberForExport struct { ChannelMember ChannelName string + Username string } func (o *ChannelMembers) ToJson() string { diff --git a/model/post.go b/model/post.go index dd78079436..09100563e1 100644 --- a/model/post.go +++ b/model/post.go @@ -122,6 +122,12 @@ type PostForExport struct { ReplyCount int } +type DirectPostForExport struct { + Post + User string + ChannelMembers *[]string +} + type ReplyForExport struct { Post Username string diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index 518328c8b3..b198041c5a 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -14,6 +14,7 @@ import ( "github.com/mattermost/gorp" "github.com/pkg/errors" + sq "github.com/Masterminds/squirrel" "github.com/mattermost/mattermost-server/einterfaces" "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" @@ -2502,3 +2503,66 @@ func (s SqlChannelStore) GetChannelMembersForExport(userId string, teamId string result.Data = members }) } + +func (s SqlChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterId string) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + var directChannelsForExport []*model.DirectChannelForExport + query := s.getQueryBuilder(). + Select("Channels.*"). + From("Channels"). + Where(sq.And{ + sq.Gt{"Channels.Id": afterId}, + sq.Eq{"Channels.DeleteAt": int(0)}, + sq.Eq{"Channels.Type": []string{"D", "G"}}, + }). + OrderBy("Channels.Id"). + Limit(uint64(limit)) + + queryString, args, err := query.ToSql() + if err != nil { + result.Err = model.NewAppError("SqlTeamStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + if _, err = s.GetReplica().Select(&directChannelsForExport, queryString, args...); err != nil { + result.Err = model.NewAppError("SqlTeamStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + var channelIds []string + for _, channel := range directChannelsForExport { + channelIds = append(channelIds, channel.Id) + } + query = s.getQueryBuilder(). + Select("*"). + From("ChannelMembers cm"). + Join("Users u ON ( u.Id = cm.UserId )"). + Where(sq.And{ + sq.Eq{"cm.ChannelId": channelIds}, + sq.Eq{"u.DeleteAt": int(0)}, + }) + + queryString, args, err = query.ToSql() + if err != nil { + result.Err = model.NewAppError("SqlTeamStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + var channelMembers []*model.ChannelMemberForExport + if _, err := s.GetReplica().Select(&channelMembers, queryString, args...); err != nil { + result.Err = model.NewAppError("SqlTeamStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + // Populate each channel with its members + dmChannelsMap := make(map[string]*model.DirectChannelForExport) + for _, channel := range directChannelsForExport { + channel.Members = &[]string{} + dmChannelsMap[channel.Id] = channel + } + for _, member := range channelMembers { + members := dmChannelsMap[member.ChannelId].Members + *members = append(*members, member.Username) + } + result.Data = directChannelsForExport + }) +} diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index f9bfd35a4a..d4a6bd42ab 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -11,6 +11,7 @@ import ( "strings" "sync" + sq "github.com/Masterminds/squirrel" "github.com/mattermost/mattermost-server/einterfaces" "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" @@ -1366,3 +1367,77 @@ func (s *SqlPostStore) GetRepliesForExport(parentId string) store.StoreChannel { } }) } + +func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId string) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + query := s.getQueryBuilder(). + Select("p.*", "Users.Username as User"). + From("Posts p"). + Join("Channels ON p.ChannelId = Channels.Id"). + Join("Users ON p.UserId = Users.Id"). + Where(sq.And{ + sq.Gt{"p.Id": afterId}, + sq.Eq{"p.ParentId": string("")}, + sq.Eq{"p.DeleteAt": int(0)}, + sq.Eq{"Channels.DeleteAt": int(0)}, + sq.Eq{"Users.DeleteAt": int(0)}, + sq.Eq{"Channels.Type": []string{"D", "G"}}, + }). + OrderBy("p.Id"). + Limit(uint64(limit)) + + queryString, args, err := query.ToSql() + if err != nil { + result.Err = model.NewAppError("SqlPostStore.GetDirectPostParentsForExportAfter", "store.sql_post.get_direct_posts.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + var posts []*model.DirectPostForExport + if _, err = s.GetReplica().Select(&posts, queryString, args...); err != nil { + result.Err = model.NewAppError("SqlPostStore.GetDirectPostParentsForExportAfter", "store.sql_post.get_direct_posts.app_error", nil, err.Error(), http.StatusInternalServerError) + } + var channelIds []string + for _, post := range posts { + channelIds = append(channelIds, post.ChannelId) + } + query = s.getQueryBuilder(). + Select("*"). + From("ChannelMembers cm"). + Join("Users u ON ( u.Id = cm.UserId )"). + Where(sq.Eq{ + "cm.ChannelId": channelIds, + }) + + queryString, args, err = query.ToSql() + if err != nil { + result.Err = model.NewAppError("SqlPostStore.GetDirectPostParentsForExportAfter", "store.sql_post.get_direct_posts.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + var channelMembers []*model.ChannelMemberForExport + if _, err := s.GetReplica().Select(&channelMembers, queryString, args...); err != nil { + result.Err = model.NewAppError("SqlPostStore.GetDirectPostParentsForExportAfter", "store.sql_post.get_direct_posts.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + // Build a map of channels and their posts + postsChannelMap := make(map[string][]*model.DirectPostForExport) + for _, post := range posts { + post.ChannelMembers = &[]string{} + postsChannelMap[post.ChannelId] = append(postsChannelMap[post.ChannelId], post) + } + + // Build a map of channels and their members + channelMembersMap := make(map[string][]string) + for _, member := range channelMembers { + channelMembersMap[member.ChannelId] = append(channelMembersMap[member.ChannelId], member.Username) + } + + // Populate each post ChannelMembers extracting it from the channelMembersMap + for channelId := range channelMembersMap { + for _, post := range postsChannelMap[channelId] { + *post.ChannelMembers = channelMembersMap[channelId] + } + } + result.Data = posts + }) +} diff --git a/store/sqlstore/post_store_test.go b/store/sqlstore/post_store_test.go index 9c23136081..73c5e45c41 100644 --- a/store/sqlstore/post_store_test.go +++ b/store/sqlstore/post_store_test.go @@ -10,5 +10,5 @@ import ( ) func TestPostStore(t *testing.T) { - StoreTest(t, storetest.TestPostStore) + StoreTestWithSqlSupplier(t, storetest.TestPostStore) } diff --git a/store/sqlstore/store.go b/store/sqlstore/store.go index 7c03e75f43..76b38627fb 100644 --- a/store/sqlstore/store.go +++ b/store/sqlstore/store.go @@ -4,10 +4,10 @@ package sqlstore import ( + sq "github.com/Masterminds/squirrel" _ "github.com/go-sql-driver/mysql" _ "github.com/lib/pq" "github.com/mattermost/gorp" - "github.com/mattermost/mattermost-server/store" ) @@ -98,4 +98,5 @@ type SqlStore interface { TermsOfService() store.TermsOfServiceStore UserTermsOfService() store.UserTermsOfServiceStore LinkMetadata() store.LinkMetadataStore + getQueryBuilder() sq.StatementBuilderType } diff --git a/store/sqlstore/supplier.go b/store/sqlstore/supplier.go index 4a35274460..9f93a5e807 100644 --- a/store/sqlstore/supplier.go +++ b/store/sqlstore/supplier.go @@ -15,6 +15,7 @@ import ( "sync/atomic" "time" + sq "github.com/Masterminds/squirrel" "github.com/dyatlov/go-opengraph/opengraph" "github.com/go-sql-driver/mysql" "github.com/lib/pq" @@ -1051,6 +1052,14 @@ func (ss *SqlSupplier) DropAllTables() { ss.master.TruncateTables() } +func (ss *SqlSupplier) getQueryBuilder() sq.StatementBuilderType { + builder := sq.StatementBuilder.PlaceholderFormat(sq.Question) + if ss.DriverName() == model.DATABASE_DRIVER_POSTGRES { + builder = builder.PlaceholderFormat(sq.Dollar) + } + return builder +} + type mattermConverter struct{} func (me mattermConverter) ToDb(val interface{}) (interface{}, error) { diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index 0e1a99e4ed..ecd471fd9f 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -67,15 +67,11 @@ func NewSqlUserStore(sqlStore SqlStore, metrics einterfaces.MetricsInterface) st metrics: metrics, } - us.usersQuery = sq. + us.usersQuery = us.getQueryBuilder(). Select("u.*", "b.UserId IS NOT NULL AS IsBot"). From("Users u"). LeftJoin("Bots b ON ( b.UserId = u.Id )") - if us.DriverName() == model.DATABASE_DRIVER_POSTGRES { - us.usersQuery = us.usersQuery.PlaceholderFormat(sq.Dollar) - } - for _, db := range sqlStore.GetAllConns() { table := db.AddTableWithName(model.User{}, "Users").SetKeys(false, "Id") table.ColMap("Id").SetMaxSize(26) diff --git a/store/store.go b/store/store.go index d24bbacb7b..e4779fc9aa 100644 --- a/store/store.go +++ b/store/store.go @@ -187,6 +187,7 @@ type ChannelStore interface { ClearAllCustomRoleAssignments() StoreChannel MigratePublicChannels() error GetAllChannelsForExportAfter(limit int, afterId string) StoreChannel + GetAllDirectChannelsForExportAfter(limit int, afterId string) StoreChannel GetChannelMembersForExport(userId string, teamId string) StoreChannel RemoveAllDeactivatedMembers(channelId string) StoreChannel } @@ -229,6 +230,7 @@ type PostStore interface { GetMaxPostSize() StoreChannel GetParentsForExportAfter(limit int, afterId string) StoreChannel GetRepliesForExport(parentId string) StoreChannel + GetDirectPostParentsForExportAfter(limit int, afterId string) StoreChannel } type UserStore interface { diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 6de8143ca8..4707c6e25f 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -36,11 +36,11 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlSupplier) { createDefaultRoles(t, ss) t.Run("Save", func(t *testing.T) { testChannelStoreSave(t, ss) }) - t.Run("SaveDirectChannel", func(t *testing.T) { testChannelStoreSaveDirectChannel(t, ss) }) + t.Run("SaveDirectChannel", func(t *testing.T) { testChannelStoreSaveDirectChannel(t, ss, s) }) t.Run("CreateDirectChannel", func(t *testing.T) { testChannelStoreCreateDirectChannel(t, ss) }) t.Run("Update", func(t *testing.T) { testChannelStoreUpdate(t, ss) }) t.Run("GetChannelUnread", func(t *testing.T) { testGetChannelUnread(t, ss) }) - t.Run("Get", func(t *testing.T) { testChannelStoreGet(t, ss) }) + t.Run("Get", func(t *testing.T) { testChannelStoreGet(t, ss, s) }) t.Run("GetForPost", func(t *testing.T) { testChannelStoreGetForPost(t, ss) }) t.Run("Restore", func(t *testing.T) { testChannelStoreRestore(t, ss) }) t.Run("Delete", func(t *testing.T) { testChannelStoreDelete(t, ss) }) @@ -51,7 +51,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("ChannelMemberStore", func(t *testing.T) { testChannelMemberStore(t, ss) }) t.Run("ChannelDeleteMemberStore", func(t *testing.T) { testChannelDeleteMemberStore(t, ss) }) t.Run("GetChannels", func(t *testing.T) { testChannelStoreGetChannels(t, ss) }) - t.Run("GetAllChannels", func(t *testing.T) { testChannelStoreGetAllChannels(t, ss) }) + t.Run("GetAllChannels", func(t *testing.T) { testChannelStoreGetAllChannels(t, ss, s) }) t.Run("GetMoreChannels", func(t *testing.T) { testChannelStoreGetMoreChannels(t, ss) }) t.Run("GetPublicChannelsForTeam", func(t *testing.T) { testChannelStoreGetPublicChannelsForTeam(t, ss) }) t.Run("GetPublicChannelsByIdsForTeam", func(t *testing.T) { testChannelStoreGetPublicChannelsByIdsForTeam(t, ss) }) @@ -67,7 +67,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("SearchMore", func(t *testing.T) { testChannelStoreSearchMore(t, ss) }) t.Run("SearchInTeam", func(t *testing.T) { testChannelStoreSearchInTeam(t, ss) }) t.Run("SearchAllChannels", func(t *testing.T) { testChannelStoreSearchAllChannels(t, ss) }) - t.Run("AutocompleteInTeamForSearch", func(t *testing.T) { testChannelStoreAutocompleteInTeamForSearch(t, ss) }) + t.Run("AutocompleteInTeamForSearch", func(t *testing.T) { testChannelStoreAutocompleteInTeamForSearch(t, ss, s) }) t.Run("GetMembersByIds", func(t *testing.T) { testChannelStoreGetMembersByIds(t, ss) }) t.Run("AnalyticsDeletedTypeCount", func(t *testing.T) { testChannelStoreAnalyticsDeletedTypeCount(t, ss) }) t.Run("GetPinnedPosts", func(t *testing.T) { testChannelStoreGetPinnedPosts(t, ss) }) @@ -80,6 +80,9 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("GetAllChannelsForExportAfter", func(t *testing.T) { testChannelStoreGetAllChannelsForExportAfter(t, ss) }) t.Run("GetChannelMembersForExport", func(t *testing.T) { testChannelStoreGetChannelMembersForExport(t, ss) }) t.Run("RemoveAllDeactivatedMembers", func(t *testing.T) { testChannelStoreRemoveAllDeactivatedMembers(t, ss) }) + t.Run("ExportAllDirectChannels", func(t *testing.T) { testChannelStoreExportAllDirectChannels(t, ss, s) }) + t.Run("ExportAllDirectChannelsExcludePrivateAndPublic", func(t *testing.T) { testChannelStoreExportAllDirectChannelsExcludePrivateAndPublic(t, ss, s) }) + t.Run("ExportAllDirectChannelsDeletedChannel", func(t *testing.T) { testChannelStoreExportAllDirectChannelsDeletedChannel(t, ss, s) }) } func testChannelStoreSave(t *testing.T, ss store.Store) { @@ -112,7 +115,7 @@ func testChannelStoreSave(t *testing.T, ss store.Store) { } } -func testChannelStoreSaveDirectChannel(t *testing.T, ss store.Store) { +func testChannelStoreSaveDirectChannel(t *testing.T, ss store.Store, s SqlSupplier) { teamId := model.NewId() o1 := model.Channel{} @@ -194,6 +197,8 @@ func testChannelStoreSaveDirectChannel(t *testing.T, ss store.Store) { t.Fatal("should have saved just 1 member") } + // Manually truncate Channels table until testlib can handle cleanups + s.GetMaster().Exec("TRUNCATE Channels") } func testChannelStoreCreateDirectChannel(t *testing.T, ss store.Store) { @@ -340,7 +345,7 @@ func testGetChannelUnread(t *testing.T, ss store.Store) { } } -func testChannelStoreGet(t *testing.T, ss store.Store) { +func testChannelStoreGet(t *testing.T, ss store.Store, s SqlSupplier) { o1 := model.Channel{} o1.TeamId = model.NewId() o1.DisplayName = "Name" @@ -423,6 +428,8 @@ func testChannelStoreGet(t *testing.T, ss store.Store) { t.Fatal("too little") } } + // Manually truncate Channels table until testlib can handle cleanups + s.GetMaster().Exec("TRUNCATE Channels") } func testChannelStoreGetForPost(t *testing.T, ss store.Store) { @@ -962,7 +969,7 @@ func testChannelStoreGetChannels(t *testing.T, ss store.Store) { ss.Channel().InvalidateAllChannelMembersForUser(m1.UserId) } -func testChannelStoreGetAllChannels(t *testing.T, ss store.Store) { +func testChannelStoreGetAllChannels(t *testing.T, ss store.Store, s SqlSupplier) { cleanupChannels(t, ss) t1 := model.Team{} @@ -1035,6 +1042,9 @@ func testChannelStoreGetAllChannels(t *testing.T, ss store.Store) { assert.Len(t, *list, 1) assert.Equal(t, (*list)[0].Id, c1.Id) assert.Equal(t, (*list)[0].TeamDisplayName, "Name") + + // Manually truncate Channels table until testlib can handle cleanups + s.GetMaster().Exec("TRUNCATE Channels") } func testChannelStoreGetMoreChannels(t *testing.T, ss store.Store) { @@ -2306,7 +2316,7 @@ func testChannelStoreSearchAllChannels(t *testing.T, ss store.Store) { } } -func testChannelStoreAutocompleteInTeamForSearch(t *testing.T, ss store.Store) { +func testChannelStoreAutocompleteInTeamForSearch(t *testing.T, ss store.Store, s SqlSupplier) { u1 := &model.User{} u1.Email = MakeEmail() u1.Username = "user1" + model.NewId() @@ -2419,6 +2429,9 @@ func testChannelStoreAutocompleteInTeamForSearch(t *testing.T, ss store.Store) { require.Len(t, *channels, 2) }) } + + // Manually truncate Channels table until testlib can handle cleanups + s.GetMaster().Exec("TRUNCATE Channels") } func testChannelStoreGetMembersByIds(t *testing.T, ss store.Store) { @@ -3158,3 +3171,160 @@ func testChannelStoreRemoveAllDeactivatedMembers(t *testing.T, ss store.Store) { assert.Len(t, *d2, 1) assert.Equal(t, (*d2)[0].UserId, u3.Id) } + +func testChannelStoreExportAllDirectChannels(t *testing.T, ss store.Store, s SqlSupplier) { + teamId := model.NewId() + + o1 := model.Channel{} + o1.TeamId = teamId + o1.DisplayName = "Name" + model.NewId() + o1.Name = "zz" + model.NewId() + "b" + o1.Type = model.CHANNEL_DIRECT + + userIds := []string{model.NewId(), model.NewId(), model.NewId()} + + o2 := model.Channel{} + o2.Name = model.GetGroupNameFromUserIds(userIds) + o2.DisplayName = "GroupChannel" + model.NewId() + o2.Name = "zz" + model.NewId() + "b" + o2.Type = model.CHANNEL_GROUP + store.Must(ss.Channel().Save(&o2, -1)) + + u1 := &model.User{} + u1.Email = MakeEmail() + u1.Nickname = model.NewId() + store.Must(ss.User().Save(u1)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)) + + u2 := &model.User{} + u2.Email = MakeEmail() + u2.Nickname = model.NewId() + store.Must(ss.User().Save(u2)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = u1.Id + m1.NotifyProps = model.GetDefaultChannelNotifyProps() + + m2 := model.ChannelMember{} + m2.ChannelId = o1.Id + m2.UserId = u2.Id + m2.NotifyProps = model.GetDefaultChannelNotifyProps() + + <-ss.Channel().SaveDirectChannel(&o1, &m1, &m2) + + r1 := <-ss.Channel().GetAllDirectChannelsForExportAfter(10000, strings.Repeat("0", 26)) + assert.Nil(t, r1.Err) + d1 := r1.Data.([]*model.DirectChannelForExport) + + assert.Equal(t, 2, len(d1)) + assert.ElementsMatch(t, []string{o1.DisplayName, o2.DisplayName}, []string{d1[0].DisplayName, d1[1].DisplayName}) + + // Manually truncate Channels table until testlib can handle cleanups + s.GetMaster().Exec("TRUNCATE Channels") +} + +func testChannelStoreExportAllDirectChannelsExcludePrivateAndPublic(t *testing.T, ss store.Store, s SqlSupplier) { + teamId := model.NewId() + + o1 := model.Channel{} + o1.TeamId = teamId + o1.DisplayName = "The Direct Channel" + model.NewId() + o1.Name = "zz" + model.NewId() + "b" + o1.Type = model.CHANNEL_DIRECT + + o2 := model.Channel{} + o2.TeamId = teamId + o2.DisplayName = "Channel2" + model.NewId() + o2.Name = "zz" + model.NewId() + "b" + o2.Type = model.CHANNEL_OPEN + store.Must(ss.Channel().Save(&o2, -1)) + + o3 := model.Channel{} + o3.TeamId = teamId + o3.DisplayName = "Channel3" + model.NewId() + o3.Name = "zz" + model.NewId() + "b" + o3.Type = model.CHANNEL_PRIVATE + store.Must(ss.Channel().Save(&o3, -1)) + + u1 := &model.User{} + u1.Email = MakeEmail() + u1.Nickname = model.NewId() + store.Must(ss.User().Save(u1)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)) + + u2 := &model.User{} + u2.Email = MakeEmail() + u2.Nickname = model.NewId() + store.Must(ss.User().Save(u2)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = u1.Id + m1.NotifyProps = model.GetDefaultChannelNotifyProps() + + m2 := model.ChannelMember{} + m2.ChannelId = o1.Id + m2.UserId = u2.Id + m2.NotifyProps = model.GetDefaultChannelNotifyProps() + + <-ss.Channel().SaveDirectChannel(&o1, &m1, &m2) + + r1 := <-ss.Channel().GetAllDirectChannelsForExportAfter(10000, strings.Repeat("0", 26)) + assert.Nil(t, r1.Err) + d1 := r1.Data.([]*model.DirectChannelForExport) + assert.Equal(t, 1, len(d1)) + assert.Equal(t, o1.DisplayName, d1[0].DisplayName) + + // Manually truncate Channels table until testlib can handle cleanups + s.GetMaster().Exec("TRUNCATE Channels") +} + +func testChannelStoreExportAllDirectChannelsDeletedChannel(t *testing.T, ss store.Store, s SqlSupplier) { + teamId := model.NewId() + + o1 := model.Channel{} + o1.TeamId = teamId + o1.DisplayName = "Different Name" + model.NewId() + o1.Name = "zz" + model.NewId() + "b" + o1.Type = model.CHANNEL_DIRECT + + u1 := &model.User{} + u1.Email = MakeEmail() + u1.Nickname = model.NewId() + store.Must(ss.User().Save(u1)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)) + + u2 := &model.User{} + u2.Email = MakeEmail() + u2.Nickname = model.NewId() + store.Must(ss.User().Save(u2)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = u1.Id + m1.NotifyProps = model.GetDefaultChannelNotifyProps() + + m2 := model.ChannelMember{} + m2.ChannelId = o1.Id + m2.UserId = u2.Id + m2.NotifyProps = model.GetDefaultChannelNotifyProps() + + result := <-ss.Channel().SaveDirectChannel(&o1, &m1, &m2) + + o1.DeleteAt = 1 + result = <-ss.Channel().SetDeleteAt(o1.Id, 1, 1) + assert.Nil(t, result.Err) + + r1 := <-ss.Channel().GetAllDirectChannelsForExportAfter(10000, strings.Repeat("0", 26)) + assert.Nil(t, r1.Err) + d1 := r1.Data.([]*model.DirectChannelForExport) + + assert.Equal(t, 0, len(d1)) + + // Manually truncate Channels table until testlib can handle cleanups + s.GetMaster().Exec("TRUNCATE Channels") +} diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 5a202bb34d..81b0a2af43 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -226,6 +226,22 @@ func (_m *ChannelStore) GetAllChannelsForExportAfter(limit int, afterId string) return r0 } +// GetAllDirectChannelsForExportAfter provides a mock function with given fields: limit, afterId +func (_m *ChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterId string) store.StoreChannel { + ret := _m.Called(limit, afterId) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(int, string) store.StoreChannel); ok { + r0 = rf(limit, afterId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + // GetByName provides a mock function with given fields: team_id, name, allowFromCache func (_m *ChannelStore) GetByName(team_id string, name string, allowFromCache bool) store.StoreChannel { ret := _m.Called(team_id, name, allowFromCache) diff --git a/store/storetest/mocks/PostStore.go b/store/storetest/mocks/PostStore.go index 2e4d63089b..973aff997e 100644 --- a/store/storetest/mocks/PostStore.go +++ b/store/storetest/mocks/PostStore.go @@ -470,3 +470,18 @@ func (_m *PostStore) Update(newPost *model.Post, oldPost *model.Post) store.Stor return r0 } + +func (_m *PostStore) GetDirectPostParentsForExportAfter(limit int, afterId string) store.StoreChannel { + ret := _m.Called(limit, afterId) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(int, string) store.StoreChannel); ok { + r0 = rf(limit, afterId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} diff --git a/store/storetest/post_store.go b/store/storetest/post_store.go index a51b2f4baa..ff8deb2060 100644 --- a/store/storetest/post_store.go +++ b/store/storetest/post_store.go @@ -5,6 +5,7 @@ package storetest import ( "fmt" + "sort" "strings" "testing" "time" @@ -16,7 +17,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestPostStore(t *testing.T, ss store.Store) { +func TestPostStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("Save", func(t *testing.T) { testPostStoreSave(t, ss) }) t.Run("SaveAndUpdateChannelMsgCounts", func(t *testing.T) { testPostStoreSaveChannelMsgCounts(t, ss) }) t.Run("Get", func(t *testing.T) { testPostStoreGet(t, ss) }) @@ -35,7 +36,7 @@ func TestPostStore(t *testing.T, ss store.Store) { t.Run("Search", func(t *testing.T) { testPostStoreSearch(t, ss) }) t.Run("UserCountsWithPostsByDay", func(t *testing.T) { testUserCountsWithPostsByDay(t, ss) }) t.Run("PostCountsByDay", func(t *testing.T) { testPostCountsByDay(t, ss) }) - t.Run("GetFlaggedPostsForTeam", func(t *testing.T) { testPostStoreGetFlaggedPostsForTeam(t, ss) }) + t.Run("GetFlaggedPostsForTeam", func(t *testing.T) { testPostStoreGetFlaggedPostsForTeam(t, ss, s) }) t.Run("GetFlaggedPosts", func(t *testing.T) { testPostStoreGetFlaggedPosts(t, ss) }) t.Run("GetFlaggedPostsForChannel", func(t *testing.T) { testPostStoreGetFlaggedPostsForChannel(t, ss) }) t.Run("GetPostsCreatedAt", func(t *testing.T) { testPostStoreGetPostsCreatedAt(t, ss) }) @@ -47,6 +48,9 @@ func TestPostStore(t *testing.T, ss store.Store) { t.Run("TestGetMaxPostSize", func(t *testing.T) { testGetMaxPostSize(t, ss) }) t.Run("GetParentsForExportAfter", func(t *testing.T) { testPostStoreGetParentsForExportAfter(t, ss) }) t.Run("GetRepliesForExport", func(t *testing.T) { testPostStoreGetRepliesForExport(t, ss) }) + t.Run("GetDirectPostParentsForExportAfter", func(t *testing.T) { testPostStoreGetDirectPostParentsForExportAfter(t, ss, s) }) + t.Run("GetDirectPostParentsForExportAfterDeleted", func(t *testing.T) { testPostStoreGetDirectPostParentsForExportAfterDeleted(t, ss, s) }) + t.Run("GetDirectPostParentsForExportAfterBatched", func(t *testing.T) { testPostStoreGetDirectPostParentsForExportAfterBatched(t, ss, s) }) } func testPostStoreSave(t *testing.T, ss store.Store) { @@ -1192,7 +1196,7 @@ func testPostCountsByDay(t *testing.T, ss store.Store) { } } -func testPostStoreGetFlaggedPostsForTeam(t *testing.T, ss store.Store) { +func testPostStoreGetFlaggedPostsForTeam(t *testing.T, ss store.Store, s SqlSupplier) { c1 := &model.Channel{} c1.TeamId = model.NewId() c1.DisplayName = "Channel1" @@ -1365,6 +1369,9 @@ func testPostStoreGetFlaggedPostsForTeam(t *testing.T, ss store.Store) { if len(r4.Order) != 3 { t.Fatal("should have 3 posts") } + + // Manually truncate Channels table until testlib can handle cleanups + s.GetMaster().Exec("TRUNCATE Channels") } func testPostStoreGetFlaggedPosts(t *testing.T, ss store.Store) { @@ -1972,3 +1979,190 @@ func testPostStoreGetRepliesForExport(t *testing.T, ss store.Store) { assert.Equal(t, reply1.Username, u1.Username) } + +func testPostStoreGetDirectPostParentsForExportAfter(t *testing.T, ss store.Store, s SqlSupplier) { + teamId := model.NewId() + + o1 := model.Channel{} + o1.TeamId = teamId + o1.DisplayName = "Name" + o1.Name = "zz" + model.NewId() + "b" + o1.Type = model.CHANNEL_DIRECT + + u1 := &model.User{} + u1.Email = MakeEmail() + u1.Nickname = model.NewId() + store.Must(ss.User().Save(u1)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)) + + u2 := &model.User{} + u2.Email = MakeEmail() + u2.Nickname = model.NewId() + store.Must(ss.User().Save(u2)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = u1.Id + m1.NotifyProps = model.GetDefaultChannelNotifyProps() + + m2 := model.ChannelMember{} + m2.ChannelId = o1.Id + m2.UserId = u2.Id + m2.NotifyProps = model.GetDefaultChannelNotifyProps() + + <-ss.Channel().SaveDirectChannel(&o1, &m1, &m2) + + p1 := &model.Post{} + p1.ChannelId = o1.Id + p1.UserId = u1.Id + p1.Message = "zz" + model.NewId() + "AAAAAAAAAAA" + p1.CreateAt = 1000 + p1 = (<-ss.Post().Save(p1)).Data.(*model.Post) + + r1 := <-ss.Post().GetDirectPostParentsForExportAfter(10000, strings.Repeat("0", 26)) + assert.Nil(t, r1.Err) + d1 := r1.Data.([]*model.DirectPostForExport) + + assert.Equal(t, p1.Message, d1[0].Message) + + // Manually truncate Channels table until testlib can handle cleanups + s.GetMaster().Exec("TRUNCATE Channels") +} + +func testPostStoreGetDirectPostParentsForExportAfterDeleted(t *testing.T, ss store.Store, s SqlSupplier) { + teamId := model.NewId() + + o1 := model.Channel{} + o1.TeamId = teamId + o1.DisplayName = "Name" + o1.Name = "zz" + model.NewId() + "b" + o1.Type = model.CHANNEL_DIRECT + + u1 := &model.User{} + u1.DeleteAt = 1 + u1.Email = MakeEmail() + u1.Nickname = model.NewId() + store.Must(ss.User().Save(u1)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)) + + u2 := &model.User{} + u2.DeleteAt = 1 + u2.Email = MakeEmail() + u2.Nickname = model.NewId() + store.Must(ss.User().Save(u2)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = u1.Id + m1.NotifyProps = model.GetDefaultChannelNotifyProps() + + m2 := model.ChannelMember{} + m2.ChannelId = o1.Id + m2.UserId = u2.Id + m2.NotifyProps = model.GetDefaultChannelNotifyProps() + + <-ss.Channel().SaveDirectChannel(&o1, &m1, &m2) + + o1.DeleteAt = 1 + result := <-ss.Channel().SetDeleteAt(o1.Id, 1, 1) + assert.Nil(t, result.Err) + + p1 := &model.Post{} + p1.ChannelId = o1.Id + p1.UserId = u1.Id + p1.Message = "zz" + model.NewId() + "BBBBBBBBBBBB" + p1.CreateAt = 1000 + p1 = (<-ss.Post().Save(p1)).Data.(*model.Post) + + o1a := &model.Post{} + *o1a = *p1 + o1a.DeleteAt = 1 + o1a.Message = p1.Message + "BBBBBBBBBB" + if result := <-ss.Post().Update(o1a, p1); result.Err != nil { + t.Fatal(result.Err) + } + + r1 := <-ss.Post().GetDirectPostParentsForExportAfter(10000, strings.Repeat("0", 26)) + assert.Nil(t, r1.Err) + d1 := r1.Data.([]*model.DirectPostForExport) + + assert.Equal(t, 0, len(d1)) + + // Manually truncate Channels table until testlib can handle cleanups + s.GetMaster().Exec("TRUNCATE Channels") +} + +func testPostStoreGetDirectPostParentsForExportAfterBatched(t *testing.T, ss store.Store, s SqlSupplier) { + teamId := model.NewId() + + o1 := model.Channel{} + o1.TeamId = teamId + o1.DisplayName = "Name" + o1.Name = "zz" + model.NewId() + "b" + o1.Type = model.CHANNEL_DIRECT + + var postIds []string + for i := 0; i < 150; i++ { + u1 := &model.User{} + u1.Email = MakeEmail() + u1.Nickname = model.NewId() + store.Must(ss.User().Save(u1)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u1.Id}, -1)) + + u2 := &model.User{} + u2.Email = MakeEmail() + u2.Nickname = model.NewId() + store.Must(ss.User().Save(u2)) + store.Must(ss.Team().SaveMember(&model.TeamMember{TeamId: model.NewId(), UserId: u2.Id}, -1)) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = u1.Id + m1.NotifyProps = model.GetDefaultChannelNotifyProps() + + m2 := model.ChannelMember{} + m2.ChannelId = o1.Id + m2.UserId = u2.Id + m2.NotifyProps = model.GetDefaultChannelNotifyProps() + + <-ss.Channel().SaveDirectChannel(&o1, &m1, &m2) + + p1 := &model.Post{} + p1.ChannelId = o1.Id + p1.UserId = u1.Id + p1.Message = "zz" + model.NewId() + "AAAAAAAAAAA" + p1.CreateAt = 1000 + p1 = (<-ss.Post().Save(p1)).Data.(*model.Post) + postIds = append(postIds, p1.Id) + } + sort.Slice(postIds, func(i, j int) bool { return postIds[i] < postIds[j] }) + + // Get all posts + r1 := <-ss.Post().GetDirectPostParentsForExportAfter(10000, strings.Repeat("0", 26)) + assert.Nil(t, r1.Err) + d1 := r1.Data.([]*model.DirectPostForExport) + assert.Equal(t, len(postIds), len(d1)) + var exportedPostIds []string + for i := range d1 { + exportedPostIds = append(exportedPostIds, d1[i].Id) + } + sort.Slice(exportedPostIds, func(i, j int) bool { return exportedPostIds[i] < exportedPostIds[j] }) + assert.ElementsMatch(t, postIds, exportedPostIds) + + // Get 100 + r1 = <-ss.Post().GetDirectPostParentsForExportAfter(100, strings.Repeat("0", 26)) + assert.Nil(t, r1.Err) + d1 = r1.Data.([]*model.DirectPostForExport) + assert.Equal(t, 100, len(d1)) + exportedPostIds = []string{} + for i := range d1 { + exportedPostIds = append(exportedPostIds, d1[i].Id) + } + sort.Slice(exportedPostIds, func(i, j int) bool { return exportedPostIds[i] < exportedPostIds[j] }) + assert.ElementsMatch(t, postIds[:100], exportedPostIds) + + // Manually truncate Channels table until testlib can handle cleanups + s.GetMaster().Exec("TRUNCATE Channels") +}